17 KiB
17 KiB
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
- Consistency - Same language as Portal backend (Go)
- Single binary - No Node.js/Python runtime needed
- Fast - Compiles to native code, instant startup
- Cross-platform - Build for Windows, macOS, Linux from one codebase
- 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.
$ 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.
$ 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.
$ 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
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.
$ 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
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.
$ 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
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.
$ mosis login
Opening browser for authentication...
Waiting for authorization...
✓ Logged in as john@example.com
API key stored in ~/.mosis/credentials
Options
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.
$ 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
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.
$ 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.
$ 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.
$ 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
{
"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
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
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)
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
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)
brew tap mosis/tap
brew install mosis
Direct Download
# 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
$ mosis build
Error: manifest.json not found
Are you in a Mosis project directory?
Run 'mosis init' to create a new project.
Verbose Mode
$ 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
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
- CLI framework selected (Go + Cobra)
initcommand (template generation)validatecommandbuildcommand (ZIP package creation)signcommand (Ed25519 signing)runcommand (designer subprocess)login/logoutcommands (OAuth2 device flow)publishcommand (HTTP upload to Portal)statuscommandkeyssubcommands (generate, register, list)- Configuration management (viper)
- Cross-platform builds (goreleaser)
- CI/CD examples (GitHub Actions)
Open Questions
Should CLI auto-update itself?→ No, manual updates via package managerOffline mode for build/sign?→ Yes, build/sign work offlinePlugin system for custom commands?→ Defer to post-MVP- IDE integrations (VS Code extension)? → Consider for v1.1