# 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 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 [options] Options: -k, --key 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 Override entry point --port Designer port (default: 8080) --no-hot-reload Disable hot reload --device 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 Use API key instead of browser auth --portal 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 Use existing package --notes Release notes --notes-file 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/)