Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
900da7a
Move springboard dev to single-port ModuleRunner
Feb 25, 2026
51fe57e
Fix dev plugin stream and module-graph typings
Mar 22, 2026
c6b22d8
Add e2e dev-route HMR test for registerServerModule
Mar 22, 2026
ac6c546
docs: clarify registerServerModule contract
Mar 25, 2026
76d916a
apps/vite-test: add start script and configurable port
Mar 25, 2026
b2a694f
Committed the two requested files as `apps/vite-test: add start scrip…
Mar 25, 2026
1543cb5
apps/vite-test: add debug log for module load
Mar 25, 2026
2335c8e
fix: use single-port dev server in vite plugin
Mar 25, 2026
b3b9628
fix: install vite dev middleware before spa fallback
Mar 25, 2026
6a8c1c4
refactor: remove unused vite plugin modules
Mar 25, 2026
c44e8be
Committed the deletions as `refactor: remove unused vite plugin modul…
Mar 25, 2026
98cb178
refactor: store generated vite files in node_modules
Mar 25, 2026
8fb45c6
edit scripts
Mar 25, 2026
1616cba
fix: serve dev web entry through vite transform
Mar 25, 2026
7b0b74c
build: support SongDrive Vite migration
Apr 23, 2026
b7ebb0d
fix: improve jamtools core vitest module resolution
May 6, 2026
d05024d
test: tighten macro input test bootstrap
May 6, 2026
83e635a
chore: initialize beads workflow for springboard
May 6, 2026
782b48d
feat: scaffold defineModule and entrypoint descriptors
May 8, 2026
5bd9eb0
feat: support async springboard entrypoint descriptors
May 10, 2026
6901287
feat: add expo-native react native host shell
May 10, 2026
75792e6
feat: add expo auth and push helpers
May 10, 2026
f2559ab
Update lockfile for Expo-native Springboard deps
May 12, 2026
7e08d2e
Limit Springboard CI to Springboard packages
May 15, 2026
d817a56
Add shared mobile WebView host E2E fixtures
Jun 8, 2026
a0a9a92
Make Springboard git refs easier to consume
Jun 8, 2026
78f4025
Include Springboard build configs in git package
Jun 8, 2026
1705b90
Declare Expo config plugins for mobile E2E
Jun 8, 2026
698d605
Fix Springboard mobile E2E Android CLI deps
Jun 8, 2026
b5eb1e8
Export Springboard React Native platform entry
Jun 8, 2026
edf4ae9
Fix mobile E2E Expo autolinking
Jun 8, 2026
7712122
Configure mobile E2E React Native autolinking
Jun 8, 2026
b707f4c
Fix Node CrossWS adapter interop
Jun 8, 2026
8f048ac
Build mobile E2E release APK fallback
Jun 8, 2026
364c4bd
Fix mobile E2E Metro bundling fallback
Jun 8, 2026
635ba86
Stabilize mobile E2E fixture Metro config
Jun 8, 2026
f696daa
Add descriptor-based Springboard test helper
Jun 8, 2026
8ef2dbb
Use direct mobile WebView host export
Jun 8, 2026
572ec0c
Stabilize mobile E2E readiness wait
Jun 8, 2026
c8c4e8a
Stabilize mobile E2E readiness check
Jun 8, 2026
6b770ae
Remove extra module dependency plumbing
Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
{
"enabledMcpjsonServers": [
"batchit"
],
"hooks": {
"PreCompact": [
{
"hooks": [
{
"command": "bd prime",
"type": "command"
}
],
"matcher": ""
}
],
"SessionStart": [
{
"hooks": [
{
"command": "bd prime",
"type": "command"
}
],
"matcher": ""
}
]
},
"permissions": {
"allow": [
"WebFetch(domain:github.com)",
Expand All @@ -23,8 +50,5 @@
"mcp__batchit__batch_execute"
],
"deny": []
},
"enabledMcpjsonServers": [
"batchit"
]
}
}
}
88 changes: 88 additions & 0 deletions .springboard/node-dev-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import process from 'node:process';

import crosswsNode from 'crossws/adapters/node';

import { initApp } from 'springboard/server/hono_app';
import { makeWebsocketServerCoreDependenciesWithSqlite } from 'springboard/platforms/node/services/ws_server_core_dependencies';
import { LocalJsonNodeKVStoreService } from 'springboard/platforms/node/services/node_kvstore_service';
import { CoreDependencies, Springboard } from 'springboard/core';
import {
springboard,
clearRegisteredModules,
clearRegisteredClassModules,
clearRegisteredSplashScreen,
} from 'springboard/core/engine/register';
import { resetServerRegistry } from 'springboard/server/register';

export type DevServerHandle = {
fetch: (request: Request) => Promise<Response>;
ws: ReturnType<typeof crosswsNode>;
dispose: () => Promise<void>;
};

springboard.reset();
clearRegisteredModules();
clearRegisteredClassModules();
clearRegisteredSplashScreen();
resetServerRegistry();

await import('../src/server-entry.ts');

export async function createDevServer(): Promise<DevServerHandle> {
const nodeKvDeps = await makeWebsocketServerCoreDependenciesWithSqlite();
const useWebSocketsForRpc = import.meta.env.VITE_USE_WEBSOCKETS_FOR_RPC === 'true';

let wsNode: ReturnType<typeof crosswsNode>;

const { app, serverAppDependencies, injectResources, createWebSocketHooks } = initApp({
broadcastMessage: (message) => {
return wsNode.publish('event', message);
},
remoteKV: nodeKvDeps.kvStoreFromKysely,
userAgentKV: new LocalJsonNodeKVStoreService('userAgent'),
enableStaticRoutes: false,
});

app.notFound((c) => {
c.header('x-springboard-fallback', '1');
return c.text('', 404);
});

wsNode = crosswsNode({
hooks: createWebSocketHooks(useWebSocketsForRpc),
});

const coreDeps: CoreDependencies = {
log: console.log,
showError: console.error,
storage: serverAppDependencies.storage,
isMaestro: () => true,
rpc: serverAppDependencies.rpc,
};

Object.assign(coreDeps, serverAppDependencies);

const engine = new Springboard(coreDeps, {});

injectResources({
engine,
serveStaticFile: async (c, _fileName, headers) => {
Object.entries(headers).forEach(([key, value]) => {
c.header(key, value);
});
c.status(404);
return c.text('Not found');
},
getEnvValue: (name) => process.env[name],
});

await engine.initialize();

return {
fetch: app.fetch,
ws: wsNode,
dispose: async () => {
wsNode.closeAll();
},
};
}
163 changes: 163 additions & 0 deletions .springboard/node-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import process from 'node:process';
import path from 'node:path';

import { serve } from '@hono/node-server';
import crosswsNode from 'crossws/adapters/node';
import type { Server } from 'node:http';

import { initApp } from 'springboard/server/hono_app';
import { makeWebsocketServerCoreDependenciesWithSqlite } from 'springboard/platforms/node/services/ws_server_core_dependencies';
import { LocalJsonNodeKVStoreService } from 'springboard/platforms/node/services/node_kvstore_service';
import { CoreDependencies, Springboard } from 'springboard/core';
import '../src/server-entry.ts';

/**
* Node.js server entrypoint with HMR support
*
* This file is generated by the Springboard Vite plugin and serves as the
* entry point for the Node.js dev server. It:
*
* 1. Imports the user's application entry (which registers modules)
* 2. Exports start/stop functions for lifecycle management
* 3. Supports HMR via import.meta.hot.dispose()
*/

let server: Server | null = null;
let engine: Springboard | null = null;

/**
* Start the node server
*/
export async function start() {
// If server is already running, stop it first
if (server) {
await stop();
}

try {
const webappFolder = process.env.WEBAPP_FOLDER || './dist';
const webappDistFolder = webappFolder;

const nodeKvDeps = await makeWebsocketServerCoreDependenciesWithSqlite();
const useWebSocketsForRpc = import.meta.env.VITE_USE_WEBSOCKETS_FOR_RPC === 'true';

let wsNode: ReturnType<typeof crosswsNode>;

const { app, serverAppDependencies, injectResources, createWebSocketHooks } = initApp({
broadcastMessage: (message) => {
return wsNode.publish('event', message);
},
remoteKV: nodeKvDeps.kvStoreFromKysely,
userAgentKV: new LocalJsonNodeKVStoreService('userAgent'),
});

wsNode = crosswsNode({
hooks: createWebSocketHooks(useWebSocketsForRpc),
});

// Use configured port (ignores process.env.PORT to avoid conflicts)
const port = 1337;

// Start the HTTP server
server = serve({
fetch: app.fetch,
port,
}, (info) => {
console.log(`Server listening on http://localhost:${info.port}`);
});

server.on('upgrade', (request, socket, head) => {
const url = new URL(request.url || '', `http://${request.headers.host}`);
if (url.pathname === '/ws') {
wsNode.handleUpgrade(request, socket, head);
} else {
socket.end('HTTP/1.1 404 Not Found\r\n\r\n');
}
});

const coreDeps: CoreDependencies = {
log: console.log,
showError: console.error,
storage: serverAppDependencies.storage,
isMaestro: () => true,
rpc: serverAppDependencies.rpc,
};

Object.assign(coreDeps, serverAppDependencies);

const extraDeps = {}; // TODO: remove this extraDeps thing from the framework

engine = new Springboard(coreDeps, extraDeps);

injectResources({
engine,
serveStaticFile: async (c, fileName, headers) => {
try {
const fullPath = `${webappDistFolder}/${fileName}`;
const fs = await import('node:fs');
const data = await fs.promises.readFile(fullPath, 'utf-8');
c.status(200);

if (headers) {
Object.entries(headers).forEach(([key, value]) => {
c.header(key, value);
});
}

return c.body(data);
} catch (error) {
console.error('Error serving file:', error);
c.status(404);
return c.text('404 Not found');
}
},
getEnvValue: name => process.env[name],
});

await engine.initialize();
console.log('Node application started successfully');
} catch (error) {
console.error('Failed to start node server:', error);
throw error;
}
}

/**
* Stop the node server
*/
export async function stop() {
if (!server) {
return;
}

return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Server close timeout'));
}, 5000);

server!.close((err) => {
clearTimeout(timeout);
if (err) {
reject(err);
} else {
console.log('Server stopped successfully');
server = null;
engine = null; // TODO: add explicit shutdown once the engine exposes it
resolve();
}
});
});
}

// HMR support: clean up before module reload
if (import.meta.hot) {
import.meta.hot.dispose(async () => {
console.log('[HMR] Stopping server before reload...');
await stop();
});
}

// Auto-start in production builds (not in dev mode)
if (!import.meta.env.DEV) {
start().catch(console.error);
}
84 changes: 84 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Agent Instructions

This project uses **bd** (beads) for issue tracking. Run `bd prime` for full workflow context.

## Quick Reference

```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work atomically
bd close <id> # Complete work
bd dolt push # Push beads data to remote
```

## Non-Interactive Shell Commands

**ALWAYS use non-interactive flags** with file operations to avoid hanging on confirmation prompts.

Shell commands like `cp`, `mv`, and `rm` may be aliased to include `-i` (interactive) mode on some systems, causing the agent to hang indefinitely waiting for y/n input.

**Use these forms instead:**
```bash
# Force overwrite without prompting
cp -f source dest # NOT: cp source dest
mv -f source dest # NOT: mv source dest
rm -f file # NOT: rm file

# For recursive operations
rm -rf directory # NOT: rm -r directory
cp -rf source dest # NOT: cp -r source dest
```

**Other commands that may prompt:**
- `scp` - use `-o BatchMode=yes` for non-interactive
- `ssh` - use `-o BatchMode=yes` to fail instead of prompting
- `apt-get` - use `-y` flag
- `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var

<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->
## Beads Issue Tracker

This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.

### Quick Reference

```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
```

### Rules

- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
- Run `bd prime` for detailed command reference and session close protocol
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files

## Session Completion

**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.

**MANDATORY WORKFLOW:**

1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd dolt push
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session

**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
<!-- END BEADS INTEGRATION -->
Loading
Loading