723 lines
17 KiB
Markdown
723 lines
17 KiB
Markdown
# 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/)
|