# Milestone 11: Developer CLI Tool

**Status**: Decided
**Goal**: Command-line tool for app development workflow.

## Decision

**Go + Cobra** for single-binary cross-platform CLI:

```
Framework:    Cobra (github.com/spf13/cobra)
Distribution: Single binary (no runtime dependencies)
Signing:      crypto/ed25519 (stdlib)
Auth:         OAuth2 device flow + API key storage
Portal URL:   Configurable (default: self-hosted Synology)
```

### Rationale

1. **Consistency** - Same language as Portal backend (Go)
2. **Single binary** - No Node.js/Python runtime needed
3. **Fast** - Compiles to native code, instant startup
4. **Cross-platform** - Build for Windows, macOS, Linux from one codebase
5. **Cobra ecosystem** - Shell completions, man pages, help generation

### Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│                         mosis CLI                                │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │  Cobra Commands                                              ││
│  │  ├── init      → Template generation                        ││
│  │  ├── build     → ZIP package creation                       ││
│  │  ├── sign      → Ed25519 signing (crypto/ed25519)           ││
│  │  ├── run       → Launch mosis-designer subprocess           ││
│  │  ├── login     → OAuth2 device flow → ~/.mosis/credentials  ││
│  │  └── publish   → HTTP client → Portal API                   ││
│  └───────────────────────────────────────────────────────────────┘│
│                              │                                   │
│  ~/.mosis/                   │                                   │
│  ├── config.json             │  Portal (Synology NAS)            │
│  ├── credentials             │  ┌──────────────────────────┐    │
│  ├── signing_key.pem ────────┼──│ POST /v1/versions        │    │
│  └── signing_key.pub         │  │ POST /auth/device        │    │
│                              │  └──────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘
```

---

## Overview

The CLI tool (`mosis`) streamlines the developer workflow: project creation, building, testing, signing, and publishing.

---

## Commands Overview

```
mosis
├── init          Create new app project
├── validate      Validate manifest and assets
├── build         Create .mosis package
├── sign          Sign package with developer key
├── run           Run in local designer/emulator
├── test          Run automated tests
├── login         Authenticate with portal
├── logout        Clear authentication
├── publish       Upload and submit for review
├── status        Check review status
├── keys
│   ├── generate  Generate signing keypair
│   ├── list      List registered keys
│   └── register  Upload public key to portal
└── config
    ├── get       Get config value
    └── set       Set config value
```

---

## Command Details

### `mosis init`

Create a new app project with boilerplate.

```bash
$ mosis init

? App name: My Calculator
? Package ID: com.myname.calculator
? Description: A simple calculator app
? Author name: John Doe
? Author email: john@example.com

Creating project structure...

✓ Created manifest.json
✓ Created assets/main.rml
✓ Created assets/styles/theme.rcss
✓ Created assets/scripts/app.lua
✓ Created icons/ (placeholder icons)

Project created! Next steps:
  cd my-calculator
  mosis run          # Preview in designer
  mosis build        # Create package
  mosis publish      # Submit to store
```

#### Generated Structure

```
my-calculator/
├── manifest.json
├── assets/
│   ├── main.rml
│   ├── styles/
│   │   └── theme.rcss
│   └── scripts/
│       └── app.lua
├── icons/
│   ├── icon-32.png
│   ├── icon-64.png
│   └── icon-128.png
└── .mosisignore      # Files to exclude from package
```

---

### `mosis validate`

Validate project without building.

```bash
$ mosis validate

Validating manifest.json...
✓ Required fields present
✓ Package ID format valid
✓ Version format valid

Validating assets...
✓ Entry point exists: assets/main.rml
✓ All RML files valid (3 files)
✓ All RCSS files valid (2 files)
✓ All Lua files valid (4 files)

Validating icons...
✓ icon-32.png (32x32)
✓ icon-64.png (64x64)
✓ icon-128.png (128x128)

Checking permissions...
✓ Permissions declared: storage, network

All validations passed!
```

---

### `mosis build`

Create a .mosis package.

```bash
$ mosis build

Reading manifest.json...
Package: com.myname.calculator v1.0.0 (1)

Collecting files...
✓ manifest.json
✓ assets/main.rml
✓ assets/styles/theme.rcss
✓ assets/scripts/app.lua
✓ icons/icon-32.png
✓ icons/icon-64.png
✓ icons/icon-128.png

Creating package...
✓ Package created: dist/com.myname.calculator-1.0.0.mosis (45.2 KB)

⚠ Package is unsigned. Run 'mosis sign' before publishing.
```

#### Options

```bash
mosis build [options]

Options:
  -o, --output <path>   Output path (default: dist/)
  --no-compress         Skip compression
  --include-source      Include .lua source maps
```

---

### `mosis sign`

Sign a package with developer key.

```bash
$ mosis sign dist/com.myname.calculator-1.0.0.mosis

Using key: ~/.mosis/signing_key.pem
Fingerprint: SHA256:abc123...

Generating file hashes...
Signing MANIFEST.MF...

✓ Package signed: dist/com.myname.calculator-1.0.0.mosis

Signature details:
  Algorithm: Ed25519
  Key fingerprint: SHA256:abc123...
  Files signed: 7
```

#### Options

```bash
mosis sign <package> [options]

Options:
  -k, --key <path>      Path to private key (default: ~/.mosis/signing_key.pem)
  --verify              Verify existing signature
```

---

### `mosis run`

Launch in desktop designer for testing.

```bash
$ mosis run

Starting Mosis Designer...
Loading: assets/main.rml

Designer running at http://localhost:8080
Press Ctrl+C to stop

[Hot reload enabled - changes auto-refresh]
```

#### Options

```bash
mosis run [options]

Options:
  --entry <file>        Override entry point
  --port <number>       Designer port (default: 8080)
  --no-hot-reload       Disable hot reload
  --device <name>       Emulate device (phone, tablet)
```

---

### `mosis login`

Authenticate with developer portal.

```bash
$ mosis login

Opening browser for authentication...
Waiting for authorization...

✓ Logged in as john@example.com
  API key stored in ~/.mosis/credentials
```

#### Options

```bash
mosis login [options]

Options:
  --api-key <key>       Use API key instead of browser auth
  --portal <url>        Portal URL (default: https://portal.mosis.dev)
```

---

### `mosis publish`

Upload and submit for review.

```bash
$ mosis publish

Checking authentication...
✓ Logged in as john@example.com

Building package...
✓ Package created: dist/com.myname.calculator-1.0.0.mosis

Signing package...
✓ Package signed

Uploading...
████████████████████████████████ 100%

Submitting for review...
✓ Version 1.0.0 submitted

Review status: In Review
Estimated review time: 24 hours

Track status: mosis status
```

#### Options

```bash
mosis publish [options]

Options:
  --package <path>      Use existing package
  --notes <text>        Release notes
  --notes-file <path>   Release notes from file
  --draft               Upload without submitting for review
```

---

### `mosis status`

Check app/version status.

```bash
$ mosis status

App: My Calculator (com.myname.calculator)

Versions:
  v1.0.0 (1)  Published    Jan 10, 2024    1,234 downloads
  v1.1.0 (2)  In Review    Jan 15, 2024    Submitted 2h ago

Latest review:
  Status: In Review
  Submitted: Jan 15, 2024 10:30 AM
  Estimated completion: Jan 16, 2024

Run 'mosis status --watch' to monitor in real-time.
```

---

### `mosis keys generate`

Generate Ed25519 signing keypair.

```bash
$ mosis keys generate

Generating Ed25519 keypair...

Private key saved to: ~/.mosis/signing_key.pem
Public key saved to: ~/.mosis/signing_key.pub

Fingerprint: SHA256:abc123def456...

⚠ IMPORTANT: Keep your private key secure!
  - Never share or commit signing_key.pem
  - Back it up securely
  - If compromised, revoke immediately

Next step: Register your public key
  mosis keys register
```

---

### `mosis keys register`

Upload public key to portal.

```bash
$ mosis keys register

Reading public key from ~/.mosis/signing_key.pub
Fingerprint: SHA256:abc123def456...

? Key name: MacBook Pro 2024

Uploading to portal...
✓ Key registered successfully

Your signing key is now active. Packages signed with this
key will be accepted for review.
```

---

## Configuration

### Config File Location

```
~/.mosis/
├── config.json       # CLI configuration
├── credentials       # Auth tokens (encrypted)
├── signing_key.pem   # Private key
└── signing_key.pub   # Public key
```

### Config Options

```json
{
  "portal_url": "https://portal.mosis.dev",
  "api_url": "https://api.mosis.dev",
  "designer_path": "/usr/local/bin/mosis-designer",
  "default_author": {
    "name": "John Doe",
    "email": "john@example.com"
  }
}
```

---

## Implementation (Go + Cobra)

### Main Entry Point

```go
package main

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

func main() {
    rootCmd := &cobra.Command{
        Use:   "mosis",
        Short: "Mosis app development CLI",
        Long:  "CLI tool for building, signing, and publishing Mosis apps",
    }

    // Global flags
    rootCmd.PersistentFlags().StringP("portal", "p", "", "Portal URL (default from config)")
    rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Verbose output")

    // Commands
    rootCmd.AddCommand(initCmd())
    rootCmd.AddCommand(buildCmd())
    rootCmd.AddCommand(signCmd())
    rootCmd.AddCommand(runCmd())
    rootCmd.AddCommand(loginCmd())
    rootCmd.AddCommand(publishCmd())
    rootCmd.AddCommand(statusCmd())
    rootCmd.AddCommand(keysCmd())
    rootCmd.AddCommand(configCmd())

    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
```

### Build Command Example

```go
func buildCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "build",
        Short: "Create .mosis package",
        RunE: func(cmd *cobra.Command, args []string) error {
            manifest, err := readManifest("manifest.json")
            if err != nil {
                return fmt.Errorf("manifest.json not found\n\nAre you in a Mosis project directory?")
            }

            output, _ := cmd.Flags().GetString("output")
            if output == "" {
                output = fmt.Sprintf("dist/%s-%s.mosis", manifest.PackageID, manifest.Version)
            }

            fmt.Printf("Building %s v%s...\n", manifest.Name, manifest.Version)

            files, err := collectFiles(manifest)
            if err != nil {
                return err
            }

            if err := createPackage(files, output); err != nil {
                return err
            }

            info, _ := os.Stat(output)
            fmt.Printf("✓ Package created: %s (%.1f KB)\n", output, float64(info.Size())/1024)
            fmt.Println("\n⚠ Package is unsigned. Run 'mosis sign' before publishing.")
            return nil
        },
    }

    cmd.Flags().StringP("output", "o", "", "Output path (default: dist/)")
    return cmd
}
```

### OAuth2 Device Flow (Login)

```go
func loginCmd() *cobra.Command {
    return &cobra.Command{
        Use:   "login",
        Short: "Authenticate with developer portal",
        RunE: func(cmd *cobra.Command, args []string) error {
            portalURL := viper.GetString("portal_url")

            // Start device flow
            resp, err := http.Post(portalURL+"/auth/device", "application/json", nil)
            if err != nil {
                return err
            }
            var device DeviceResponse
            json.NewDecoder(resp.Body).Decode(&device)

            fmt.Printf("Go to: %s\n", device.VerificationURI)
            fmt.Printf("Enter code: %s\n\n", device.UserCode)
            fmt.Println("Waiting for authorization...")

            // Poll for token
            token, err := pollForToken(portalURL, device.DeviceCode, device.Interval)
            if err != nil {
                return err
            }

            // Save credentials
            if err := saveCredentials(token); err != nil {
                return err
            }

            fmt.Printf("✓ Logged in as %s\n", token.Email)
            return nil
        },
    }
}
```

### Ed25519 Signing

```go
func signPackage(packagePath, keyPath string) error {
    // Read private key
    keyPEM, err := os.ReadFile(keyPath)
    if err != nil {
        return fmt.Errorf("failed to read key: %w", err)
    }

    block, _ := pem.Decode(keyPEM)
    privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        return fmt.Errorf("invalid key format: %w", err)
    }

    ed25519Key := privateKey.(ed25519.PrivateKey)

    // Generate MANIFEST.MF with file hashes
    manifest, err := generateManifest(packagePath)
    if err != nil {
        return err
    }

    // Sign manifest
    signature := ed25519.Sign(ed25519Key, manifest)

    // Add signature to package
    return addSignatureToPackage(packagePath, manifest, signature)
}

---

## Distribution

### npm (Node.js version)

```bash
npm install -g @mosis/cli
```

### Homebrew (macOS)

```bash
brew tap mosis/tap
brew install mosis
```

### Direct Download

```bash
# Linux/macOS
curl -fsSL https://mosis.dev/install.sh | sh

# Windows
irm https://mosis.dev/install.ps1 | iex
```

### Package Managers

| Platform | Package Manager | Command |
|----------|-----------------|---------|
| macOS | Homebrew | `brew install mosis` |
| Windows | Scoop | `scoop install mosis` |
| Linux | apt (deb) | `apt install mosis` |
| Any | npm | `npm install -g @mosis/cli` |

---

## Error Handling

### User-Friendly Errors

```bash
$ mosis build

Error: manifest.json not found

Are you in a Mosis project directory?
Run 'mosis init' to create a new project.
```

### Verbose Mode

```bash
$ mosis build --verbose

[DEBUG] Reading manifest from ./manifest.json
[DEBUG] Manifest parsed: {id: "com.example.app", ...}
[DEBUG] Collecting files from ./assets
[DEBUG] Found 7 files
[DEBUG] Creating ZIP archive
[DEBUG] Writing to dist/com.example.app-1.0.0.mosis
[DEBUG] Package size: 45234 bytes
✓ Package created
```

---

## CI/CD Integration

### GitHub Actions

```yaml
name: Build and Publish

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Mosis CLI
        run: npm install -g @mosis/cli

      - name: Build and Sign
        env:
          MOSIS_SIGNING_KEY: ${{ secrets.MOSIS_SIGNING_KEY }}
        run: |
          echo "$MOSIS_SIGNING_KEY" > signing_key.pem
          mosis build
          mosis sign dist/*.mosis --key signing_key.pem

      - name: Publish
        env:
          MOSIS_API_KEY: ${{ secrets.MOSIS_API_KEY }}
        run: mosis publish --api-key "$MOSIS_API_KEY"
```

---

## Deliverables

- [x] CLI framework selected (Go + Cobra)
- [ ] `init` command (template generation)
- [ ] `validate` command
- [ ] `build` command (ZIP package creation)
- [ ] `sign` command (Ed25519 signing)
- [ ] `run` command (designer subprocess)
- [ ] `login/logout` commands (OAuth2 device flow)
- [ ] `publish` command (HTTP upload to Portal)
- [ ] `status` command
- [ ] `keys` subcommands (generate, register, list)
- [ ] Configuration management (viper)
- [ ] Cross-platform builds (goreleaser)
- [ ] CI/CD examples (GitHub Actions)

---

## Open Questions

1. ~~Should CLI auto-update itself?~~ → No, manual updates via package manager
2. ~~Offline mode for build/sign?~~ → Yes, build/sign work offline
3. ~~Plugin system for custom commands?~~ → Defer to post-MVP
4. IDE integrations (VS Code extension)? → Consider for v1.1

---

## References

- [Cobra CLI Framework](https://cobra.dev/)
- [oclif Framework](https://oclif.io/)
- [Clap for Rust](https://docs.rs/clap/)
