diff --git a/.planning/sb-docs-questionnaire.md b/.planning/sb-docs-questionnaire.md new file mode 100644 index 00000000..bce0b9dc --- /dev/null +++ b/.planning/sb-docs-questionnaire.md @@ -0,0 +1,487 @@ +# Springboard `sb docs` Implementation Questionnaire + +This document contains questions that need answers to implement each `sb docs` command. Please fill in the answers to guide implementation. + +--- + +## General Architecture Questions + +### Q1: Documentation Source Location + +Where should the documentation content come from? + +**Options:** +- [ ] A. Bundle markdown files directly in the CLI npm package (like examples) +- [ ] B. Read from `/doks/content/docs/` at runtime (requires doks to be present) +- [ ] C. Host documentation at a URL (e.g., `docs.springboard.dev/llms.txt`) and fetch at runtime +- [ ] D. Other: _______________ + +**Your Answer:** + +--- + +### Q2: Documentation Format + +What format should docs be in for AI consumption? + +**Options:** +- [ ] A. Raw markdown files from doks (as-is) +- [ ] B. LLM-optimized condensed format (like svelte-mcp's `/llms.txt`) +- [ ] C. Both - raw for humans, condensed for `sb docs get` +- [ ] D. Other: _______________ + +**Your Answer:** + +--- + +### Q3: Use Cases Metadata + +svelte-mcp pre-generates "use_cases" keywords for each doc section using Claude Batch API. Should we do the same? + +**Options:** +- [ ] A. Yes, generate `use_cases.json` via Claude Batch API +- [ ] B. Manually write use cases for each section +- [ ] C. Skip use cases, just use doc titles +- [ ] D. Use categories/tags instead of free-form keywords + +**Your Answer:** + +--- + +## Command-Specific Questions + +--- + +## `sb docs list` Command + +### Q4: What documentation sections should be listed? + +Based on the doks content, I found these potential sections: + +**Springboard Core:** +- [ ] `springboard/overview` - Framework introduction +- [ ] `springboard/module-development` - Module types (feature/utility/initializer) +- [ ] `springboard/state-management` - createSharedState, createPersistentState, createUserAgentState +- [ ] `springboard/actions` - createAction, createActions, RPC behavior +- [ ] `springboard/routing` - registerRoute, documentMeta, hideApplicationShell +- [ ] `springboard/lifecycle` - onDestroy, initialization order +- [ ] `springboard/module-communication` - getModule, interface merging +- [ ] `springboard/core-dependencies` - log, showError, files, storage, rpc, isMaestro +- [ ] `springboard/platforms` - browser, node, react-native, desktop, partykit +- [ ] `springboard/conditional-compilation` - @platform directives +- [ ] `springboard/server-modules` - serverRegistry, Hono routes, RPC middleware + +**Guides:** +- [ ] `guides/registering-ui-routes` - Route registration patterns +- [ ] `guides/creating-feature-module` - Feature module walkthrough +- [ ] `guides/creating-utility-module` - Utility module walkthrough +- [ ] `guides/multi-module-apps` - Module dependencies and communication +- [ ] `guides/offline-first` - Offline mode patterns +- [ ] `guides/multi-workspace` - Workspace context patterns + +**CLI:** +- [ ] `cli/commands` - sb dev, sb build, sb start +- [ ] `cli/plugins` - Plugin system + +**JamTools (if applicable):** +- [ ] `jamtools/overview` - MIDI/IO extensions +- [ ] `jamtools/macros` - Macro system + +**Which sections should be included?** (Check all that apply or add more) + +**Your Answer:** + +--- + +### Q5: Section Metadata Format + +What metadata should each section have in `sb docs list`? + +``` +Option A (svelte-mcp style): +springboard/state-management + Use cases: shared state, persistent state, user agent state, createSharedState, cross-device sync... + +Option B (Category-based): +springboard/state-management + Category: core + Tags: state, sync, persistence + +Option C (Minimal): +springboard/state-management - State management with createSharedState, createPersistentState, createUserAgentState +``` + +**Your Answer:** + +--- + +## `sb docs get` Command + +### Q6: Documentation Fetching Behavior + +When `sb docs get springboard/state-management` is called: + +**Options:** +- [ ] A. Output raw markdown file content +- [ ] B. Output condensed LLM-optimized version +- [ ] C. Output with examples included inline +- [ ] D. Other: _______________ + +**Your Answer:** + +--- + +### Q7: Multiple Section Fetching + +Should `sb docs get` support fetching multiple sections at once? + +```bash +# Option A: Multiple arguments +sb docs get springboard/state-management springboard/actions + +# Option B: Single section only +sb docs get springboard/state-management + +# Option C: Category/tag based +sb docs get --category=core +``` + +**Your Answer:** + +--- + +## `sb docs context` Command + +### Q8: Context Prompt Content + +What should `sb docs context` include? + +**Options (check all that apply):** +- [ ] Framework overview (what is Springboard) +- [ ] Full list of available doc sections with use cases +- [ ] Key APIs summary (ModuleAPI methods) +- [ ] Common patterns and anti-patterns +- [ ] Workflow instructions (when to use each command) +- [ ] TypeScript type definitions +- [ ] Example code snippets +- [ ] Link to examples command + +**Your Answer:** + +--- + +### Q9: Context Length + +How long should the context output be? + +**Options:** +- [ ] A. Minimal (~500 tokens) - just workflow + doc list +- [ ] B. Medium (~2000 tokens) - workflow + doc list + key APIs +- [ ] C. Comprehensive (~5000 tokens) - everything including examples +- [ ] D. Configurable via flag (e.g., `--full`, `--minimal`) + +**Your Answer:** + +--- + +## `sb docs validate` Command + +### Q10: Validation Scope + +What should `sb docs validate` check for? + +**From songdrive analysis, common patterns/issues:** + +**Module Structure:** +- [ ] Module has return type matching AllModules interface declaration +- [ ] Module uses proper state creation methods +- [ ] Module registers cleanup via onDestroy when needed + +**State Patterns:** +- [ ] Using setState/setStateImmer correctly (not direct mutation) +- [ ] Correct state type chosen (shared vs persistent vs userAgent) +- [ ] State namespacing follows conventions + +**Actions:** +- [ ] Actions are async functions +- [ ] Actions properly handle errors +- [ ] Actions have proper TypeScript types + +**Routing:** +- [ ] Routes don't conflict +- [ ] Routes have proper components + +**Module Communication:** +- [ ] getModule called inside routes/actions (not at module level) +- [ ] Optional chaining used for potentially undefined modules + +**Anti-patterns:** +- [ ] Computed values stored in state (should use useMemo) +- [ ] Race conditions in initialization + +**Which checks should be implemented?** (Prioritize) + +**Your Answer:** + +--- + +### Q11: Validation Output Format + +How should validation results be formatted? + +**Options:** +- [ ] A. JSON only (for programmatic use) +- [ ] B. Human-readable by default, JSON with `--json` flag +- [ ] C. ESLint-style output (file:line:col message) + +**Your Answer:** + +--- + +### Q12: Validation Strictness + +Should validation have severity levels? + +``` +Option A: Binary (issues = errors, suggestions = warnings) +{ + "issues": ["Line 15: getModule called at module level"], + "suggestions": ["Line 8: Consider using setStateImmer for complex updates"], + "hasErrors": true +} + +Option B: Severity levels +{ + "errors": [...], + "warnings": [...], + "info": [...], + "hasErrors": true +} +``` + +**Your Answer:** + +--- + +## `sb docs types` Command + +### Q13: Which Types to Output + +What TypeScript definitions should `sb docs types` output? + +**Options (check all that apply):** +- [ ] ModuleAPI interface +- [ ] StatesAPI interface +- [ ] StateSupervisor interface +- [ ] CoreDependencies interface +- [ ] ModuleDependencies interface +- [ ] RegisterRouteOptions type +- [ ] ActionCallOptions type +- [ ] KVStore interface +- [ ] Rpc interface +- [ ] AllModules declaration pattern +- [ ] ServerModuleAPI interface + +**Your Answer:** + +--- + +### Q14: Types Format + +How should types be output? + +**Options:** +- [ ] A. Single concatenated TypeScript file +- [ ] B. Structured with headers/comments explaining each type +- [ ] C. Just the interface definitions (no explanations) +- [ ] D. Interactive (show list, pick which to output) + +**Your Answer:** + +--- + +## `sb docs scaffold` Command + +### Q15: Should Scaffold Remain? + +svelte-mcp does NOT have scaffolding. Should we keep it? + +**Options:** +- [ ] A. Remove scaffold - follow svelte-mcp pattern exactly +- [ ] B. Keep scaffold - useful for Springboard's module structure +- [ ] C. Replace with examples only - agents copy from examples +- [ ] D. Keep scaffold but simplify to one command + +**Your Answer:** + +--- + +### Q16: Scaffold Templates (if keeping) + +If keeping scaffold, what templates should be available? + +**Options (check all that apply):** +- [ ] `sb docs scaffold module ` - Basic module +- [ ] `sb docs scaffold feature ` - Feature module with routes +- [ ] `sb docs scaffold utility ` - Utility module with exports +- [ ] `sb docs scaffold initializer ` - Initializer module +- [ ] `sb docs scaffold server ` - Server module + +**Your Answer:** + +--- + +## `sb docs examples` Command + +### Q17: Example Categories + +What example categories are most useful? + +**Current examples:** +- [x] basic-feature-module (shared state + actions + routes) +- [x] persistent-state-module (database-backed state) +- [x] user-agent-state-module (localStorage state) + +**Additional examples to add:** +- [ ] utility-module (exports for other modules) +- [ ] server-module (Hono routes + RPC middleware) +- [ ] module-communication (getModule patterns) +- [ ] modal-integration (using modals module) +- [ ] navigation-patterns (programmatic navigation) +- [ ] offline-first-module (offline mode handling) +- [ ] workspace-aware-module (multi-tenant patterns) +- [ ] platform-specific-module (@platform directives) + +**Which examples should be included?** + +**Your Answer:** + +--- + +## Implementation Priority + +### Q18: Command Priority + +In what order should commands be implemented? + +**Rank 1-7 (1 = highest priority):** +- [ ] `sb docs context` - Primary entry point for agents +- [ ] `sb docs list` - Documentation discovery +- [ ] `sb docs get` - Documentation fetching +- [ ] `sb docs validate` - Code validation +- [ ] `sb docs types` - TypeScript definitions +- [ ] `sb docs examples` - (Already implemented) +- [ ] `sb docs scaffold` - Template generation + +**Your Answer:** + +--- + +### Q19: MVP vs Full Implementation + +What's the minimum viable implementation? + +**Options:** +- [ ] A. Just `context` + `examples` (agents use examples as templates) +- [ ] B. `context` + `list` + `get` (documentation access) +- [ ] C. `context` + `list` + `get` + `validate` (full workflow) +- [ ] D. Everything (all commands fully implemented) + +**Your Answer:** + +--- + +## Integration Questions + +### Q20: CLAUDE.md / AGENTS.md Updates + +Should the generated CLAUDE.md/AGENTS.md files be updated to match what commands are actually implemented? + +**Current content tells agents to:** +1. Run `sb docs context` +2. Use `sb docs validate` +3. Use `sb docs get` +4. Use `sb docs scaffold` + +**Should we update these files as we implement each command?** + +**Your Answer:** + +--- + +### Q21: Documentation Hosting + +If documentation is bundled in the npm package: + +**Questions:** +1. Maximum acceptable package size? _____ MB +2. Should docs be compressed? Yes / No +3. Should docs auto-update somehow? Yes / No / How: _____ + +**Your Answer:** + +--- + +### Q22: Versioning Strategy + +How should documentation versioning work? + +**Options:** +- [ ] A. Docs version matches CLI version (bundled) +- [ ] B. Docs fetched from latest (always current) +- [ ] C. Docs version configurable via flag +- [ ] D. Not concerned about versioning + +**Your Answer:** + +--- + +## Additional Questions + +### Q23: Anything Missing? + +Are there other commands or features that should be added? + +**Suggestions from songdrive patterns:** +- `sb docs patterns` - Common patterns (navigation reasons, modal system) +- `sb docs anti-patterns` - Things to avoid +- `sb docs migrate` - Migration helpers between versions +- `sb docs debug` - Debugging tips + +**Your Answer:** + +--- + +### Q24: CLI Name Preference + +Is `sb docs` the right namespace? + +**Options:** +- [ ] A. `sb docs` (current) +- [ ] B. `sb ai` (explicit AI agent focus) +- [ ] C. `sb help` (general help system) +- [ ] D. `sb llm` (explicit LLM focus) +- [ ] E. Other: _______________ + +**Your Answer:** + +--- + +### Q25: Additional Context + +Anything else I should know about how you want this to work? + +**Your Answer:** + +--- + +## Summary of Key Decisions Needed + +| Question | Topic | Options | +|----------|-------|---------| +| Q1 | Doc source | Bundle / Runtime / Hosted | +| Q3 | Use cases | Claude Batch / Manual / Skip | +| Q10 | Validation checks | Which patterns to detect | +| Q15 | Scaffold | Keep / Remove / Replace | +| Q18 | Priority | Implementation order | +| Q19 | MVP scope | Minimum viable set | diff --git a/.planning/sb-docs-vs-svelte-mcp.md b/.planning/sb-docs-vs-svelte-mcp.md new file mode 100644 index 00000000..03bce577 --- /dev/null +++ b/.planning/sb-docs-vs-svelte-mcp.md @@ -0,0 +1,85 @@ +# sb docs vs svelte-mcp Command Comparison + +## What svelte-mcp Has (MCP Tools) + +| Tool | Purpose | Status in sb docs | +|------|---------|-------------------| +| `list-sections` | List docs with use_cases | ✅ `sb docs list` | +| `get-documentation` | Fetch specific docs | ✅ `sb docs get` | +| `svelte-autofixer` | Validate component code | ✅ `sb docs validate` | +| `playground-link` | Generate svelte.dev playground URL | ❌ Not implemented | +| **Prompt: `svelte-task`** | Pre-load context + workflow | ✅ `sb docs context` | + +## What sb docs Has (CLI Commands) + +| Command | Purpose | Status in svelte-mcp | +|---------|---------|---------------------| +| `sb docs list` | List docs with use_cases | ✅ `list-sections` tool | +| `sb docs get` | Fetch specific docs | ✅ `get-documentation` tool | +| `sb docs validate` | Validate module code | ✅ `svelte-autofixer` tool | +| `sb docs context` | Full context prompt | ✅ `svelte-task` prompt | +| `sb docs types` | Output TypeScript types | ❌ Not in svelte-mcp | +| `sb docs scaffold` | Generate templates | ❌ Not in svelte-mcp | +| `sb docs --help` | Onboarding/workflow | ❌ Not in svelte-mcp (implicit in prompts) | + +## Key Differences + +### 1. Delivery Mechanism +- **svelte-mcp**: MCP protocol (tools + prompts) +- **sb docs**: CLI commands (shell execution) + +### 2. Scaffold/Templates +- **svelte-mcp**: No scaffolding - uses `playground-link` to share code +- **sb docs**: Has `scaffold` subcommands (diverges from svelte-mcp) + +### 3. Type Definitions +- **svelte-mcp**: No dedicated types command +- **sb docs**: `types` command to output ModuleAPI, StateSupervisor, etc. + +### 4. Onboarding +- **svelte-mcp**: Implicit in prompts (agents get workflow via `svelte-task` prompt) +- **sb docs**: Explicit via `--help` text + CLAUDE.md/AGENTS.md files + +### 5. Playground/Examples +- **svelte-mcp**: `playground-link` generates URLs to svelte.dev/playground + - Compresses code with gzip + base64 + - Embeds in MCP UI resources + - Requires `App.svelte` entry point +- **sb docs**: ❌ Not implemented yet + +## Missing from sb docs + +1. **Playground/Examples System** ❌ + - svelte-mcp has `playground-link` tool + - We should add example bundling in npm package + +2. **Live Documentation Fetching** ❌ + - svelte-mcp fetches from svelte.dev at runtime + - We need to decide: bundle in package or fetch from doks site + +3. **Use Cases Metadata** ❌ + - svelte-mcp has pre-computed `use_cases.json` + - We need to generate this from springboard docs + +4. **Actual Implementation** ❌ + - All commands currently return "TODO" + - Need to implement each command + +## Recommendations + +### Must Have (Match svelte-mcp) +1. ✅ Remove `scaffold` - doesn't match svelte-mcp pattern +2. ❌ Add examples system (bundled in npm package) +3. ❌ Implement `list` with use_cases metadata +4. ❌ Implement `get` to fetch/bundle docs +5. ❌ Implement `validate` with AST visitors +6. ❌ Implement `context` with pre-loaded docs list + +### Optional (Extensions) +- Keep `types` command (useful, not in svelte-mcp) +- Keep `scaffold` if valuable for Springboard (diverges) + +### Architecture Decisions Needed +1. **Docs source**: Bundle in package or fetch at runtime? +2. **Examples**: In-package files or external URLs? +3. **Scaffold**: Remove or keep as Springboard-specific feature? diff --git a/.planning/springboard-mcp-analysis.md b/.planning/springboard-mcp-analysis.md new file mode 100644 index 00000000..6537b274 --- /dev/null +++ b/.planning/springboard-mcp-analysis.md @@ -0,0 +1,351 @@ +# Springboard `sb docs` CLI for AI Agents + +This document analyzes the svelte-mcp patterns and adapts them into a `sb docs` subcommand that AI coding agents can use when building springboard applications. + +## Why CLI Instead of MCP? + +- **Simpler integration** - Any AI agent can run shell commands +- **No protocol overhead** - Direct stdin/stdout communication +- **Easier testing** - Run commands manually to verify behavior +- **Portable** - Works with any AI tool, not just MCP-compatible ones +- **Unified CLI** - Extends existing `sb` command rather than adding new tool + +## Key Patterns from svelte-mcp (Adapted for CLI) + +### 1. Command Structure + +svelte-mcp exposes these via MCP tools - we expose them as `sb docs` subcommands: + +```bash +# Documentation discovery +sb docs list # List available docs with use_cases +sb docs get # Fetch specific documentation + +# Code validation +sb docs validate # Validate a module file +sb docs validate --stdin # Validate code from stdin + +# Scaffolding +sb docs scaffold module # Generate module template +sb docs scaffold feature # Generate feature module +sb docs scaffold utility # Generate utility module + +# Context for agents +sb docs context # Output full context prompt for AI agents +sb docs types # Output core TypeScript definitions +``` + +### 2. Use Cases as Keywords + +Pre-generated metadata lets agents select docs without semantic search: + +```json +{ + "springboard/module-development": "creating modules, registerModule, feature modules, utility modules, initializer modules...", + "springboard/state-management": "shared state, persistent state, user agent state, createSharedState, useState...", + "springboard/guides/registering-ui-routes": "routing, routes, registerRoute, hideApplicationShell, documentMeta..." +} +``` + +**CLI output format:** +```bash +$ sb docs list +springboard/module-development + Use cases: creating modules, registerModule, feature modules, utility modules... + +springboard/state-management + Use cases: shared state, persistent state, user agent state, createSharedState... +``` + +### 3. Iterative Validator Pattern + +```bash +$ sb docs validate src/modules/my-module.ts +{ + "issues": [ + "Line 15: Direct state mutation detected. Use state.setState() or state.setStateImmer()" + ], + "suggestions": [ + "Line 8: Consider adding onDestroy() cleanup for the subscription on line 12" + ], + "hasErrors": true +} +``` + +Agents can iterate: +``` +Generate code → sb docs validate → Fix issues → sb docs validate → Until clean +``` + +### 4. Context Prompt + +```bash +$ sb docs context +You are working on a Springboard application. Springboard is a full-stack +JavaScript framework built on React, Hono, JSON-RPC, and WebSockets. + +Available documentation sections: +- springboard/module-development (creating modules, registerModule...) +- springboard/state-management (shared state, persistent state...) +- springboard/guides/registering-ui-routes (routing, routes...) +... + +Workflow: +1. Use your knowledge of React and TypeScript first +2. Run `sb docs validate ` to check your code +3. Only fetch docs with `sb docs get
` when needed +4. For new modules, use `sb docs scaffold module ` + +Key concepts: +- Modules are registered with `springboard.registerModule()` +- State types: shared (cross-device), persistent (DB), userAgent (local) +- Actions are automatically RPC-enabled +- Routes use React Router under the hood +``` + +--- + +## CLI Design + +### Commands + +| Command | Purpose | Output | +|---------|---------|--------| +| `sb docs list` | List docs with use_cases | Text or JSON (`--json`) | +| `sb docs get ` | Fetch documentation | Markdown content | +| `sb docs validate ` | Validate module code | JSON `{issues, suggestions, hasErrors}` | +| `sb docs validate --stdin` | Validate from stdin | JSON `{issues, suggestions, hasErrors}` | +| `sb docs scaffold ` | Generate templates | File path created | +| `sb docs context` | Full agent context | Text prompt | +| `sb docs types` | Core type definitions | TypeScript definitions | + +### Output Formats + +```bash +# Default: human-readable +$ sb docs list + +# JSON for programmatic use +$ sb docs list --json + +# Quiet mode (errors only) +$ sb docs validate src/module.ts --quiet +``` + +--- + +## Validator Patterns to Detect + +| Pattern | Detection | Message | +|---------|-----------|---------| +| Direct state mutation | `state.value = x` | Use `state.setState()` or `state.setStateImmer()` | +| Missing module interface | registerModule without AllModules merge | Add interface merge for type-safe `getModule()` | +| Wrong state type | userAgent state for cross-device data | Use `createSharedState` or `createPersistentState` | +| Missing cleanup | Subscriptions without `onDestroy` | Add `moduleAPI.onDestroy()` callback | +| Route conflicts | Duplicate route paths | Make route paths unique | +| Sync getModule | getModule in synchronous code | Move to async initialization | +| Missing error handling | Actions without error handling | Use `coreDeps.showError()` | +| Platform directive issues | Unclosed @platform tags | Close `@platform` with `@platform end` | + +### Validation Layers + +1. **TypeScript** - Type errors via tsc +2. **Custom AST visitors** - Springboard-specific patterns +3. **ESLint** - React/TypeScript best practices + +--- + +## Implementation Structure + +Extends existing `sb` CLI in `/packages/springboard/cli/`: + +``` +packages/springboard/cli/ + src/ + commands/ + docs/ # New `sb docs` subcommand + index.ts # Register docs subcommands + list.ts + get.ts + validate.ts + scaffold.ts + context.ts + types.ts + validators/ + index.ts # Orchestrates validation layers + visitors/ + state-mutation.ts + missing-cleanup.ts + route-conflicts.ts + module-interface.ts + platform-directives.ts + docs-data/ + sections.json # Doc metadata with use_cases + content/ # LLM-optimized doc content (or fetch from doks) + templates/ + module.ts.template + feature.ts.template + utility.ts.template +``` + +### Key Dependencies to Add + +```json +{ + "dependencies": { + "@typescript-eslint/parser": "^7.0.0", + "@typescript-eslint/typescript-estree": "^7.0.0" + } +} +``` + +--- + +## Documentation Pipeline + +### Source Files + +- `/doks/content/docs/springboard/` - Core docs +- `/doks/content/docs/jamtools/` - Jamtools extension +- `/packages/springboard/cli/docs-out/CLI_DOCS_sb.md` - CLI reference + +### LLM-Optimized Format + +Convert markdown docs to condensed format: + +```markdown +# State Management + +## createSharedState(name, initialValue) +Creates state synchronized across all connected devices. +- Returns: StateSupervisor +- Use when: multiplayer features, real-time collaboration + +## createPersistentState(name, initialValue) +Creates state persisted to database. +- Returns: StateSupervisor +- Use when: user preferences, saved data + +## createUserAgentState(name, initialValue) +Creates state stored locally (localStorage). +- Returns: StateSupervisor +- Use when: UI state, device-specific settings +``` + +### Use Cases Generation + +Run Claude batch API on docs to generate `sections.json`: + +```json +{ + "sections": [ + { + "slug": "springboard/module-development", + "title": "Module Development", + "use_cases": "creating modules, registerModule, feature modules, utility modules, initializer modules, module lifecycle, module dependencies" + } + ] +} +``` + +--- + +## API Reference (for context command) + +```typescript +interface ModuleAPI { + // Lifecycle + onDestroy(callback: () => void): void + + // State Management + statesAPI: { + createSharedState(name: string, initial: T): StateSupervisor + createPersistentState(name: string, initial: T): StateSupervisor + createUserAgentState(name: string, initial: T): StateSupervisor + } + + // Actions + createActions(actions: T): Actions + + // Routing + registerRoute(path: string, options: RouteOptions, component: Component): void + + // Module Interaction + getModule(id: K): AllModules[K] + + // Core Dependencies + coreDependencies: CoreDependencies +} + +interface StateSupervisor { + getState(): T + setState(newState: T): void + setStateImmer(callback: (draft: T) => void): void + useState(): T // React hook + subject: Subject // RxJS observable +} + +interface CoreDependencies { + log: (...args: any[]) => void + showError: (error: string) => void + files: { saveFile(name: string, content: string): Promise } + storage: { remote: KVStore; userAgent: KVStore } + rpc: { remote: Rpc; local?: Rpc } + isMaestro: () => boolean +} +``` + +--- + +## Usage Examples + +### Agent Workflow + +```bash +# 1. Get context at start of session +sb docs context > /tmp/springboard-context.md + +# 2. Find relevant docs for task +sb docs list | grep -i "state" + +# 3. Fetch specific documentation +sb docs get springboard/state-management + +# 4. Generate module scaffold +sb docs scaffold feature user-profile + +# 5. Validate after writing code +sb docs validate src/modules/user-profile.ts + +# 6. Fix issues and re-validate until clean +``` + +### Integration with AI Tools + +**Claude Code CLAUDE.md:** +```markdown +## Springboard Development + +When working on springboard modules: +1. Run `sb docs context` to understand the framework +2. Use `sb docs list` to find relevant docs +3. Validate code with `sb docs validate ` before finishing +4. Use `sb docs scaffold` for new modules +``` + +**Cursor rules:** +``` +When creating springboard modules, always run `sb docs validate` on the file. +``` + +--- + +## Next Steps + +1. **Add `docs` subcommand to `sb` CLI** - Extend `/packages/springboard/cli/` +2. **Implement `sb docs list`** - Parse doks content, generate use_cases +3. **Implement `sb docs get`** - Fetch doc content by section +4. **Implement `sb docs context`** - Output agent context prompt +5. **Implement `sb docs validate`** - TypeScript parsing + custom visitors +6. **Implement `sb docs scaffold`** - Module templates +7. **Generate use_cases.json** - Run Claude batch on docs for keyword metadata diff --git a/packages/springboard/cli/package.json b/packages/springboard/cli/package.json index be1a87d8..0269c0c2 100644 --- a/packages/springboard/cli/package.json +++ b/packages/springboard/cli/package.json @@ -21,7 +21,9 @@ ], "scripts": { "build": "npm run clean && npm run build-cli", - "build-cli": "tsc && npm run add-header", + "build-cli": "tsc && npm run copy-examples && npm run copy-docs && npm run add-header", + "copy-examples": "mkdir -p dist/examples && cp src/examples/*.txt dist/examples/", + "copy-docs": "mkdir -p dist/docs/content && cp src/docs/sections.json dist/docs/ && cp src/docs/content/*.md dist/docs/content/", "prepublishOnly": "npm run build", "dev:setup": "[ -f ./dist/cli.js ] || npm run build", "build-saas": "DISABLE_IO=true npx tsx src/cli.ts build ../../../apps/jamtools/modules/index.ts", diff --git a/packages/springboard/cli/src/cli.ts b/packages/springboard/cli/src/cli.ts index 0a4e91fa..1986e8f7 100644 --- a/packages/springboard/cli/src/cli.ts +++ b/packages/springboard/cli/src/cli.ts @@ -1,25 +1,12 @@ -/** - * Springboard CLI - * - * Vite-based CLI wrapper for multi-platform application builds. - * Implements Option D: Monolithic CLI Wrapper from PLAN_VITE_CLI_INTEGRATION.md - * - * Commands: - * - sb dev - Start development server with HMR - * - sb build - Build for production - * - sb start - Start the production server - */ - import path from 'path'; -import fs from 'node:fs'; import { program } from 'commander'; -import concurrently from 'concurrently'; +// import concurrently from 'concurrently'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const packageJSON = require('../package.json'); +const packageJSON = require('../../package.json'); -import type { SpringboardPlatform, Plugin } from './types.js'; +import {createDocsCommand} from './docs_command.js'; /** * Resolve an entrypoint path to an absolute path @@ -62,13 +49,6 @@ async function loadPlugins(pluginPaths?: string): Promise { return plugins; } -/** - * Parse platforms string into a Set - */ -function parsePlatforms(platformsStr: string): Set { - return new Set(platformsStr.split(',') as SpringboardPlatform[]); -} - // ============================================================================= // CLI Program Setup // ============================================================================= @@ -95,10 +75,10 @@ program plugins?: string; port?: string; }) => { - const applicationEntrypoint = resolveEntrypoint(entrypoint); - const plugins = await loadPlugins(options.plugins); - const platformsToBuild = parsePlatforms(options.platforms || 'main'); - const port = parseInt(options.port || '5173', 10); + // const applicationEntrypoint = resolveEntrypoint(entrypoint); + // const plugins = await loadPlugins(options.plugins); + // const platformsToBuild = parsePlatforms(options.platforms || 'main'); + // const port = parseInt(options.port || '5173', 10); console.log(`Starting development server for platforms: ${options.platforms || 'main'}`); @@ -204,21 +184,23 @@ program .action(async () => { console.log('Starting production server...'); - concurrently( - [ - { - command: 'node dist/server/dist/local-server.cjs', - name: 'Server', - prefixColor: 'blue', - }, - ], - { - prefix: 'name', - restartTries: 0, - } - ); + // concurrently( + // [ + // { + // command: 'node dist/server/dist/local-server.cjs', + // name: 'Server', + // prefixColor: 'blue', + // }, + // ], + // { + // prefix: 'name', + // restartTries: 0, + // } + // ); }); +program.addCommand(createDocsCommand()); + // ============================================================================= // Parse and Execute // ============================================================================= @@ -228,4 +210,4 @@ if (!(globalThis as Record).AVOID_PROGRAM_PARSE) { } // Export for testing -export { program, resolveEntrypoint, loadPlugins, parsePlatforms }; +export { program, resolveEntrypoint, loadPlugins }; diff --git a/packages/springboard/cli/src/docs/content/springboard-actions.md b/packages/springboard/cli/src/docs/content/springboard-actions.md new file mode 100644 index 00000000..b25ccb2e --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-actions.md @@ -0,0 +1,174 @@ +# Actions & RPC + +Actions are async functions that automatically work across client/server via RPC. + +## Creating Actions + +### createActions (recommended) +```typescript +const actions = moduleAPI.createActions({ + increment: async () => { + const current = counter.getState(); + counter.setState(current + 1); + return { newValue: current + 1 }; + }, + + addItem: async (args: { text: string }) => { + items.setStateImmer(draft => { + draft.push({ id: uuid(), text: args.text }); + }); + }, + + fetchData: async (args: { id: string }) => { + const response = await fetch(`/api/data/${args.id}`); + return response.json(); + } +}); +``` + +### createAction (single action) +```typescript +const doSomething = moduleAPI.createAction( + 'doSomething', + {}, + async (args: { value: number }) => { + return args.value * 2; + } +); +``` + +## How RPC Works + +1. **Client calls action** → Serialized and sent via WebSocket +2. **Server receives** → Executes action callback +3. **Result returned** → Sent back to client + +```typescript +// On browser (client) +const result = await actions.increment(); +// ↓ RPC call to server +// ↓ Server executes increment() +// ↓ Result returned +console.log(result.newValue); // From server +``` + +## Execution Modes + +### Default: Remote (RPC) +```typescript +await actions.doSomething(args); // Uses RPC +``` + +### Force Local Execution +```typescript +await actions.doSomething(args, { mode: 'local' }); +``` +Runs directly without RPC. Use for: +- Actions that only affect local state +- Performance-critical operations +- Offline mode + +### Force Remote Execution +```typescript +await actions.doSomething(args, { mode: 'remote' }); +``` + +### Module-Level RPC Mode +```typescript +// All actions in this module run locally +moduleAPI.setRpcMode('local'); +``` + +## isMaestro Check + +The "maestro" is the authoritative device (usually server). + +```typescript +const actions = moduleAPI.createActions({ + syncData: async () => { + if (moduleAPI.deps.core.isMaestro()) { + // Running on server - do authoritative work + return await fetchFromDatabase(); + } else { + // Running on client - this shouldn't happen with default RPC + } + } +}); +``` + +## Error Handling + +```typescript +const actions = moduleAPI.createActions({ + riskyAction: async (args) => { + try { + return await doRiskyThing(args); + } catch (error) { + moduleAPI.deps.core.showError(`Failed: ${error.message}`); + throw error; // Re-throw to inform caller + } + } +}); +``` + +## Action Patterns + +### Optimistic Updates +```typescript +const actions = moduleAPI.createActions({ + addTodo: async (args: { text: string }) => { + const tempId = `temp-${Date.now()}`; + + // Optimistic update + todos.setStateImmer(draft => { + draft.push({ id: tempId, text: args.text, pending: true }); + }); + + try { + const realId = await saveTodoToServer(args.text); + // Replace temp with real + todos.setStateImmer(draft => { + const item = draft.find(t => t.id === tempId); + if (item) { + item.id = realId; + item.pending = false; + } + }); + } catch (error) { + // Rollback + todos.setStateImmer(draft => { + const idx = draft.findIndex(t => t.id === tempId); + if (idx >= 0) draft.splice(idx, 1); + }); + throw error; + } + } +}); +``` + +### Debounced Actions +```typescript +let saveTimeout: NodeJS.Timeout; + +const actions = moduleAPI.createActions({ + saveDocument: async (args: { content: string }) => { + clearTimeout(saveTimeout); + saveTimeout = setTimeout(async () => { + await persistToDatabase(args.content); + }, 1000); + } +}); +``` + +## Calling Actions from Components + +```typescript +function MyComponent() { + const handleClick = async () => { + const result = await actions.doSomething({ value: 42 }); + console.log(result); + }; + + return ; +} +``` diff --git a/packages/springboard/cli/src/docs/content/springboard-anti-patterns.md b/packages/springboard/cli/src/docs/content/springboard-anti-patterns.md new file mode 100644 index 00000000..79b58067 --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-anti-patterns.md @@ -0,0 +1,250 @@ +# Anti-Patterns to Avoid + +Common mistakes when developing Springboard modules. + +## Module Level getModule + +### ❌ Problem +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + // DON'T: Called during registration, other module might not exist yet + const auth = moduleAPI.getModule('auth'); + const user = auth.userState.getState(); // May crash or return undefined + + moduleAPI.registerRoute('/', {}, () =>
{user?.name}
); +}); +``` + +### ✅ Solution +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + moduleAPI.registerRoute('/', {}, () => { + // DO: Called at render time, all modules registered + const auth = moduleAPI.getModule('auth'); + const user = auth.userState.useState(); + + return
{user?.name}
; + }); +}); +``` + +## Missing Optional Chaining + +### ❌ Problem +```typescript +const maybeModule = moduleAPI.getModule('optionalFeature'); +maybeModule.actions.doSomething(); // Crashes if module doesn't exist +``` + +### ✅ Solution +```typescript +const maybeModule = moduleAPI.getModule('optionalFeature'); +maybeModule?.actions?.doSomething(); // Safe + +// Or with explicit check +if (maybeModule) { + await maybeModule.actions.doSomething(); +} +``` + +## Direct State Mutation + +### ❌ Problem +```typescript +const items = itemsState.getState(); +items.push(newItem); // Mutating directly - won't trigger updates +itemsState.setState(items); // Same reference, may not trigger re-render +``` + +### ✅ Solution +```typescript +// Option 1: Immutable update +itemsState.setState([...items, newItem]); + +// Option 2: setStateImmer (recommended) +itemsState.setStateImmer(draft => { + draft.push(newItem); +}); +``` + +## Computed Values in State + +### ❌ Problem +```typescript +const [items, setItems] = useState([]); +const [filteredItems, setFilteredItems] = useState([]); // Derived state + +useEffect(() => { + setFilteredItems(items.filter(i => i.active)); // Redundant state +}, [items]); +``` + +### ✅ Solution +```typescript +const items = itemsState.useState(); + +// Compute on each render, memoize if expensive +const filteredItems = useMemo( + () => items.filter(i => i.active), + [items] +); +``` + +## Race Conditions in Initialization + +### ❌ Problem +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + // These run in parallel - state2 might use state1 before it's ready + const state1Promise = moduleAPI.statesAPI.createPersistentState('a', {}); + const state2Promise = moduleAPI.statesAPI.createPersistentState('b', { + aRef: state1Promise.getState() // state1 not resolved yet! + }); +}); +``` + +### ✅ Solution +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + // Await in sequence if there are dependencies + const state1 = await moduleAPI.statesAPI.createPersistentState('a', {}); + const state2 = await moduleAPI.statesAPI.createPersistentState('b', { + aRef: state1.getState() // Safe - state1 is resolved + }); + + // Or use Promise.all if truly independent + const [stateA, stateB] = await Promise.all([ + moduleAPI.statesAPI.createPersistentState('a', {}), + moduleAPI.statesAPI.createPersistentState('b', {}) + ]); +}); +``` + +## Missing Cleanup + +### ❌ Problem +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + const subscription = someObservable.subscribe(handleChange); + // Subscription never cleaned up - memory leak! + + setInterval(pollData, 5000); + // Interval never cleared - runs forever! +}); +``` + +### ✅ Solution +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + const subscription = someObservable.subscribe(handleChange); + moduleAPI.onDestroy(() => subscription.unsubscribe()); + + const intervalId = setInterval(pollData, 5000); + moduleAPI.onDestroy(() => clearInterval(intervalId)); +}); +``` + +## Wrong State Type Choice + +### ❌ Problem +```typescript +// Using SharedState for data that should persist +const userPrefs = await moduleAPI.statesAPI.createSharedState('prefs', { + theme: 'light' +}); +// Lost on server restart! + +// Using PersistentState for ephemeral UI state +const isModalOpen = await moduleAPI.statesAPI.createPersistentState('modal', false); +// Unnecessary database writes! +``` + +### ✅ Solution +```typescript +// Persistent for data that should survive restarts +const userPrefs = await moduleAPI.statesAPI.createPersistentState('prefs', { + theme: 'light' +}); + +// UserAgent for local UI state +const isModalOpen = await moduleAPI.statesAPI.createUserAgentState('modal', false); + +// Shared for real-time sync without persistence +const cursorPosition = await moduleAPI.statesAPI.createSharedState('cursor', null); +``` + +## Blocking Actions + +### ❌ Problem +```typescript +const actions = moduleAPI.createActions({ + processLargeData: async (args: { data: BigData }) => { + // Blocks UI for entire processing time + for (const item of args.data.items) { + await heavyProcessing(item); + } + } +}); +``` + +### ✅ Solution +```typescript +const actions = moduleAPI.createActions({ + processLargeData: async (args: { data: BigData }) => { + // Process in chunks, yield to UI + const chunks = chunkArray(args.data.items, 100); + + for (const chunk of chunks) { + await Promise.all(chunk.map(heavyProcessing)); + // Allow UI to update + await new Promise(r => setTimeout(r, 0)); + progressState.setStateImmer(d => { d.processed += chunk.length; }); + } + } +}); +``` + +## Sync getModule in Render + +### ❌ Problem +```typescript +function MyComponent() { + // Called on every render - inefficient + const otherModule = moduleAPI.getModule('other'); + const data = otherModule.state.useState(); + + return
{data}
; +} +``` + +### ✅ Solution +```typescript +// Get module reference once +const otherModule = moduleAPI.getModule('other'); + +function MyComponent() { + // Just use the state hook + const data = otherModule.state.useState(); + return
{data}
; +} + +// Or if conditional: +function MyComponent() { + const otherModule = useMemo(() => moduleAPI.getModule('other'), []); + const data = otherModule?.state?.useState() ?? defaultValue; + return
{data}
; +} +``` + +## Summary Checklist + +Before completing a module: + +- [ ] No `getModule` calls at module registration level +- [ ] All optional modules accessed with `?.` +- [ ] State updated via `setState` or `setStateImmer`, never mutated directly +- [ ] No derived state stored - use `useMemo` instead +- [ ] All subscriptions cleaned up in `onDestroy` +- [ ] All timers/intervals cleared in `onDestroy` +- [ ] Correct state type chosen (Shared/Persistent/UserAgent) +- [ ] Heavy operations chunked or run in background diff --git a/packages/springboard/cli/src/docs/content/springboard-core-dependencies.md b/packages/springboard/cli/src/docs/content/springboard-core-dependencies.md new file mode 100644 index 00000000..ae3e7837 --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-core-dependencies.md @@ -0,0 +1,170 @@ +# Core Dependencies + +Every module has access to `CoreDependencies` via `moduleAPI.deps.core`. + +## Overview + +```typescript +type CoreDependencies = { + log: (...args: any[]) => void; + showError: (error: string) => void; + files: { + saveFile: (name: string, content: string) => Promise; + }; + storage: { + remote: KVStore; // Server/shared storage + userAgent: KVStore; // Local device storage + }; + rpc: { + remote: Rpc; // Server communication + local?: Rpc; // Local RPC (on maestro) + }; + isMaestro: () => boolean; +}; +``` + +## Logging + +### log +```typescript +moduleAPI.deps.core.log('Message', { data: 'value' }); +``` +Logs to console with module prefix. Use for debugging. + +### showError +```typescript +moduleAPI.deps.core.showError('Something went wrong'); +``` +Shows error to user (toast notification). Use for user-facing errors. + +```typescript +const actions = moduleAPI.createActions({ + saveDocument: async (args) => { + try { + await save(args); + } catch (error) { + moduleAPI.deps.core.showError(`Save failed: ${error.message}`); + throw error; + } + } +}); +``` + +## File Operations + +### saveFile +```typescript +await moduleAPI.deps.core.files.saveFile('export.json', JSON.stringify(data)); +``` +Triggers file download in browser, writes to disk in Node. + +## Storage (KVStore) + +Low-level key-value storage. Prefer `createPersistentState` for most cases. + +### KVStore Interface +```typescript +type KVStore = { + get: (key: string) => Promise; + set: (key: string, value: T) => Promise; + getAll: () => Promise | null>; +}; +``` + +### Remote Storage +```typescript +const { remote } = moduleAPI.deps.core.storage; + +// Stored on server, shared across devices +await remote.set('settings', { theme: 'dark' }); +const settings = await remote.get('settings'); +``` + +### UserAgent Storage +```typescript +const { userAgent } = moduleAPI.deps.core.storage; + +// Stored locally (localStorage in browser) +await userAgent.set('recentSearches', ['query1', 'query2']); +const searches = await userAgent.get('recentSearches'); +``` + +## RPC (Remote Procedure Call) + +Low-level RPC access. Usually not needed (use `createActions` instead). + +### Rpc Interface +```typescript +type Rpc = { + callRpc: (name: string, args: Args) => Promise; + broadcastRpc: (name: string, args: Args) => Promise; + registerRpc: (name: string, cb: (args: Args) => Promise) => void; + role: 'server' | 'client'; +}; +``` + +### Example: Custom RPC +```typescript +const { rpc } = moduleAPI.deps.core; + +// Register handler (on server) +rpc.remote.registerRpc('custom:ping', async (args: { message: string }) => { + return { pong: args.message }; +}); + +// Call from client +const result = await rpc.remote.callRpc('custom:ping', { message: 'hello' }); +console.log(result.pong); // 'hello' +``` + +## isMaestro + +Checks if running on the authoritative device (server). + +```typescript +if (moduleAPI.deps.core.isMaestro()) { + // Running on server - do authoritative work + await syncWithExternalAPI(); +} else { + // Running on client +} +``` + +**Use cases:** +- Server-only initialization +- Authoritative game logic +- Scheduled tasks + +## Module Dependencies (modDeps) + +Access via `moduleAPI.deps.module`: + +```typescript +type ModuleDependencies = { + moduleRegistry: ModuleRegistry; + toast: (options: ToastOptions) => void; + rpc: { remote: Rpc; local?: Rpc }; + services: { + remoteSharedStateService: SharedStateService; + localSharedStateService: SharedStateService; + }; +}; +``` + +### Toast Notifications +```typescript +moduleAPI.deps.module.toast({ + target: 'all', // 'all' | 'self' | 'others' + message: 'Hello!', + variant: 'info', // 'info' | 'success' | 'warning' | 'error' + flash: false, // Brief highlight + persistent: false // Stays until dismissed +}); +``` + +## Best Practices + +1. **Prefer high-level APIs**: Use `createActions` over raw RPC +2. **Use showError for users**: Don't just log errors silently +3. **Check isMaestro for server logic**: Keep authoritative code on server +4. **Use state APIs over storage**: `createPersistentState` handles sync automatically diff --git a/packages/springboard/cli/src/docs/content/springboard-lifecycle.md b/packages/springboard/cli/src/docs/content/springboard-lifecycle.md new file mode 100644 index 00000000..f69d2909 --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-lifecycle.md @@ -0,0 +1,188 @@ +# Module Lifecycle + +Understanding when modules initialize and how to clean up resources. + +## Initialization Order + +``` +1. Springboard Engine created +2. RPC connections established +3. SharedStateServices initialized +4. Modules registered (in import order): + a. Module object created + b. ModuleAPI instantiated + c. registerModule callback executed + d. Module added to registry +5. React app renders +6. Module Providers stacked +7. Routes become active +``` + +## Registration Phase + +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + // This runs during registration (step 4c) + // - RPC is available + // - Other modules may not be registered yet + + const state = await moduleAPI.statesAPI.createSharedState('data', {}); + + moduleAPI.registerRoute('/', {}, () => { + // This runs during render (step 7) + // - All modules are registered + // - Safe to call getModule + return
Hello
; + }); + + return { state }; +}); +``` + +## onDestroy - Cleanup + +Register cleanup callbacks with `moduleAPI.onDestroy()`: + +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + // Create subscription + const subscription = someObservable.subscribe(handleChange); + + // Create interval + const intervalId = setInterval(checkUpdates, 5000); + + // Register cleanup + moduleAPI.onDestroy(() => { + subscription.unsubscribe(); + clearInterval(intervalId); + }); +}); +``` + +### When onDestroy Runs + +- Module is explicitly destroyed +- App is unmounting +- Hot module replacement (development) + +### Multiple Cleanup Callbacks + +```typescript +moduleAPI.onDestroy(() => { + console.log('Cleanup 1'); +}); + +moduleAPI.onDestroy(() => { + console.log('Cleanup 2'); +}); +// Both will be called +``` + +## Common Cleanup Scenarios + +### RxJS Subscriptions +```typescript +const sub = state.subject.subscribe(handleChange); +moduleAPI.onDestroy(() => sub.unsubscribe()); +``` + +### Event Listeners +```typescript +window.addEventListener('resize', handleResize); +moduleAPI.onDestroy(() => { + window.removeEventListener('resize', handleResize); +}); +``` + +### Timers +```typescript +const timerId = setInterval(poll, 1000); +moduleAPI.onDestroy(() => clearInterval(timerId)); +``` + +### WebSocket Connections +```typescript +const ws = new WebSocket(url); +moduleAPI.onDestroy(() => ws.close()); +``` + +### Third-Party Library Cleanup +```typescript +const chart = new Chart(canvas, config); +moduleAPI.onDestroy(() => chart.destroy()); +``` + +## Provider Pattern + +Each module can have a React Provider: + +```typescript +springboard.registerClassModule(async (coreDeps, modDeps) => { + const MyContext = createContext(null); + + return { + moduleId: 'myModule', + Provider: ({ children }) => { + const [state, setState] = useState(initialState); + return ( + + {children} + + ); + } + }; +}); +``` + +Providers are stacked in registration order: +``` + + + + + + + +``` + +## Splash Screen + +Register a loading screen shown during initialization: + +```typescript +springboard.registerSplashScreen(() => ( +
+ +

Loading...

+
+)); +``` + +Shown until all modules are registered and initial state is loaded. + +## Async Initialization + +Module registration callbacks can be async: + +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + // Await initial data + const initialData = await fetchInitialData(); + + const state = await moduleAPI.statesAPI.createPersistentState( + 'data', + initialData + ); + + // Module isn't "ready" until this callback completes + return { state }; +}); +``` + +## Best Practices + +1. **Always clean up subscriptions**: Use `onDestroy` for every subscription +2. **Don't call getModule during registration**: Wait for routes/actions +3. **Keep registration fast**: Defer heavy work to actions or effects +4. **Use splash screen**: Show loading state during async init +5. **Import order matters**: Dependencies before dependents diff --git a/packages/springboard/cli/src/docs/content/springboard-module-api.md b/packages/springboard/cli/src/docs/content/springboard-module-api.md new file mode 100644 index 00000000..ad414089 --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-module-api.md @@ -0,0 +1,132 @@ +# ModuleAPI Reference + +The `ModuleAPI` is the primary interface for module development. Received as parameter in `registerModule` callback. + +## Properties + +```typescript +moduleAPI.moduleId // string - Unique module identifier +moduleAPI.fullPrefix // string - Namespaced prefix +moduleAPI.statesAPI // StatesAPI - State management +moduleAPI.deps.core // CoreDependencies +moduleAPI.deps.module // ModuleDependencies +``` + +## State Management + +### createSharedState +```typescript +const state = await moduleAPI.statesAPI.createSharedState(name: string, initial: T) +``` +Cross-device synchronized state. Use for real-time features. + +### createPersistentState +```typescript +const state = await moduleAPI.statesAPI.createPersistentState(name: string, initial: T) +``` +Database-backed state. Survives restarts, syncs across devices. + +### createUserAgentState +```typescript +const state = await moduleAPI.statesAPI.createUserAgentState(name: string, initial: T) +``` +Local-only state (localStorage). Device-specific preferences. + +### StateSupervisor Methods +```typescript +state.getState() // Get current value +state.setState(newValue) // Set new value +state.setState(prev => newValue) // Set with callback +state.setStateImmer(draft => {}) // Mutate with Immer +state.useState() // React hook +state.subject // RxJS Subject +``` + +## Actions + +### createActions +```typescript +const actions = moduleAPI.createActions({ + actionName: async (args: Args) => { + // Automatically RPC-enabled + return result; + } +}); +``` + +### createAction (single) +```typescript +const action = moduleAPI.createAction('name', {}, async (args) => result); +``` + +### Action Options +```typescript +// Force local execution (skip RPC) +await actions.doSomething(args, { mode: 'local' }); + +// Force remote execution +await actions.doSomething(args, { mode: 'remote' }); +``` + +## Routing + +### registerRoute +```typescript +moduleAPI.registerRoute(path, options, Component); + +// Examples: +moduleAPI.registerRoute('/', {}, HomePage); // Absolute: / +moduleAPI.registerRoute('items/:id', {}, ItemPage); // Relative: /modules/MyModule/items/:id +moduleAPI.registerRoute('/admin', { // Absolute with options + hideApplicationShell: true, + documentMeta: { title: 'Admin' } +}, AdminPage); +``` + +### Route Options +```typescript +type RegisterRouteOptions = { + hideApplicationShell?: boolean; // Hide app shell + documentMeta?: { + title?: string; + description?: string; + 'og:image'?: string; + // ... SEO metadata + }; +} +``` + +### registerApplicationShell +```typescript +moduleAPI.registerApplicationShell(({ children, modules }) => ( + {children} +)); +``` + +## Module Communication + +### getModule +```typescript +const otherModule = moduleAPI.getModule('OtherModuleId'); +await otherModule.actions.doSomething(); +``` + +**Important**: Call inside routes/actions, not at module level. + +## Lifecycle + +### onDestroy +```typescript +moduleAPI.onDestroy(() => { + // Cleanup subscriptions, timers, etc. + subscription.unsubscribe(); +}); +``` + +## RPC Mode + +### setRpcMode +```typescript +moduleAPI.setRpcMode('local'); // All actions run locally +moduleAPI.setRpcMode('remote'); // All actions use RPC (default) +``` diff --git a/packages/springboard/cli/src/docs/content/springboard-module-communication.md b/packages/springboard/cli/src/docs/content/springboard-module-communication.md new file mode 100644 index 00000000..fb2167e1 --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-module-communication.md @@ -0,0 +1,163 @@ +# Module Communication + +Modules can access other modules' APIs using `getModule()` with type-safe interface merging. + +## Basic Usage + +```typescript +// In a route or action (NOT at module level) +const otherModule = moduleAPI.getModule('otherModuleId'); +await otherModule.actions.doSomething(); +``` + +## Interface Merging Pattern + +For type-safe `getModule()`, declare your module's exports: + +```typescript +// In auth module +springboard.registerModule('auth', {}, async (moduleAPI) => { + const userState = await moduleAPI.statesAPI.createPersistentState('user', null as User | null); + + const actions = moduleAPI.createActions({ + login: async (args: { email: string; password: string }) => { /* ... */ }, + logout: async () => { /* ... */ } + }); + + return { userState, actions }; +}); + +// Type declaration (usually at bottom of file or in types.ts) +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + auth: { + userState: StateSupervisor; + actions: { + login: (args: { email: string; password: string }) => Promise; + logout: () => Promise; + }; + }; + } +} +``` + +Now in other modules: +```typescript +const auth = moduleAPI.getModule('auth'); // Type-safe! +const user = auth.userState.useState(); // TypeScript knows this is User | null +await auth.actions.login({ email, password }); // Autocomplete works +``` + +## Important: Where to Call getModule + +### ❌ Wrong: At Module Level +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + // DON'T DO THIS - module might not be registered yet + const auth = moduleAPI.getModule('auth'); + + // ... +}); +``` + +### ✅ Correct: Inside Routes or Actions +```typescript +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + moduleAPI.registerRoute('/', {}, () => { + // ✓ Safe - called after all modules registered + const auth = moduleAPI.getModule('auth'); + const user = auth.userState.useState(); + + return
Hello {user?.name}
; + }); + + const actions = moduleAPI.createActions({ + doSomething: async () => { + // ✓ Safe - called at runtime + const auth = moduleAPI.getModule('auth'); + const user = auth.userState.getState(); + // ... + } + }); +}); +``` + +## Optional Module Access + +If a module might not be registered: + +```typescript +const maybeModule = moduleAPI.getModule('optionalModule'); + +// Use optional chaining +maybeModule?.actions?.doSomething(); + +// Or check existence +if (maybeModule) { + await maybeModule.actions.doSomething(); +} +``` + +## Common Patterns + +### Shared Notification System +```typescript +// In feature module +const handleError = (error: Error) => { + const notifications = moduleAPI.getModule('notifications'); + notifications.actions.show({ + message: error.message, + type: 'error' + }); +}; +``` + +### Auth-Protected Actions +```typescript +const actions = moduleAPI.createActions({ + saveDocument: async (args: { content: string }) => { + const auth = moduleAPI.getModule('auth'); + const user = auth.userState.getState(); + + if (!user) { + throw new Error('Must be logged in'); + } + + await saveToDatabase(user.id, args.content); + } +}); +``` + +### Cross-Module State Subscription +```typescript +springboard.registerModule('Dashboard', {}, async (moduleAPI) => { + moduleAPI.registerRoute('/', {}, () => { + const todos = moduleAPI.getModule('todos'); + const calendar = moduleAPI.getModule('calendar'); + + // Subscribe to multiple modules' state + const todoItems = todos.state.useState(); + const events = calendar.eventsState.useState(); + + return ( +
+ + +
+ ); + }); +}); +``` + +## Module Dependencies + +Modules are initialized in registration order. If module B depends on module A: + +```typescript +// index.tsx - register in dependency order +import './modules/utilities/auth'; // First +import './modules/utilities/notifications'; +import './modules/features/dashboard'; // Last (depends on above) +``` + +For circular dependencies, use lazy access via `getModule()` inside routes/actions. diff --git a/packages/springboard/cli/src/docs/content/springboard-module-types.md b/packages/springboard/cli/src/docs/content/springboard-module-types.md new file mode 100644 index 00000000..38924148 --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-module-types.md @@ -0,0 +1,195 @@ +# Module Types + +Springboard has three module types, each with a specific purpose. + +## Feature Module + +**Purpose:** Implements user-facing features with UI. + +**Characteristics:** +- Registers routes +- Has visual components +- May use other modules +- Most common type + +**Example:** +```typescript +springboard.registerModule('TodoList', {}, async (moduleAPI) => { + const todosState = await moduleAPI.statesAPI.createPersistentState('todos', []); + + const actions = moduleAPI.createActions({ + addTodo: async (args: { text: string }) => { + todosState.setStateImmer(draft => { + draft.push({ id: Date.now(), text: args.text, done: false }); + }); + }, + toggleTodo: async (args: { id: number }) => { + todosState.setStateImmer(draft => { + const todo = draft.find(t => t.id === args.id); + if (todo) todo.done = !todo.done; + }); + } + }); + + moduleAPI.registerRoute('/', {}, () => { + const todos = todosState.useState(); + return ( +
+

Todos

+ {todos.map(todo => ( +
actions.toggleTodo({ id: todo.id })}> + {todo.done ? '✓' : '○'} {todo.text} +
+ ))} +
+ ); + }); + + return { state: todosState, actions }; +}); +``` + +## Utility Module + +**Purpose:** Provides shared functionality for other modules. + +**Characteristics:** +- No routes or UI +- Exports APIs for other modules +- Uses interface merging for type-safety +- Examples: auth, notifications, analytics + +**Example:** +```typescript +springboard.registerModule('notifications', {}, async (moduleAPI) => { + const notificationsState = await moduleAPI.statesAPI.createSharedState('notifications', [] as Notification[]); + + const actions = moduleAPI.createActions({ + show: async (args: { message: string; type: 'info' | 'error' | 'success' }) => { + const id = Date.now(); + notificationsState.setStateImmer(draft => { + draft.push({ id, ...args }); + }); + + // Auto-dismiss after 5s + setTimeout(() => { + notificationsState.setStateImmer(draft => { + const idx = draft.findIndex(n => n.id === id); + if (idx >= 0) draft.splice(idx, 1); + }); + }, 5000); + }, + + dismiss: async (args: { id: number }) => { + notificationsState.setStateImmer(draft => { + const idx = draft.findIndex(n => n.id === args.id); + if (idx >= 0) draft.splice(idx, 1); + }); + } + }); + + // Expose for other modules + return { state: notificationsState, actions }; +}); + +// Type declaration for getModule +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + notifications: { + state: StateSupervisor; + actions: { + show: (args: { message: string; type: 'info' | 'error' | 'success' }) => Promise; + dismiss: (args: { id: number }) => Promise; + }; + }; + } +} +``` + +**Using in another module:** +```typescript +springboard.registerModule('MyFeature', {}, async (moduleAPI) => { + moduleAPI.registerRoute('/', {}, () => { + const notifications = moduleAPI.getModule('notifications'); + + const handleSave = async () => { + try { + await saveData(); + notifications.actions.show({ message: 'Saved!', type: 'success' }); + } catch (e) { + notifications.actions.show({ message: 'Save failed', type: 'error' }); + } + }; + + return ; + }); +}); +``` + +## Initializer Module + +**Purpose:** Performs setup during app initialization. + +**Characteristics:** +- Runs before other modules +- No UI or routes +- Platform-specific initialization +- Examples: theme setup, analytics init, feature flags + +**Example:** +```typescript +springboard.registerModule('ThemeInitializer', {}, async (moduleAPI) => { + const themeState = await moduleAPI.statesAPI.createUserAgentState('theme', { + mode: 'light' as 'light' | 'dark' + }); + + // Apply theme on load + const theme = themeState.getState(); + document.documentElement.setAttribute('data-theme', theme.mode); + + // Watch for changes + themeState.subject.subscribe(({ mode }) => { + document.documentElement.setAttribute('data-theme', mode); + }); + + return { + state: themeState, + setTheme: (mode: 'light' | 'dark') => { + themeState.setState({ mode }); + } + }; +}); +``` + +## When to Use Each Type + +| Scenario | Module Type | +|----------|-------------| +| User-facing feature with pages | Feature | +| Shared service (auth, notifications) | Utility | +| App-wide setup (themes, analytics) | Initializer | +| API integration | Utility | +| Dashboard with routes | Feature | +| State shared across features | Utility | + +## Module Organization Pattern + +``` +src/ + modules/ + features/ + todos/ + index.tsx # Feature module + components/ # UI components + dashboard/ + index.tsx + utilities/ + notifications/ + index.tsx # Utility module + auth/ + index.tsx + initializers/ + theme.tsx # Initializer module + analytics.tsx + index.tsx # Register all modules +``` diff --git a/packages/springboard/cli/src/docs/content/springboard-overview.md b/packages/springboard/cli/src/docs/content/springboard-overview.md new file mode 100644 index 00000000..5a0cb70c --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-overview.md @@ -0,0 +1,62 @@ +# Springboard Overview + +Springboard is a full-stack JavaScript framework for building real-time, multi-device applications. Built on React, Hono, JSON-RPC, and WebSockets. + +## Core Philosophy + +"Your codebase should only be feature-level code" - Springboard abstracts infrastructure so you focus on features. + +## Key Features + +- **Multi-platform**: Browser, Node.js, Desktop (Tauri), React Native, PartyKit +- **Real-time sync**: State automatically syncs across connected devices +- **Module-based**: Organize code into isolated, reusable modules +- **RPC built-in**: Actions automatically work across client/server +- **Type-safe**: Full TypeScript support with module interface merging + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ Your Modules │ +│ (Feature, Utility, Initializer) │ +├─────────────────────────────────────────┤ +│ ModuleAPI │ +│ (States, Actions, Routes, Lifecycle) │ +├─────────────────────────────────────────┤ +│ Springboard Engine │ +│ (RPC, State Sync, Module Registry) │ +├─────────────────────────────────────────┤ +│ Platform Layer │ +│ (Browser, Node, Desktop, Mobile) │ +└─────────────────────────────────────────┘ +``` + +## Basic Module Structure + +```typescript +import springboard from 'springboard'; + +springboard.registerModule('MyModule', {}, async (moduleAPI) => { + // 1. Create state + const state = await moduleAPI.statesAPI.createSharedState('data', initialValue); + + // 2. Create actions + const actions = moduleAPI.createActions({ + doSomething: async (args) => { /* ... */ } + }); + + // 3. Register routes + moduleAPI.registerRoute('/', {}, MyComponent); + + // 4. Return public API + return { state, actions }; +}); +``` + +## When to Use Springboard + +- Real-time collaborative apps +- Multi-device synchronized experiences +- Apps needing offline-first capabilities +- Full-stack apps with shared business logic diff --git a/packages/springboard/cli/src/docs/content/springboard-patterns.md b/packages/springboard/cli/src/docs/content/springboard-patterns.md new file mode 100644 index 00000000..31858664 --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-patterns.md @@ -0,0 +1,300 @@ +# Common Patterns + +Proven patterns from real Springboard applications. + +## State Patterns + +### Derived State with useMemo +Don't store computed values in state: + +```typescript +// ❌ Wrong: Storing computed value +const [filteredItems, setFilteredItems] = useState([]); +useEffect(() => { + setFilteredItems(items.filter(i => i.active)); +}, [items]); + +// ✅ Correct: Compute on render +function ItemList() { + const items = itemsState.useState(); + const filter = filterState.useState(); + + const filteredItems = useMemo( + () => items.filter(i => i.active && i.name.includes(filter)), + [items, filter] + ); + + return ; +} +``` + +### Optimistic Updates +Update UI immediately, then sync with server: + +```typescript +const actions = moduleAPI.createActions({ + toggleLike: async (args: { postId: string }) => { + // Optimistic update + likesState.setStateImmer(draft => { + draft[args.postId] = !draft[args.postId]; + }); + + try { + await api.toggleLike(args.postId); + } catch (error) { + // Rollback on failure + likesState.setStateImmer(draft => { + draft[args.postId] = !draft[args.postId]; + }); + throw error; + } + } +}); +``` + +### State Selectors +Subscribe to specific parts of state: + +```typescript +function UserName() { + const user = userState.useState(); + // Re-renders on any user change + + return {user.name}; +} + +// Better: Extract only what you need +function UserName() { + const user = userState.useState(); + const name = useMemo(() => user.name, [user.name]); + + return {name}; +} +``` + +## Action Patterns + +### Action Queuing +Prevent concurrent execution: + +```typescript +let savePromise: Promise | null = null; + +const actions = moduleAPI.createActions({ + save: async (args: { data: Data }) => { + // Wait for previous save + if (savePromise) await savePromise; + + savePromise = (async () => { + await api.save(args.data); + })(); + + await savePromise; + savePromise = null; + } +}); +``` + +### Debounced Actions +Delay execution until activity stops: + +```typescript +let debounceTimer: NodeJS.Timeout; + +const actions = moduleAPI.createActions({ + search: async (args: { query: string }) => { + return new Promise((resolve) => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(async () => { + const results = await api.search(args.query); + searchResults.setState(results); + resolve(results); + }, 300); + }); + } +}); +``` + +### Batch Updates +Group multiple state changes: + +```typescript +const actions = moduleAPI.createActions({ + importData: async (args: { items: Item[] }) => { + // Single state update instead of many + itemsState.setStateImmer(draft => { + for (const item of args.items) { + draft.push(item); + } + }); + } +}); +``` + +## Navigation Patterns + +### Navigation with Reason +Track why navigation happened: + +```typescript +type NavReason = + | { type: 'user_click' } + | { type: 'form_submit'; formId: string } + | { type: 'auto_redirect' } + | { type: 'deep_link' }; + +const navReasonState = await moduleAPI.statesAPI.createUserAgentState( + 'navReason', + null +); + +function navigateWithReason(path: string, reason: NavReason) { + navReasonState.setState(reason); + navigate(path); +} + +// Usage +navigateWithReason('/dashboard', { type: 'user_click' }); +``` + +### Breadcrumb Trail +Track navigation history: + +```typescript +const breadcrumbState = await moduleAPI.statesAPI.createUserAgentState( + 'breadcrumbs', + [] +); + +function pushBreadcrumb(path: string) { + breadcrumbState.setStateImmer(draft => { + draft.push(path); + if (draft.length > 10) draft.shift(); // Keep last 10 + }); +} +``` + +## Module Communication Patterns + +### Event Bus +Loose coupling between modules: + +```typescript +// events module +type AppEvent = + | { type: 'user:login'; userId: string } + | { type: 'document:saved'; docId: string }; + +const eventBus = new Subject(); + +springboard.registerModule('events', {}, async () => { + return { + emit: (event: AppEvent) => eventBus.next(event), + subscribe: (handler: (event: AppEvent) => void) => eventBus.subscribe(handler) + }; +}); + +// In other module +const events = moduleAPI.getModule('events'); +const sub = events.subscribe(event => { + if (event.type === 'user:login') { + // Handle login + } +}); +moduleAPI.onDestroy(() => sub.unsubscribe()); +``` + +### Hook System +Let modules extend functionality: + +```typescript +type Hook = (value: T) => T | Promise; + +const hooks = new Map[]>(); + +springboard.registerModule('hooks', {}, async () => { + return { + register: (name: string, hook: Hook) => { + if (!hooks.has(name)) hooks.set(name, []); + hooks.get(name)!.push(hook); + }, + run: async (name: string, value: T): Promise => { + const hookList = hooks.get(name) || []; + let result = value; + for (const hook of hookList) { + result = await hook(result); + } + return result; + } + }; +}); + +// Usage: before-save hook +hooks.register('document:before-save', async (doc) => { + return { ...doc, updatedAt: Date.now() }; +}); +``` + +## Error Handling Patterns + +### Global Error Boundary +```typescript +moduleAPI.registerRoute('/', {}, () => { + return ( + }> + + + ); +}); +``` + +### Action Error Wrapper +```typescript +function withErrorHandling Promise>( + action: T, + errorMessage: string +): T { + return (async (...args: Parameters) => { + try { + return await action(...args); + } catch (error) { + moduleAPI.deps.core.showError(errorMessage); + throw error; + } + }) as T; +} + +const actions = moduleAPI.createActions({ + save: withErrorHandling( + async (args: { data: Data }) => await api.save(args.data), + 'Failed to save' + ) +}); +``` + +## Loading State Pattern + +```typescript +interface AsyncState { + data: T | null; + loading: boolean; + error: string | null; +} + +const dataState = await moduleAPI.statesAPI.createSharedState>( + 'data', + { data: null, loading: false, error: null } +); + +const actions = moduleAPI.createActions({ + fetchData: async () => { + dataState.setState({ data: null, loading: true, error: null }); + try { + const data = await api.fetchData(); + dataState.setState({ data, loading: false, error: null }); + } catch (error) { + dataState.setState({ data: null, loading: false, error: error.message }); + } + } +}); +``` diff --git a/packages/springboard/cli/src/docs/content/springboard-platforms.md b/packages/springboard/cli/src/docs/content/springboard-platforms.md new file mode 100644 index 00000000..a341f6bd --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-platforms.md @@ -0,0 +1,217 @@ +# Platform Support + +Springboard supports multiple deployment platforms with platform-specific code. + +## Supported Platforms + +| Platform | Runtime | Use Case | +|----------|---------|----------| +| browser | Web browser | Standard web apps | +| node | Node.js | Server, CLI tools | +| desktop | Tauri | Desktop apps | +| react-native | React Native | Mobile apps | +| partykit | PartyKit edge | Serverless realtime | + +## Platform Detection + +```typescript +// Check platform at runtime +if (typeof window !== 'undefined') { + // Browser +} else { + // Node.js +} + +// Check if maestro (server/authoritative) +if (moduleAPI.deps.core.isMaestro()) { + // Running as server +} +``` + +## Conditional Compilation + +Use `@platform` directives for platform-specific code: + +```typescript +// @platform "node" +import fs from 'fs'; + +async function readFile(path: string) { + return fs.readFileSync(path, 'utf-8'); +} +// @platform end + +// @platform "browser" +async function readFile(path: string) { + const response = await fetch(path); + return response.text(); +} +// @platform end +``` + +The code block is only included in the specified platform build. + +### Multiple Platforms +```typescript +// @platform "node" +// @platform "desktop" +// Code included in both Node.js and Desktop builds +import { exec } from 'child_process'; +// @platform end +``` + +## Platform-Specific Services + +### Browser + +- **RPC**: WebSocket + HTTP fallback +- **Storage**: localStorage +- **Files**: Download via browser API + +```typescript +// Browser-specific features +const { userAgent } = moduleAPI.deps.core.storage; +await userAgent.set('key', 'value'); // Uses localStorage +``` + +### Node.js + +- **RPC**: WebSocket client +- **Storage**: JSON file on disk +- **Files**: fs module + +```typescript +// @platform "node" +import { writeFileSync } from 'fs'; + +async function exportData(data: any) { + writeFileSync('./export.json', JSON.stringify(data, null, 2)); +} +// @platform end +``` + +### Desktop (Tauri) + +- **RPC**: Tauri IPC bridge +- **Storage**: Local file system +- **Files**: Native file dialogs + +```typescript +// @platform "desktop" +import { save } from '@tauri-apps/api/dialog'; + +async function saveFileDialog(content: string) { + const path = await save({ defaultPath: 'document.txt' }); + if (path) { + await writeTextFile(path, content); + } +} +// @platform end +``` + +### React Native + +- **RPC**: WebView bridge +- **Storage**: AsyncStorage +- **Files**: Platform-specific APIs + +```typescript +// @platform "react-native" +import AsyncStorage from '@react-native-async-storage/async-storage'; + +async function saveLocal(key: string, value: string) { + await AsyncStorage.setItem(key, value); +} +// @platform end +``` + +## Build Configuration + +### CLI Options +```bash +# Build for specific platform +sb build src/index.tsx --platforms browser +sb build src/index.tsx --platforms node +sb build src/index.tsx --platforms desktop +sb build src/index.tsx --platforms all + +# Development +sb dev src/index.tsx --platforms main # browser + node +``` + +### Environment Variables +```typescript +// Available in browser builds +process.env.WS_HOST // WebSocket host +process.env.DATA_HOST // HTTP API host + +// Available in node builds +process.env.PORT // Server port +process.env.NODE_KV_STORE_DATA_FILE // KV store path +``` + +## Cross-Platform Patterns + +### Abstract Platform Differences +```typescript +// platform-utils.ts +export async function readLocalFile(key: string): Promise { + // @platform "browser" + return localStorage.getItem(key); + // @platform end + + // @platform "node" + try { + const { readFileSync } = await import('fs'); + return readFileSync(`./data/${key}.txt`, 'utf-8'); + } catch { + return null; + } + // @platform end +} +``` + +### Feature Detection +```typescript +const features = { + hasFileSystem: typeof window === 'undefined', + hasClipboard: typeof navigator?.clipboard !== 'undefined', + isOnline: typeof navigator?.onLine !== 'undefined' ? navigator.onLine : true +}; +``` + +## Offline Support + +### Browser Offline Mode +```bash +sb build src/index.tsx --platforms browser_offline +``` + +Builds a fully offline-capable version: +- All assets bundled +- `isMaestro()` returns `true` +- LocalStorage for all state + +### Detecting Online Status +```typescript +// @platform "browser" +function useOnlineStatus() { + const [online, setOnline] = useState(navigator.onLine); + + useEffect(() => { + const handleOnline = () => setOnline(true); + const handleOffline = () => setOnline(false); + + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + + return () => { + window.removeEventListener('online', handleOnline); + window.removeEventListener('offline', handleOffline); + }; + }, []); + + return online; +} +// @platform end +``` diff --git a/packages/springboard/cli/src/docs/content/springboard-routing.md b/packages/springboard/cli/src/docs/content/springboard-routing.md new file mode 100644 index 00000000..fc775ccd --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-routing.md @@ -0,0 +1,191 @@ +# Routing & Navigation + +Springboard uses React Router under the hood. Routes are registered via `moduleAPI.registerRoute()`. + +## Registering Routes + +### Basic Route +```typescript +moduleAPI.registerRoute('/', {}, HomePage); +``` + +### Route with Parameters +```typescript +moduleAPI.registerRoute('items/:itemId', {}, ItemDetailPage); + +function ItemDetailPage() { + const { itemId } = useParams(); + return
Item: {itemId}
; +} +``` + +### Absolute vs Relative Paths + +```typescript +// Absolute (starts with /) +moduleAPI.registerRoute('/admin', {}, AdminPage); +// → matches: /admin + +// Relative (no leading /) +moduleAPI.registerRoute('settings', {}, SettingsPage); +// → matches: /modules/MyModule/settings + +moduleAPI.registerRoute('', {}, ModuleHomePage); +// → matches: /modules/MyModule +``` + +## Route Options + +### hideApplicationShell +```typescript +moduleAPI.registerRoute('/fullscreen', { + hideApplicationShell: true +}, FullscreenPage); +``` +Hides the app shell (navigation, sidebar, etc.) for this route. Use for: +- Presentation modes +- Embedded views +- Login pages + +### documentMeta +```typescript +moduleAPI.registerRoute('/product/:id', { + documentMeta: { + title: 'Product Details', + description: 'View product information', + 'og:type': 'product' + } +}, ProductPage); +``` + +### Dynamic documentMeta +```typescript +moduleAPI.registerRoute('/product/:id', { + documentMeta: async ({ params }) => { + const product = await getProduct(params.id); + return { + title: product.name, + description: product.description, + 'og:image': product.imageUrl + }; + } +}, ProductPage); +``` + +## Application Shell + +### Registering Custom Shell +```typescript +moduleAPI.registerApplicationShell(({ children, modules }) => ( +
+ +
{children}
+
+)); +``` + +The shell wraps all routes except those with `hideApplicationShell: true`. + +## Navigation + +### Programmatic Navigation +```typescript +import { useNavigate } from 'react-router'; + +function MyComponent() { + const navigate = useNavigate(); + + const handleClick = () => { + navigate('/modules/MyModule/items/123'); + }; + + return ; +} +``` + +### Navigation with Reason (Pattern from SongDrive) +```typescript +// Define navigation reasons for type-safety +type NavigationReason = + | { reason: 'user_click' } + | { reason: 'action_complete'; actionId: string } + | { reason: 'auto_redirect' }; + +// Track why navigation happened +function navigateWithReason(path: string, reason: NavigationReason) { + // Store reason for analytics/debugging + sessionStorage.setItem('nav_reason', JSON.stringify(reason)); + navigate(path); +} +``` + +### Link Component +```typescript +import { Link } from 'react-router'; + +function Navigation() { + return ( + + ); +} +``` + +## Route Component Props + +Route components receive a `navigate` prop: + +```typescript +type RouteComponentProps = { + navigate: (routeName: string) => void; +}; + +function MyPage({ navigate }: RouteComponentProps) { + return ( + + ); +} +``` + +## Common Patterns + +### Protected Routes +```typescript +function ProtectedPage() { + const user = authState.useState(); + + if (!user) { + return ; + } + + return ; +} +``` + +### Route Guards in Module +```typescript +springboard.registerModule('Admin', {}, async (moduleAPI) => { + const authModule = moduleAPI.getModule('auth'); + + moduleAPI.registerRoute('/admin', {}, () => { + const user = authModule.userState.useState(); + + if (!user?.isAdmin) { + return
Access Denied
; + } + + return ; + }); +}); +``` + +### Nested Routes +```typescript +moduleAPI.registerRoute('dashboard', {}, DashboardLayout); +moduleAPI.registerRoute('dashboard/overview', {}, Overview); +moduleAPI.registerRoute('dashboard/settings', {}, Settings); +``` diff --git a/packages/springboard/cli/src/docs/content/springboard-server-modules.md b/packages/springboard/cli/src/docs/content/springboard-server-modules.md new file mode 100644 index 00000000..2900cc7e --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-server-modules.md @@ -0,0 +1,241 @@ +# Server Modules + +Server modules run on the Node.js server and can register HTTP routes and RPC middleware. + +## Server Module Registration + +```typescript +import { serverRegistry } from 'springboard-server'; + +serverRegistry.registerServerModule((serverAPI) => { + const { hono, hooks, getEngine } = serverAPI; + + // Register HTTP routes + hono.get('/api/health', (c) => c.json({ status: 'ok' })); + + // Register RPC middleware + hooks.registerRpcMiddleware(async (c) => { + const authHeader = c.req.header('Authorization'); + return { userId: extractUserId(authHeader) }; + }); +}); +``` + +## ServerModuleAPI + +```typescript +type ServerModuleAPI = { + hono: Hono; // Hono web framework instance + hooks: ServerHooks; // Server lifecycle hooks + getEngine: () => Springboard; // Access Springboard engine +}; +``` + +## Hono Routes + +Server modules use [Hono](https://hono.dev) for HTTP routing: + +```typescript +serverRegistry.registerServerModule(({ hono }) => { + // GET request + hono.get('/api/items', async (c) => { + const items = await db.getItems(); + return c.json(items); + }); + + // POST request + hono.post('/api/items', async (c) => { + const body = await c.req.json(); + const item = await db.createItem(body); + return c.json(item, 201); + }); + + // URL parameters + hono.get('/api/items/:id', async (c) => { + const { id } = c.req.param(); + const item = await db.getItem(id); + if (!item) return c.json({ error: 'Not found' }, 404); + return c.json(item); + }); + + // Query parameters + hono.get('/api/search', async (c) => { + const query = c.req.query('q'); + const results = await db.search(query); + return c.json(results); + }); +}); +``` + +## RPC Middleware + +Middleware runs before each RPC call and can inject context: + +```typescript +serverRegistry.registerServerModule(({ hooks }) => { + hooks.registerRpcMiddleware(async (c) => { + // Extract user from request + const token = c.req.header('Authorization')?.replace('Bearer ', ''); + + if (token) { + const user = await verifyToken(token); + return { user }; // Available in RPC handlers + } + + return {}; + }); +}); +``` + +The returned object is merged into RPC context: + +```typescript +// In a module action +const actions = moduleAPI.createActions({ + getUserData: async (args, options) => { + // Access middleware context (implementation specific) + const userId = options?.context?.user?.id; + return await db.getUserData(userId); + } +}); +``` + +## Accessing Springboard Engine + +```typescript +serverRegistry.registerServerModule(({ getEngine }) => { + // Access the Springboard engine for state/modules + const engine = getEngine(); + + // Example: Background task using engine + setInterval(async () => { + // Access registered modules + const registry = engine.getModuleRegistry(); + // ... perform background sync + }, 60000); +}); +``` + +## File Uploads + +```typescript +serverRegistry.registerServerModule(({ hono }) => { + hono.post('/api/upload', async (c) => { + const formData = await c.req.formData(); + const file = formData.get('file') as File; + + const buffer = await file.arrayBuffer(); + const filename = `uploads/${Date.now()}-${file.name}`; + + await writeFile(filename, Buffer.from(buffer)); + + return c.json({ path: filename }); + }); +}); +``` + +## Static Files + +```typescript +import { serveStatic } from 'hono/serve-static'; + +serverRegistry.registerServerModule(({ hono }) => { + // Serve static files from 'public' directory + hono.use('/static/*', serveStatic({ root: './' })); +}); +``` + +## CORS Configuration + +```typescript +import { cors } from 'hono/cors'; + +serverRegistry.registerServerModule(({ hono }) => { + hono.use('/api/*', cors({ + origin: ['https://myapp.com'], + allowMethods: ['GET', 'POST', 'PUT', 'DELETE'], + allowHeaders: ['Content-Type', 'Authorization'] + })); +}); +``` + +## Error Handling + +```typescript +serverRegistry.registerServerModule(({ hono }) => { + // Global error handler + hono.onError((err, c) => { + console.error('Server error:', err); + return c.json({ + error: err.message, + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined + }, 500); + }); + + // Route-specific try/catch + hono.get('/api/risky', async (c) => { + try { + const result = await riskyOperation(); + return c.json(result); + } catch (error) { + return c.json({ error: error.message }, 400); + } + }); +}); +``` + +## Database Integration + +```typescript +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import Database from 'better-sqlite3'; + +const sqlite = new Database('data.db'); +const db = drizzle(sqlite); + +serverRegistry.registerServerModule(({ hono }) => { + hono.get('/api/users', async (c) => { + const users = await db.select().from(usersTable); + return c.json(users); + }); +}); +``` + +## Common Patterns + +### Auth-Protected Routes +```typescript +const requireAuth = async (c, next) => { + const token = c.req.header('Authorization'); + if (!token) return c.json({ error: 'Unauthorized' }, 401); + + try { + const user = await verifyToken(token); + c.set('user', user); + await next(); + } catch { + return c.json({ error: 'Invalid token' }, 401); + } +}; + +serverRegistry.registerServerModule(({ hono }) => { + hono.use('/api/protected/*', requireAuth); + + hono.get('/api/protected/me', (c) => { + const user = c.get('user'); + return c.json(user); + }); +}); +``` + +### Rate Limiting +```typescript +import { rateLimiter } from 'hono-rate-limiter'; + +serverRegistry.registerServerModule(({ hono }) => { + hono.use('/api/*', rateLimiter({ + windowMs: 60 * 1000, // 1 minute + limit: 100, // 100 requests per window + })); +}); +``` diff --git a/packages/springboard/cli/src/docs/content/springboard-state-management.md b/packages/springboard/cli/src/docs/content/springboard-state-management.md new file mode 100644 index 00000000..0f1200cd --- /dev/null +++ b/packages/springboard/cli/src/docs/content/springboard-state-management.md @@ -0,0 +1,143 @@ +# State Management + +Springboard provides three types of state, each with different persistence and sync behavior. + +## State Types Comparison + +| Type | Persistence | Sync | Use Case | +|------|-------------|------|----------| +| SharedState | In-memory (server cache) | Real-time cross-device | Temporary shared data, UI sync | +| PersistentState | Database | Cross-device on load | User data, settings, saved state | +| UserAgentState | localStorage | None (device-local) | UI preferences, local settings | + +## createSharedState + +```typescript +const counter = await moduleAPI.statesAPI.createSharedState('counter', 0); +``` + +**Behavior:** +- Cached on server +- Broadcasts changes to all connected devices instantly +- Lost on server restart (unless persisted separately) +- Use for: cursor positions, typing indicators, live collaboration + +**Example:** +```typescript +const presenceState = await moduleAPI.statesAPI.createSharedState('presence', { + users: [] as { id: string; cursor: { x: number; y: number } }[] +}); + +// Update broadcasts to all devices +presenceState.setStateImmer(draft => { + const user = draft.users.find(u => u.id === myId); + if (user) user.cursor = { x, y }; +}); +``` + +## createPersistentState + +```typescript +const settings = await moduleAPI.statesAPI.createPersistentState('settings', { + theme: 'light', + language: 'en' +}); +``` + +**Behavior:** +- Stored in database (server-side) +- Loaded on app start +- Synced across devices +- Survives restarts +- Use for: user preferences, saved documents, game state + +**Example:** +```typescript +const todoList = await moduleAPI.statesAPI.createPersistentState('todos', { + items: [] as Todo[] +}); + +// Changes persist to database +todoList.setStateImmer(draft => { + draft.items.push({ id: uuid(), text: 'New todo', done: false }); +}); +``` + +## createUserAgentState + +```typescript +const uiState = await moduleAPI.statesAPI.createUserAgentState('ui', { + sidebarOpen: true, + lastViewedTab: 'home' +}); +``` + +**Behavior:** +- Stored in localStorage (browser) or local file (node) +- Never synced to server or other devices +- Device-specific +- Use for: collapsed panels, scroll positions, local drafts + +**Example:** +```typescript +const localPrefs = await moduleAPI.statesAPI.createUserAgentState('localPrefs', { + volume: 0.8, + recentSearches: [] as string[] +}); + +// Only affects this device +localPrefs.setStateImmer(draft => { + draft.recentSearches.unshift(query); + draft.recentSearches = draft.recentSearches.slice(0, 10); +}); +``` + +## StateSupervisor API + +All state types return a `StateSupervisor`: + +```typescript +// Get current value +const value = state.getState(); + +// Set new value (immutable) +state.setState(newValue); +state.setState(prev => ({ ...prev, count: prev.count + 1 })); + +// Mutate with Immer (mutable syntax, immutable result) +state.setStateImmer(draft => { + draft.count += 1; + draft.items.push(newItem); +}); + +// React hook (auto-subscribes component) +function MyComponent() { + const value = state.useState(); + return
{value.count}
; +} + +// RxJS Subject for manual subscriptions +const subscription = state.subject.subscribe(newValue => { + console.log('State changed:', newValue); +}); + +// Cleanup in onDestroy +moduleAPI.onDestroy(() => subscription.unsubscribe()); +``` + +## Choosing the Right State Type + +**Use SharedState when:** +- Changes need immediate sync (< 100ms) +- Data is ephemeral (OK to lose on restart) +- Examples: cursor positions, typing status, live reactions + +**Use PersistentState when:** +- Data must survive restarts +- Changes should sync but don't need instant delivery +- Examples: documents, settings, user profiles + +**Use UserAgentState when:** +- Data is device-specific +- No need to sync across devices +- Examples: UI state, local preferences, cached data diff --git a/packages/springboard/cli/src/docs/index.ts b/packages/springboard/cli/src/docs/index.ts new file mode 100644 index 00000000..28bd628e --- /dev/null +++ b/packages/springboard/cli/src/docs/index.ts @@ -0,0 +1,83 @@ +/** + * Documentation system for Springboard CLI + * + * Provides documentation discovery and retrieval for AI coding agents. + */ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +export interface DocSection { + slug: string; + title: string; + use_cases: string; +} + +export interface SectionsData { + sections: DocSection[]; +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const docsDir = __dirname; + +/** + * Load sections metadata + */ +export function getSections(): DocSection[] { + const data = JSON.parse( + readFileSync(join(docsDir, 'sections.json'), 'utf-8') + ) as SectionsData; + return data.sections; +} + +/** + * Get a section by slug + */ +export function getSection(slug: string): DocSection | undefined { + const sections = getSections(); + return sections.find(s => + s.slug === slug || + s.slug.endsWith(`/${slug}`) || + s.title.toLowerCase() === slug.toLowerCase() + ); +} + +/** + * Get documentation content for a section + */ +export function getDocContent(slug: string): string | null { + const section = getSection(slug); + if (!section) return null; + + // Convert slug to filename: springboard/module-api -> springboard-module-api.md + const filename = section.slug.replace(/\//g, '-') + '.md'; + + try { + return readFileSync(join(docsDir, 'content', filename), 'utf-8'); + } catch { + return null; + } +} + +/** + * List sections with their use_cases for display + */ +export function listSections(): { slug: string; title: string; use_cases: string }[] { + return getSections().map(s => ({ + slug: s.slug, + title: s.title, + use_cases: s.use_cases + })); +} + +/** + * Format sections list for output + */ +export function formatSectionsList(): string { + const sections = listSections(); + return sections.map(s => + `${s.slug}\n ${s.title}\n Use cases: ${s.use_cases}` + ).join('\n\n'); +} diff --git a/packages/springboard/cli/src/docs/sections.json b/packages/springboard/cli/src/docs/sections.json new file mode 100644 index 00000000..7e99dc5d --- /dev/null +++ b/packages/springboard/cli/src/docs/sections.json @@ -0,0 +1,69 @@ +{ + "sections": [ + { + "slug": "springboard/overview", + "title": "Springboard Overview", + "use_cases": "getting started, framework introduction, what is springboard, architecture overview, multi-device apps, real-time sync, full-stack javascript" + }, + { + "slug": "springboard/module-api", + "title": "ModuleAPI Reference", + "use_cases": "always, any springboard module, core API, registerRoute, createActions, onDestroy, getModule, statesAPI, module development" + }, + { + "slug": "springboard/state-management", + "title": "State Management", + "use_cases": "state, data storage, createSharedState, createPersistentState, createUserAgentState, cross-device sync, localStorage, database, StateSupervisor, useState, setState, setStateImmer" + }, + { + "slug": "springboard/actions", + "title": "Actions & RPC", + "use_cases": "actions, createActions, createAction, RPC, remote procedure call, server communication, async functions, action handlers" + }, + { + "slug": "springboard/routing", + "title": "Routing & Navigation", + "use_cases": "routes, registerRoute, navigation, React Router, URL params, documentMeta, SEO, hideApplicationShell, page components" + }, + { + "slug": "springboard/module-types", + "title": "Module Types", + "use_cases": "feature module, utility module, initializer module, module patterns, when to use each type, module organization" + }, + { + "slug": "springboard/module-communication", + "title": "Module Communication", + "use_cases": "getModule, module dependencies, AllModules interface, type-safe module access, interface merging, cross-module calls" + }, + { + "slug": "springboard/core-dependencies", + "title": "Core Dependencies", + "use_cases": "coreDependencies, log, showError, files, storage, rpc, isMaestro, KVStore, error handling, file operations" + }, + { + "slug": "springboard/lifecycle", + "title": "Module Lifecycle", + "use_cases": "onDestroy, cleanup, initialization order, module registration, Provider, subscriptions, memory leaks" + }, + { + "slug": "springboard/platforms", + "title": "Platform Support", + "use_cases": "browser, node, react-native, desktop, tauri, partykit, platform-specific code, deployment targets" + }, + { + "slug": "springboard/server-modules", + "title": "Server Modules", + "use_cases": "serverRegistry, Hono, HTTP routes, API endpoints, RPC middleware, server-side code, backend" + }, + { + "slug": "springboard/patterns", + "title": "Common Patterns", + "use_cases": "best practices, navigation reasons, modal system, activity tracking, hook system, songdrive patterns" + }, + { + "slug": "springboard/anti-patterns", + "title": "Anti-Patterns to Avoid", + "use_cases": "common mistakes, getModule at module level, missing optional chaining, race conditions, computed values in state" + } + ] +} diff --git a/packages/springboard/cli/src/docs_command.ts b/packages/springboard/cli/src/docs_command.ts new file mode 100644 index 00000000..91290bbf --- /dev/null +++ b/packages/springboard/cli/src/docs_command.ts @@ -0,0 +1,332 @@ +import { Command } from 'commander'; + +/** + * Creates the `sb docs` command with all subcommands for AI agent support. + * + * Provides documentation discovery and context for AI coding agents + * working with Springboard applications. + */ +export function createDocsCommand(): Command { + const docs = new Command('docs') + .description('Documentation and AI agent support tools') + .addHelpText('after', ` +Getting Started: + For AI agents: Run 'sb docs context' first to get comprehensive framework + information and available documentation sections. This provides everything + you need to start working with Springboard. + + The 'context' command includes the full list of available docs, so you + don't need to run 'list' separately. + +Workflow: + 1. sb docs context # Get full framework context (run this first) + 2. sb docs get
# Fetch specific docs only when needed + 3. sb docs examples show # View example code +`) + .action(() => { + // When `sb docs` is called without subcommand, show help + docs.help(); + }); + + // sb docs list + docs.command('list') + .description('List available documentation sections with use cases') + .option('--json', 'Output as JSON') + .action(async (options: { json?: boolean }) => { + const { listSections } = await import('./docs/index.js'); + const sections = listSections(); + + if (options.json) { + console.log(JSON.stringify(sections, null, 2)); + } else { + console.log('Available Springboard Documentation Sections:\n'); + for (const section of sections) { + console.log(`${section.slug}`); + console.log(` ${section.title}`); + console.log(` Use cases: ${section.use_cases}`); + console.log(); + } + } + }); + + // sb docs get + docs.command('get') + .description('Fetch documentation for specific sections') + .argument('', 'Documentation section(s) to fetch') + .action(async (sections: string[]) => { + const { getDocContent, getSection } = await import('./docs/index.js'); + + for (const slug of sections) { + const section = getSection(slug); + if (!section) { + console.error(`Section "${slug}" not found. Run 'sb docs list' to see available sections.\n`); + continue; + } + + const content = getDocContent(slug); + if (!content) { + console.error(`Content for "${slug}" not available.\n`); + continue; + } + + console.log(content); + console.log('\n---\n'); + } + }); + + // sb docs context + docs.command('context') + .description('Output full context prompt for AI agents') + .action(async () => { + const { formatSectionsList } = await import('./docs/index.js'); + const { listExamples } = await import('./examples/index.js'); + + const sectionsList = formatSectionsList(); + const examples = listExamples(); + + const context = `# Springboard Development Context + +You are working on a Springboard application. Springboard is a full-stack JavaScript framework built on React, Hono, JSON-RPC, and WebSockets for building real-time, multi-device applications. + +## Available Documentation Sections + +${sectionsList} + +## Key Concepts + +### Module Structure +\`\`\`typescript +import springboard from 'springboard'; + +springboard.registerModule('ModuleName', {}, async (moduleAPI) => { + // Create state (pick the right type!) + const state = await moduleAPI.statesAPI.createSharedState('name', initialValue); + + // Create actions (automatically RPC-enabled) + const actions = moduleAPI.createActions({ + actionName: async (args) => { /* ... */ } + }); + + // Register routes + moduleAPI.registerRoute('/', {}, MyComponent); + + // Cleanup + moduleAPI.onDestroy(() => { /* cleanup */ }); + + // Return public API + return { state, actions }; +}); +\`\`\` + +### State Types +- **createSharedState**: Real-time sync across devices (ephemeral) +- **createPersistentState**: Database-backed, survives restarts +- **createUserAgentState**: Local only (localStorage) + +### StateSupervisor Methods +\`\`\`typescript +state.getState() // Get current value +state.setState(newValue) // Immutable update +state.setStateImmer(draft => {}) // Mutable update with Immer +state.useState() // React hook +\`\`\` + +## Workflow + +1. **Use this context** + your React/TypeScript knowledge to write code +2. **Fetch specific docs** with \`sb docs get
\` only when needed +3. **See examples** with \`sb docs examples list\` and \`sb docs examples show \` + +## Available Examples + +${examples.map(e => `- ${e.name}: ${e.description}`).join('\n')} + +## Common Mistakes to Avoid + +1. **Don't call getModule at module level** - call inside routes/actions +2. **Use optional chaining** for module access: \`maybeModule?.actions?.doSomething()\` +3. **Don't mutate state directly** - use setState or setStateImmer +4. **Clean up subscriptions** in onDestroy +5. **Don't store computed values** in state - use useMemo + +## Getting Specific Documentation + +If you need detailed documentation on a topic, run: +\`\`\`bash +sb docs get springboard/module-api +sb docs get springboard/state-management +sb docs get springboard/patterns +\`\`\` +`; + + console.log(context); + }); + + // sb docs types + docs.command('types') + .description('Output core TypeScript type definitions') + .action(async () => { + const types = `# Springboard Core Type Definitions + +## ModuleAPI + +\`\`\`typescript +interface ModuleAPI { + moduleId: string; + fullPrefix: string; + + statesAPI: { + createSharedState(name: string, initial: T): Promise>; + createPersistentState(name: string, initial: T): Promise>; + createUserAgentState(name: string, initial: T): Promise>; + }; + + createActions>(actions: T): T; + createAction(name: string, opts: {}, cb: ActionCallback): ActionFn; + + registerRoute(path: string, options: RegisterRouteOptions, component: React.ComponentType): void; + registerApplicationShell(component: React.ComponentType): void; + + getModule(id: K): AllModules[K]; + + onDestroy(callback: () => void): void; + setRpcMode(mode: 'local' | 'remote'): void; + + deps: { + core: CoreDependencies; + module: ModuleDependencies; + }; +} +\`\`\` + +## StateSupervisor + +\`\`\`typescript +interface StateSupervisor { + getState(): T; + setState(value: T | ((prev: T) => T)): T; + setStateImmer(callback: (draft: T) => void): T; + useState(): T; // React hook + subject: Subject; // RxJS Subject +} +\`\`\` + +## CoreDependencies + +\`\`\`typescript +interface CoreDependencies { + log: (...args: any[]) => void; + showError: (error: string) => void; + files: { + saveFile: (name: string, content: string) => Promise; + }; + storage: { + remote: KVStore; + userAgent: KVStore; + }; + rpc: { + remote: Rpc; + local?: Rpc; + }; + isMaestro: () => boolean; +} +\`\`\` + +## KVStore + +\`\`\`typescript +interface KVStore { + get(key: string): Promise; + set(key: string, value: T): Promise; + getAll(): Promise | null>; +} +\`\`\` + +## RegisterRouteOptions + +\`\`\`typescript +interface RegisterRouteOptions { + hideApplicationShell?: boolean; + documentMeta?: DocumentMeta | ((context: RouteContext) => DocumentMeta); +} + +interface DocumentMeta { + title?: string; + description?: string; + keywords?: string; + 'og:title'?: string; + 'og:description'?: string; + 'og:image'?: string; + [key: string]: string | undefined; +} +\`\`\` + +## Module Interface Merging + +\`\`\`typescript +// Declare your module's exports for type-safe getModule() +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + myModule: { + state: StateSupervisor; + actions: { + doSomething: (args: Args) => Promise; + }; + }; + } +} +\`\`\` +`; + + console.log(types); + }); + + // sb docs examples + const examplesCmd = docs.command('examples') + .description('View example modules'); + + examplesCmd.command('list') + .description('List all available examples') + .option('--json', 'Output as JSON') + .action(async (options: { json?: boolean }) => { + const { listExamples } = await import('./examples/index.js'); + const examplesList = listExamples(); + + if (options.json) { + console.log(JSON.stringify(examplesList, null, 2)); + } else { + console.log('Available Springboard Examples:\n'); + for (const ex of examplesList) { + console.log(`${ex.name}`); + console.log(` ${ex.title}`); + console.log(` ${ex.description}`); + console.log(` Category: ${ex.category}`); + console.log(` Tags: ${ex.tags.join(', ')}`); + console.log(); + } + } + }); + + examplesCmd.command('show') + .description('Show code for a specific example') + .argument('', 'Example name') + .action(async (name: string) => { + const { getExample } = await import('./examples/index.js'); + const example = getExample(name); + + if (!example) { + console.error(`Example "${name}" not found. Run 'sb docs examples list' to see available examples.`); + process.exit(1); + } + + console.log(`# ${example.title}\n`); + console.log(`${example.description}\n`); + console.log(`Category: ${example.category}`); + console.log(`Tags: ${example.tags.join(', ')}\n`); + console.log('```tsx'); + console.log(example.code); + console.log('```'); + }); + + return docs; +} diff --git a/packages/springboard/cli/src/examples/basic-feature-module.txt b/packages/springboard/cli/src/examples/basic-feature-module.txt new file mode 100644 index 00000000..e4d7fb24 --- /dev/null +++ b/packages/springboard/cli/src/examples/basic-feature-module.txt @@ -0,0 +1,95 @@ +/** + * Example: Basic Feature Module + * + * This example demonstrates a simple feature module with: + * - Shared state (synced across devices) + * - Actions (RPC-enabled functions) + * - Routes (UI pages) + * - Lifecycle cleanup + */ + +import springboard from 'springboard'; + +springboard.registerModule( + 'BasicFeatureExample', + {}, + async (moduleAPI) => { + // 1. Create shared state (synced across all connected devices) + const counterState = await moduleAPI.statesAPI.createSharedState( + 'counter', + 0 + ); + + // 2. Create actions (automatically RPC-enabled) + const actions = moduleAPI.createActions({ + increment: async () => { + const current = counterState.getState(); + counterState.setState(current + 1); + return { newValue: current + 1 }; + }, + + decrement: async () => { + const current = counterState.getState(); + counterState.setState(current - 1); + return { newValue: current - 1 }; + }, + + reset: async () => { + counterState.setState(0); + return { newValue: 0 }; + }, + }); + + // 3. Register routes + moduleAPI.registerRoute( + '/', + {}, + () => { + const counter = counterState.useState(); + + return ( +
+

Basic Feature Example

+

Counter: {counter}

+ + + +
+ ); + } + ); + + // 4. Lifecycle cleanup (if needed) + moduleAPI.onDestroy(() => { + console.log('BasicFeatureExample module destroyed'); + }); + + // 5. Return public API for other modules + return { + states: { counter: counterState }, + actions, + }; + } +); + +// Type-safe module registry +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + BasicFeatureExample: { + states: { + counter: ReturnType>; + }; + actions: { + increment: () => Promise<{ newValue: number }>; + decrement: () => Promise<{ newValue: number }>; + reset: () => Promise<{ newValue: number }>; + }; + }; + } +} diff --git a/packages/springboard/cli/src/examples/index.ts b/packages/springboard/cli/src/examples/index.ts new file mode 100644 index 00000000..719e9d48 --- /dev/null +++ b/packages/springboard/cli/src/examples/index.ts @@ -0,0 +1,78 @@ +/** + * Springboard Example Modules + * + * These examples are bundled with the CLI package to help AI agents + * understand common patterns in Springboard development. + */ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +export interface Example { + name: string; + title: string; + description: string; + category: 'state' | 'actions' | 'routing' | 'patterns'; + tags: string[]; + code: string; +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const examplesDir = __dirname; + +export const examples: Example[] = [ + { + name: 'basic-feature-module', + title: 'Basic Feature Module', + description: 'Simple feature module with shared state, actions, and routes. Good starting point for most features.', + category: 'patterns', + tags: ['shared-state', 'actions', 'routes', 'beginner'], + code: readFileSync(join(examplesDir, 'basic-feature-module.txt'), 'utf-8'), + }, + { + name: 'persistent-state-module', + title: 'Persistent State Module', + description: 'Module using persistent state stored in database. Data survives app restarts and syncs across devices.', + category: 'state', + tags: ['persistent-state', 'database', 'settings'], + code: readFileSync(join(examplesDir, 'persistent-state-module.txt'), 'utf-8'), + }, + { + name: 'user-agent-state-module', + title: 'User Agent State Module', + description: 'Module using localStorage-backed state. Perfect for device-specific UI preferences.', + category: 'state', + tags: ['user-agent-state', 'localStorage', 'ui-state'], + code: readFileSync(join(examplesDir, 'user-agent-state-module.txt'), 'utf-8'), + }, +]; + +/** + * Get an example by name + */ +export function getExample(name: string): Example | undefined { + return examples.find((ex) => ex.name === name); +} + +/** + * Get examples by category + */ +export function getExamplesByCategory(category: Example['category']): Example[] { + return examples.filter((ex) => ex.category === category); +} + +/** + * Get examples by tag + */ +export function getExamplesByTag(tag: string): Example[] { + return examples.filter((ex) => ex.tags.includes(tag)); +} + +/** + * List all examples with metadata (no code) + */ +export function listExamples(): Omit[] { + return examples.map(({ code, ...metadata }) => metadata); +} diff --git a/packages/springboard/cli/src/examples/persistent-state-module.txt b/packages/springboard/cli/src/examples/persistent-state-module.txt new file mode 100644 index 00000000..98729ff6 --- /dev/null +++ b/packages/springboard/cli/src/examples/persistent-state-module.txt @@ -0,0 +1,124 @@ +/** + * Example: Persistent State Module + * + * This example demonstrates using persistent state that is: + * - Stored in the database + * - Survives app restarts + * - Synced across devices + */ + +import springboard from 'springboard'; + +interface UserSettings { + theme: 'light' | 'dark'; + notifications: boolean; + language: string; +} + +springboard.registerModule( + 'PersistentStateExample', + {}, + async (moduleAPI) => { + // Persistent state - stored in database + const settingsState = await moduleAPI.statesAPI.createPersistentState( + 'userSettings', + { + theme: 'light', + notifications: true, + language: 'en', + } + ); + + // Actions to modify settings + const actions = moduleAPI.createActions({ + toggleTheme: async () => { + settingsState.setStateImmer((draft) => { + draft.theme = draft.theme === 'light' ? 'dark' : 'light'; + }); + }, + + toggleNotifications: async () => { + settingsState.setStateImmer((draft) => { + draft.notifications = !draft.notifications; + }); + }, + + setLanguage: async (args: { language: string }) => { + settingsState.setStateImmer((draft) => { + draft.language = args.language; + }); + }, + }); + + // Register UI + moduleAPI.registerRoute( + '/settings', + {}, + () => { + const settings = settingsState.useState(); + + return ( +
+

User Settings

+ +
+ +
+ +
+ +
+ +
+ +
+
+ ); + } + ); + + return { + states: { settings: settingsState }, + actions, + }; + } +); + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + PersistentStateExample: { + states: { + settings: ReturnType>; + }; + actions: { + toggleTheme: () => Promise; + toggleNotifications: () => Promise; + setLanguage: (args: { language: string }) => Promise; + }; + }; + } +} diff --git a/packages/springboard/cli/src/examples/user-agent-state-module.txt b/packages/springboard/cli/src/examples/user-agent-state-module.txt new file mode 100644 index 00000000..afe0b009 --- /dev/null +++ b/packages/springboard/cli/src/examples/user-agent-state-module.txt @@ -0,0 +1,134 @@ +/** + * Example: User Agent State Module + * + * This example demonstrates local state that is: + * - Stored only in localStorage (not synced) + * - Device-specific + * - Perfect for UI preferences, local-only data + */ + +import springboard from 'springboard'; + +interface LocalUIState { + sidebarCollapsed: boolean; + lastViewedPage: string; + recentSearches: string[]; +} + +springboard.registerModule( + 'UserAgentStateExample', + {}, + async (moduleAPI) => { + // User agent state - stored locally in localStorage + const uiState = await moduleAPI.statesAPI.createUserAgentState( + 'localUIState', + { + sidebarCollapsed: false, + lastViewedPage: '/', + recentSearches: [], + } + ); + + // Actions for local UI state + const actions = moduleAPI.createActions({ + toggleSidebar: async () => { + uiState.setStateImmer((draft) => { + draft.sidebarCollapsed = !draft.sidebarCollapsed; + }); + }, + + recordPageView: async (args: { page: string }) => { + uiState.setStateImmer((draft) => { + draft.lastViewedPage = args.page; + }); + }, + + addSearch: async (args: { query: string }) => { + uiState.setStateImmer((draft) => { + // Keep only last 10 searches + draft.recentSearches = [ + args.query, + ...draft.recentSearches.filter((q) => q !== args.query), + ].slice(0, 10); + }); + }, + + clearSearchHistory: async () => { + uiState.setStateImmer((draft) => { + draft.recentSearches = []; + }); + }, + }); + + // Register UI + moduleAPI.registerRoute( + '/local-ui', + {}, + () => { + const ui = uiState.useState(); + + return ( +
+ {/* Sidebar */} + {!ui.sidebarCollapsed && ( + + )} + + {/* Main content */} +
+

User Agent State Example

+

Last viewed: {ui.lastViewedPage}

+ + +
+ { + if (e.key === 'Enter' && e.currentTarget.value) { + actions.addSearch({ query: e.currentTarget.value }); + e.currentTarget.value = ''; + } + }} + /> +
+
+
+ ); + } + ); + + return { + states: { ui: uiState }, + actions, + }; + } +); + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + UserAgentStateExample: { + states: { + ui: ReturnType>; + }; + actions: { + toggleSidebar: () => Promise; + recordPageView: (args: { page: string }) => Promise; + addSearch: (args: { query: string }) => Promise; + clearSearchHistory: () => Promise; + }; + }; + } +} diff --git a/packages/springboard/cli/src/index.ts b/packages/springboard/cli/src/index.ts index 87063124..89fb7330 100644 --- a/packages/springboard/cli/src/index.ts +++ b/packages/springboard/cli/src/index.ts @@ -26,5 +26,4 @@ export { program, resolveEntrypoint, loadPlugins, - parsePlatforms, } from './cli.js'; diff --git a/packages/springboard/create-springboard-app/src/cli.ts b/packages/springboard/create-springboard-app/src/cli.ts index aac19b69..24b42aca 100644 --- a/packages/springboard/create-springboard-app/src/cli.ts +++ b/packages/springboard/create-springboard-app/src/cli.ts @@ -103,6 +103,38 @@ program writeFileSync(`${process.cwd()}/vite.config.ts`, viteString); console.log('Created vite config vite.config.ts'); + const agentDocsContent = `# Springboard Development Guide + +This application is built with the **Springboard framework**. + +## Getting Started + +**Before writing any code, run:** + +\`\`\`bash +npx sb docs context +\`\`\` + +This outputs comprehensive framework information including available documentation +sections, key concepts, and workflow guidance. + +## Recommended Workflow + +1. **Run \`sb docs context\`** at the start of your session +2. **Write code** using your knowledge + the context from step 1 +3. **Fetch specific docs** only when needed: \`sb docs get
\` +4. **View examples** for reference code: \`sb docs examples show \` + +## Other Useful Commands + +- \`sb docs --help\` - See all available commands +- \`sb docs types\` - Get TypeScript type definitions +- \`sb docs examples list\` - See available example modules +`; + writeFileSync(`${process.cwd()}/CLAUDE.md`, agentDocsContent); + writeFileSync(`${process.cwd()}/AGENTS.md`, agentDocsContent); + console.log('Created CLAUDE.md and AGENTS.md with AI agent instructions'); + const packageJsonPath = `${process.cwd()}/package.json`; const packageJson = JSON.parse(readFileSync(packageJsonPath).toString()); diff --git a/packages/springboard/package.json b/packages/springboard/package.json index 59019bf6..71a7907b 100644 --- a/packages/springboard/package.json +++ b/packages/springboard/package.json @@ -322,10 +322,14 @@ ] } }, + "bin": { + "sb": "./cli/dist/cli.js" + }, "files": [ "src", "dist", - "vite-plugin" + "vite-plugin", + "cli/dist" ], "scripts": { "test": "vitest --run", @@ -337,7 +341,7 @@ "build:watch": "npm run build -- --watch", "build:all": "./scripts/build-all.sh", "build:vite-plugin": "cd vite-plugin && npm run build", - "prepublishOnly": "npm run build && npm run build:vite-plugin", + "prepublishOnly": "npm run build:all", "publish:local": "./scripts/publish-local.sh" }, "repository": { @@ -362,6 +366,7 @@ }, "homepage": "https://springboard.js.org", "dependencies": { + "commander": "^14.0.3", "dexie": "^4.2.1", "json-rpc-2.0": "^1.7.1", "reconnecting-websocket": "^4.4.0" diff --git a/packages/springboard/scripts/build-all.sh b/packages/springboard/scripts/build-all.sh index 23ca1b54..bb2ff39c 100755 --- a/packages/springboard/scripts/build-all.sh +++ b/packages/springboard/scripts/build-all.sh @@ -17,7 +17,12 @@ cd "$PACKAGE_DIR" pnpm build echo "" -echo "2. Building vite-plugin..." +echo "2. Building CLI..." +cd "$PACKAGE_DIR/cli" +pnpm build + +echo "" +echo "3. Building vite-plugin..." cd "$PACKAGE_DIR/vite-plugin" pnpm build @@ -27,4 +32,5 @@ echo "✓ Build complete!" echo "" echo "Outputs:" echo " - Main package: $PACKAGE_DIR/dist/" +echo " - CLI: $PACKAGE_DIR/cli/dist/" echo " - Vite plugin: $PACKAGE_DIR/vite-plugin/dist/" diff --git a/packages/springboard/src/core/engine/engine.tsx b/packages/springboard/src/core/engine/engine.tsx index 03e6b302..7755f46d 100644 --- a/packages/springboard/src/core/engine/engine.tsx +++ b/packages/springboard/src/core/engine/engine.tsx @@ -5,7 +5,7 @@ import {ClassModuleCallback, ModuleCallback, RegisterModuleOptions, springboard, import React, {createContext, useContext, useState} from 'react'; import {useMount} from '../hooks/useMount.js'; -import {ExtraModuleDependencies, Module, ModuleRegistry} from '../module_registry/module_registry.js'; +import {AllModules, ExtraModuleDependencies, Module, ModuleRegistry} from '../module_registry/module_registry.js'; import {SharedStateService} from '../services/states/shared_state_service.js'; import {ModuleAPI} from './module_api.js'; @@ -227,6 +227,19 @@ export const useSpringboardEngine = () => { return useContext(engineContext); }; +/** + * React hook to access a module by ID from within a component. + * Use this instead of moduleAPI.getModule() when in React components. + * + * @example + * const audioPlayer = useModule('AudioPlayer'); + * const currentFile = audioPlayer.currentlyPlayingFile.useState(); + */ +export const useModule = (moduleId: ModuleId): AllModules[ModuleId] => { + const engine = useSpringboardEngine(); + return engine.moduleRegistry.getModule(moduleId); +}; + type SpringboardProviderProps = React.PropsWithChildren<{ engine: Springboard; }>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9a3d1e3..5de93829 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -417,6 +417,9 @@ importers: '@tauri-apps/plugin-shell': specifier: ^2.3.3 version: 2.3.3 + commander: + specifier: ^14.0.3 + version: 14.0.3 crossws: specifier: ^0.4.4 version: 0.4.4 @@ -2000,6 +2003,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -2471,7 +2478,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} @@ -5491,7 +5498,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(@vitest/ui@4.0.18)(jsdom@25.0.1)(tsx@4.20.6) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.17.48)(@vitest/ui@4.0.18)(jsdom@25.0.1)(tsx@4.21.0) '@vitest/utils@2.1.9': dependencies: @@ -5785,6 +5792,8 @@ snapshots: commander@12.1.0: {} + commander@14.0.3: {} + commander@4.1.1: {} composed-offset-position@0.0.6(@floating-ui/utils@0.2.10):