@mindstudio-ai/local-model-tunnel 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -11
- package/dist/chunk-C3JPRLSS.js +1485 -0
- package/dist/chunk-C3JPRLSS.js.map +1 -0
- package/dist/{chunk-KLOTDVWL.js → chunk-QALGC7T7.js} +56 -310
- package/dist/chunk-QALGC7T7.js.map +1 -0
- package/dist/chunk-WFQXIMTS.js +378 -0
- package/dist/chunk-WFQXIMTS.js.map +1 -0
- package/dist/cli.js +18 -2
- package/dist/cli.js.map +1 -1
- package/dist/headless.d.ts +113 -0
- package/dist/headless.js +8 -0
- package/dist/headless.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -2
- package/dist/{tui-VXHUEAFM.js → tui-4PJCFILV.js} +1467 -318
- package/dist/tui-4PJCFILV.js.map +1 -0
- package/package.json +3 -1
- package/dist/chunk-KLOTDVWL.js.map +0 -1
- package/dist/tui-VXHUEAFM.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/headless.ts"],"sourcesContent":["/**\n * Headless Dev Mode\n *\n * Runs the MindStudio dev tunnel without a TUI. Designed for programmatic\n * control by a parent process (e.g., a sandbox C&C server or CI pipeline).\n *\n * Outputs structured JSON events to stdout (one per line, newline-delimited).\n * The parent process reads these to track session state, method execution,\n * errors, and connection health.\n *\n * Does NOT start a dev server — the parent process manages that separately.\n * The tunnel just needs to know which port to proxy to.\n *\n * ## JSON Event Protocol\n *\n * Every line written to stdout is a JSON object with an `event` field:\n *\n * | Event | When | Key Fields |\n * |------------------------|-----------------------------------------|-----------------------------------------------|\n * | `session-starting` | Session initializing | `appId`, `name` |\n * | `session-started` | Platform session active, proxy running | `sessionId`, `branch`, `proxyPort`, `proxyUrl`|\n * | `session-stopping` | Graceful shutdown initiated | |\n * | `session-stopped` | All resources cleaned up | |\n * | `session-expired` | Platform expired the dev session | |\n * | `method-started` | Method execution request received | `id`, `method` |\n * | `method-completed` | Method execution finished | `id`, `success`, `duration`, `error?` |\n * | `scenario-started` | Scenario execution started | `id`, `name` |\n * | `scenario-completed` | Scenario execution finished | `id`, `success`, `duration`, `roles`, `error?`|\n * | `schema-sync-started` | Table file changed, syncing schema | |\n * | `schema-sync-completed`| Schema synced to platform | `created`, `altered`, `errors` |\n * | `impersonation-changed`| Role override set or cleared | `roles` |\n * | `connection-lost` | Lost connection, retrying with backoff | `message` |\n * | `connection-restored` | Reconnected after connection loss | |\n * | `config-changed` | mindstudio.json changed, restarting | |\n * | `config-error` | Config invalid (non-fatal) | `message` |\n * | `command-error` | Stdin command failed (non-fatal) | `message` |\n * | `error` | Fatal startup error | `message` |\n *\n * ## Usage\n *\n * CLI:\n * ```bash\n * mindstudio-local --headless --port 5173 --bind 0.0.0.0\n * ```\n *\n * Programmatic:\n * ```typescript\n * import { startHeadless } from '@mindstudio-ai/local-model-tunnel';\n *\n * await startHeadless({\n * cwd: '/path/to/project',\n * devPort: 5173,\n * bindAddress: '0.0.0.0',\n * });\n * ```\n *\n * @module\n */\n\nimport { DevRunner } from './dev/runner';\nimport { DevProxy } from './dev/proxy';\nimport { devRequestEvents } from './dev/events';\nimport { syncSchema } from './dev/api';\nimport {\n detectAppConfig,\n getWebInterfaceConfig,\n readTableSources,\n} from './dev/app-config';\nimport type { AppConfig } from './dev/types';\nimport {\n getApiKey,\n getApiBaseUrl,\n getUserId,\n getEnvironment,\n getConfigPath,\n} from './config';\nimport { initLoggerHeadless, log, type LogLevel } from './dev/logger';\nimport { stablePort, detectGitBranch } from './dev/utils';\nimport { watchTableFiles } from './dev/table-watcher';\nimport { watch, type FSWatcher } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Options for headless dev mode.\n */\nexport interface HeadlessOptions {\n /** Working directory containing mindstudio.json. Defaults to process.cwd(). */\n cwd?: string;\n /** Port the dev server is running on. If omitted, reads from web.json. If neither, proxy is skipped. */\n devPort?: number;\n /** Preferred port for the local proxy. Defaults to a stable port derived from the app ID. */\n proxyPort?: number;\n /** Bind address for the proxy server. Use '0.0.0.0' for hosted sandboxes. Defaults to '127.0.0.1'. */\n bindAddress?: string;\n /** Log level for stderr output. Defaults to 'info'. */\n logLevel?: LogLevel;\n}\n\n/** Mutable state shared across the session lifecycle, stdin commands, and file watcher. */\ninterface SessionState {\n runner: DevRunner | null;\n proxy: DevProxy | null;\n appConfig: AppConfig | null;\n proxyPort: number | null;\n unsubscribers: Array<() => void>;\n}\n\n/** Write a JSON event to stdout. */\nfunction emit(event: string, data?: Record<string, unknown>): void {\n process.stdout.write(JSON.stringify({ event, ...data }) + '\\n');\n}\n\n/**\n * Start a dev session: read config, start runner, sync schema, start proxy,\n * subscribe to events. Returns true on success, false on config/startup error\n * (non-fatal — caller can retry on next config change).\n */\nasync function startSession(\n cwd: string,\n opts: HeadlessOptions,\n state: SessionState,\n shutdown: () => Promise<void>,\n): Promise<boolean> {\n const bindAddress = opts.bindAddress ?? '127.0.0.1';\n\n // Read fresh config\n const appConfig = detectAppConfig(cwd);\n if (!appConfig) {\n emit('config-error', {\n message: 'No valid mindstudio.json found in ' + cwd,\n });\n return false;\n }\n\n if (!appConfig.appId) {\n emit('config-error', { message: 'Missing \"appId\" in mindstudio.json' });\n return false;\n }\n\n state.appConfig = appConfig;\n\n // Resolve dev port\n let devPort = opts.devPort ?? null;\n if (devPort === null) {\n const webConfig = getWebInterfaceConfig(appConfig, cwd);\n devPort = webConfig?.devPort ?? null;\n }\n\n emit('session-starting', { appId: appConfig.appId, name: appConfig.name });\n\n try {\n // Start platform session\n const branch = detectGitBranch();\n const runner = new DevRunner(appConfig.appId, cwd, {\n branch,\n methods: appConfig.methods.map((m) => ({\n id: m.id,\n export: m.export,\n path: m.path,\n })),\n });\n const session = await runner.start();\n state.runner = runner;\n\n // Sync schema\n if (appConfig.tables.length > 0) {\n try {\n const tableSources = readTableSources(appConfig, cwd);\n if (tableSources.length > 0) {\n const syncResult = await syncSchema(\n appConfig.appId,\n session.sessionId,\n tableSources,\n );\n session.databases = syncResult.databases;\n emit('schema-sync-completed', {\n created: syncResult.created,\n altered: syncResult.altered,\n errors: syncResult.errors,\n });\n }\n } catch (err) {\n emit('schema-sync-completed', {\n created: [],\n altered: [],\n errors: [err instanceof Error ? err.message : 'Schema sync failed'],\n });\n }\n }\n\n // Start proxy — sits in front of the dev server, injects __MINDSTUDIO__.\n // Only started if we have a dev server port and the platform returned clientContext.\n // In headless mode we don't start the dev server (caller manages it), but we\n // do start the proxy so the preview URL works.\n let proxyPort: number | null = null;\n if (devPort !== null && session.clientContext) {\n const proxy = new DevProxy(devPort, session.clientContext, bindAddress);\n const preferred = opts.proxyPort ?? stablePort(appConfig.appId);\n proxyPort = await proxy.start(preferred);\n runner.setProxyUrl(\n `http://${bindAddress === '0.0.0.0' ? 'localhost' : bindAddress}:${proxyPort}`,\n );\n runner.setProxy(proxy);\n state.proxy = proxy;\n }\n state.proxyPort = proxyPort;\n\n emit('session-started', {\n sessionId: session.sessionId,\n releaseId: session.releaseId,\n branch: session.branch,\n proxyPort,\n proxyUrl: proxyPort\n ? `http://${bindAddress === '0.0.0.0' ? 'localhost' : bindAddress}:${proxyPort}/`\n : null,\n webInterfaceUrl: session.webInterfaceUrl,\n roles: appConfig.roles.map((r) => ({\n id: r.id,\n name: r.name ?? r.id,\n description: r.description,\n })),\n scenarios: appConfig.scenarios.map((s) => ({\n id: s.id,\n name: s.name ?? s.export,\n description: s.description,\n path: s.path,\n roles: s.roles,\n })),\n });\n\n // Subscribe to events and relay as JSON.\n // Store unsubscribe functions so we can clean up on restart.\n const unsubs = state.unsubscribers;\n\n unsubs.push(\n devRequestEvents.onStart((event) => {\n emit('method-started', { id: event.id, method: event.method });\n }),\n );\n\n unsubs.push(\n devRequestEvents.onComplete((event) => {\n emit('method-completed', {\n id: event.id,\n success: event.success,\n duration: event.duration,\n ...(event.error ? { error: event.error } : {}),\n });\n }),\n );\n\n unsubs.push(\n devRequestEvents.onConnectionWarning((message) => {\n emit('connection-lost', { message });\n }),\n );\n\n unsubs.push(\n devRequestEvents.onConnectionRestored(() => {\n emit('connection-restored');\n }),\n );\n\n unsubs.push(\n devRequestEvents.onSessionExpired(() => {\n emit('session-expired');\n shutdown().then(() => process.exit(1));\n }),\n );\n\n unsubs.push(\n devRequestEvents.onAuthRefreshStart((url) => {\n emit('auth-refresh-start', { url });\n }),\n );\n\n unsubs.push(\n devRequestEvents.onAuthRefreshSuccess(() => {\n emit('auth-refresh-success');\n }),\n );\n\n unsubs.push(\n devRequestEvents.onAuthRefreshFailed(() => {\n emit('auth-refresh-failed');\n }),\n );\n\n unsubs.push(\n devRequestEvents.onImpersonate((event) => {\n emit('impersonation-changed', { roles: event.roles });\n }),\n );\n\n unsubs.push(\n devRequestEvents.onScenarioStart((event) => {\n emit('scenario-started', { id: event.id, name: event.name });\n }),\n );\n\n unsubs.push(\n devRequestEvents.onScenarioComplete((event) => {\n emit('scenario-completed', {\n id: event.id,\n success: event.success,\n duration: event.duration,\n roles: event.roles,\n ...(event.error ? { error: event.error } : {}),\n });\n }),\n );\n\n // Watch table source files for changes — auto-sync without session restart\n setupTableWatchers(cwd, state);\n\n return true;\n } catch (err) {\n emit('config-error', {\n message: err instanceof Error ? err.message : 'Failed to start session',\n });\n return false;\n }\n}\n\n/** Set up table file watchers that auto-sync schema on change. */\nfunction setupTableWatchers(cwd: string, state: SessionState): void {\n if (!state.appConfig || state.appConfig.tables.length === 0) return;\n\n const cleanup = watchTableFiles(state.appConfig.tables, cwd, async () => {\n if (!state.runner || !state.appConfig?.appId) return;\n const session = state.runner.getSession();\n if (!session) return;\n\n emit('schema-sync-started');\n log.info('headless Table file changed, syncing schema');\n\n try {\n const tableSources = readTableSources(state.appConfig, cwd);\n if (tableSources.length > 0) {\n const result = await syncSchema(\n state.appConfig.appId,\n session.sessionId,\n tableSources,\n );\n session.databases = result.databases;\n emit('schema-sync-completed', {\n created: result.created,\n altered: result.altered,\n errors: result.errors,\n });\n log.info('headless Schema synced', {\n created: result.created,\n altered: result.altered,\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Schema sync failed';\n emit('command-error', { message });\n log.warn('headless Schema sync failed', { error: message });\n }\n });\n\n state.unsubscribers.push(cleanup);\n}\n\n/** Tear down the current session: unsubscribe events, stop proxy, stop runner. */\nasync function teardownSession(state: SessionState): Promise<void> {\n for (const unsub of state.unsubscribers) unsub();\n state.unsubscribers = [];\n\n state.proxy?.stop();\n state.proxy = null;\n state.proxyPort = null;\n\n if (state.runner) {\n await state.runner.stop().catch(() => {});\n state.runner = null;\n }\n}\n\n/**\n * Start the dev tunnel in headless mode.\n *\n * Reads mindstudio.json, starts a platform session, syncs schema,\n * starts the local proxy, and enters the poll loop. Outputs JSON\n * events to stdout. Does not return until shutdown (SIGTERM/SIGINT).\n *\n * Watches mindstudio.json for changes and automatically restarts the\n * session when the config is updated (same behavior as the TUI).\n *\n * Does NOT start a dev server — the caller is responsible for that.\n *\n * @param opts - Configuration options\n *\n * @example\n * ```typescript\n * // From a C&C server — spawn and read events\n * import { startHeadless } from '@mindstudio-ai/local-model-tunnel';\n *\n * await startHeadless({\n * cwd: '/workspace/my-app',\n * devPort: 5173,\n * bindAddress: '0.0.0.0',\n * });\n * ```\n */\nexport async function startHeadless(opts: HeadlessOptions = {}): Promise<void> {\n initLoggerHeadless(opts.logLevel ?? 'info');\n\n const cwd = opts.cwd ?? process.cwd();\n\n // Log auth config so sandbox operators can diagnose issues\n const apiKey = getApiKey();\n const userId = getUserId();\n log.info('headless Auth config', {\n configPath: getConfigPath(),\n environment: getEnvironment(),\n apiBaseUrl: getApiBaseUrl(),\n hasApiKey: !!apiKey,\n apiKeyPrefix: apiKey ? apiKey.slice(0, 8) + '...' : null,\n hasUserId: !!userId,\n userId: userId ?? null,\n cwd,\n });\n\n const state: SessionState = {\n runner: null,\n proxy: null,\n appConfig: null,\n proxyPort: null,\n unsubscribers: [],\n };\n\n // File watcher state\n let restartTimer: ReturnType<typeof setTimeout> | undefined;\n let restarting = false;\n let watcher: FSWatcher | undefined;\n\n // Graceful shutdown\n let stopping = false;\n const shutdown = async () => {\n if (stopping) return;\n stopping = true;\n emit('session-stopping');\n clearTimeout(restartTimer);\n watcher?.close();\n await teardownSession(state);\n emit('session-stopped');\n };\n\n process.on('SIGTERM', () => {\n shutdown().then(() => process.exit(0));\n });\n process.on('SIGINT', () => {\n shutdown().then(() => process.exit(0));\n });\n\n // Initial session start\n const ok = await startSession(cwd, opts, state, shutdown);\n if (!ok && !state.appConfig) {\n // No valid config at all on first try — emit fatal error.\n // The watcher below will still start if the file exists, so the\n // process stays alive to retry on config fix.\n emit('error', { message: 'No valid mindstudio.json found in ' + cwd });\n }\n\n // Stdin command loop — reads from state so it always sees current runner/config\n setupStdinCommands(state, cwd);\n\n // Watch mindstudio.json for changes — restart session on edit (500ms debounce)\n try {\n const configPath = join(cwd, 'mindstudio.json');\n watcher = watch(configPath, () => {\n clearTimeout(restartTimer);\n restartTimer = setTimeout(async () => {\n if (stopping || restarting) return;\n restarting = true;\n try {\n log.info('headless Config changed, restarting session');\n emit('config-changed');\n await teardownSession(state);\n await startSession(cwd, opts, state, shutdown);\n } finally {\n restarting = false;\n }\n }, 500);\n });\n } catch {\n // File might not exist yet or watch not supported\n }\n\n // Keep the process alive — the poll loop runs in DevRunner\n await new Promise<void>(() => {});\n}\n\n/**\n * Read NDJSON commands from stdin and dispatch them.\n * Uses the shared state object so commands always reference the current session.\n */\nfunction setupStdinCommands(state: SessionState, cwd: string): void {\n if (!process.stdin.readable) return;\n\n let buffer = '';\n process.stdin.setEncoding('utf-8');\n process.stdin.on('data', (chunk: string) => {\n buffer += chunk;\n let idx: number;\n while ((idx = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, idx).trim();\n buffer = buffer.slice(idx + 1);\n if (!line) continue;\n\n try {\n const cmd = JSON.parse(line) as {\n action: string;\n [key: string]: unknown;\n };\n handleStdinCommand(cmd, state, cwd);\n } catch {\n emit('command-error', {\n message: `Invalid JSON on stdin: ${line.slice(0, 100)}`,\n });\n }\n }\n });\n}\n\nasync function handleStdinCommand(\n cmd: { action: string; [key: string]: unknown },\n state: SessionState,\n cwd: string,\n): Promise<void> {\n switch (cmd.action) {\n case 'run-scenario': {\n if (!state.runner) {\n emit('command-error', { message: 'No active session' });\n return;\n }\n const freshConfig = detectAppConfig(cwd) ?? state.appConfig;\n const scenario = freshConfig?.scenarios.find(\n (s) => s.id === cmd.scenarioId,\n );\n if (!scenario) {\n emit('command-error', {\n message: `Unknown scenario: ${cmd.scenarioId}`,\n });\n return;\n }\n // Runner emits scenario-start/complete events which are already relayed\n await state.runner.runScenario(scenario);\n break;\n }\n\n case 'impersonate': {\n if (!state.runner) {\n emit('command-error', { message: 'No active session' });\n return;\n }\n const roles = cmd.roles as string[];\n if (!Array.isArray(roles)) {\n emit('command-error', { message: 'impersonate requires roles array' });\n return;\n }\n await state.runner.setImpersonation(roles);\n break;\n }\n\n case 'clear-impersonation': {\n if (!state.runner) {\n emit('command-error', { message: 'No active session' });\n return;\n }\n await state.runner.clearImpersonation();\n break;\n }\n\n default:\n emit('command-error', { message: `Unknown action: ${cmd.action}` });\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
function getFlag(name) {
|
|
5
|
+
const idx = process.argv.indexOf(name);
|
|
6
|
+
if (idx === -1) return void 0;
|
|
7
|
+
return process.argv[idx + 1];
|
|
8
|
+
}
|
|
4
9
|
async function main() {
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
if (process.argv.includes("--headless")) {
|
|
11
|
+
const { startHeadless } = await import("./headless.js");
|
|
12
|
+
await startHeadless({
|
|
13
|
+
cwd: process.cwd(),
|
|
14
|
+
devPort: getFlag("--port") ? Number(getFlag("--port")) : void 0,
|
|
15
|
+
proxyPort: getFlag("--proxy-port") ? Number(getFlag("--proxy-port")) : void 0,
|
|
16
|
+
bindAddress: getFlag("--bind"),
|
|
17
|
+
logLevel: getFlag("--log-level") ?? void 0
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
const { startTUI } = await import("./tui-4PJCFILV.js");
|
|
21
|
+
await startTUI();
|
|
22
|
+
}
|
|
7
23
|
process.exit(0);
|
|
8
24
|
}
|
|
9
25
|
main();
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nasync function main() {\n const { startTUI } = await import('./tui/index.js');\n
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nfunction getFlag(name: string): string | undefined {\n const idx = process.argv.indexOf(name);\n if (idx === -1) return undefined;\n return process.argv[idx + 1];\n}\n\nasync function main() {\n if (process.argv.includes('--headless')) {\n const { startHeadless } = await import('./headless.js');\n await startHeadless({\n cwd: process.cwd(),\n devPort: getFlag('--port') ? Number(getFlag('--port')) : undefined,\n proxyPort: getFlag('--proxy-port') ? Number(getFlag('--proxy-port')) : undefined,\n bindAddress: getFlag('--bind'),\n logLevel: (getFlag('--log-level') as 'error' | 'warn' | 'info' | 'debug') ?? undefined,\n });\n } else {\n const { startTUI } = await import('./tui/index.js');\n await startTUI();\n }\n process.exit(0);\n}\n\nmain();\n"],"mappings":";;;AAEA,SAAS,QAAQ,MAAkC;AACjD,QAAM,MAAM,QAAQ,KAAK,QAAQ,IAAI;AACrC,MAAI,QAAQ,GAAI,QAAO;AACvB,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;AAEA,eAAe,OAAO;AACpB,MAAI,QAAQ,KAAK,SAAS,YAAY,GAAG;AACvC,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,eAAe;AACtD,UAAM,cAAc;AAAA,MAClB,KAAK,QAAQ,IAAI;AAAA,MACjB,SAAS,QAAQ,QAAQ,IAAI,OAAO,QAAQ,QAAQ,CAAC,IAAI;AAAA,MACzD,WAAW,QAAQ,cAAc,IAAI,OAAO,QAAQ,cAAc,CAAC,IAAI;AAAA,MACvE,aAAa,QAAQ,QAAQ;AAAA,MAC7B,UAAW,QAAQ,aAAa,KAA6C;AAAA,IAC/E,CAAC;AAAA,EACH,OAAO;AACL,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,mBAAgB;AAClD,UAAM,SAAS;AAAA,EACjB;AACA,UAAQ,KAAK,CAAC;AAChB;AAEA,KAAK;","names":[]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logger with configurable level and output target.
|
|
3
|
+
*
|
|
4
|
+
* - Headless mode: writes to stderr (stdout reserved for JSON events)
|
|
5
|
+
* - Interactive mode: writes to .mindstudio-dev.log in cwd (won't interfere with Ink TUI)
|
|
6
|
+
*
|
|
7
|
+
* Levels: error > warn > info > debug
|
|
8
|
+
*/
|
|
9
|
+
type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Headless Dev Mode
|
|
13
|
+
*
|
|
14
|
+
* Runs the MindStudio dev tunnel without a TUI. Designed for programmatic
|
|
15
|
+
* control by a parent process (e.g., a sandbox C&C server or CI pipeline).
|
|
16
|
+
*
|
|
17
|
+
* Outputs structured JSON events to stdout (one per line, newline-delimited).
|
|
18
|
+
* The parent process reads these to track session state, method execution,
|
|
19
|
+
* errors, and connection health.
|
|
20
|
+
*
|
|
21
|
+
* Does NOT start a dev server — the parent process manages that separately.
|
|
22
|
+
* The tunnel just needs to know which port to proxy to.
|
|
23
|
+
*
|
|
24
|
+
* ## JSON Event Protocol
|
|
25
|
+
*
|
|
26
|
+
* Every line written to stdout is a JSON object with an `event` field:
|
|
27
|
+
*
|
|
28
|
+
* | Event | When | Key Fields |
|
|
29
|
+
* |------------------------|-----------------------------------------|-----------------------------------------------|
|
|
30
|
+
* | `session-starting` | Session initializing | `appId`, `name` |
|
|
31
|
+
* | `session-started` | Platform session active, proxy running | `sessionId`, `branch`, `proxyPort`, `proxyUrl`|
|
|
32
|
+
* | `session-stopping` | Graceful shutdown initiated | |
|
|
33
|
+
* | `session-stopped` | All resources cleaned up | |
|
|
34
|
+
* | `session-expired` | Platform expired the dev session | |
|
|
35
|
+
* | `method-started` | Method execution request received | `id`, `method` |
|
|
36
|
+
* | `method-completed` | Method execution finished | `id`, `success`, `duration`, `error?` |
|
|
37
|
+
* | `scenario-started` | Scenario execution started | `id`, `name` |
|
|
38
|
+
* | `scenario-completed` | Scenario execution finished | `id`, `success`, `duration`, `roles`, `error?`|
|
|
39
|
+
* | `schema-sync-started` | Table file changed, syncing schema | |
|
|
40
|
+
* | `schema-sync-completed`| Schema synced to platform | `created`, `altered`, `errors` |
|
|
41
|
+
* | `impersonation-changed`| Role override set or cleared | `roles` |
|
|
42
|
+
* | `connection-lost` | Lost connection, retrying with backoff | `message` |
|
|
43
|
+
* | `connection-restored` | Reconnected after connection loss | |
|
|
44
|
+
* | `config-changed` | mindstudio.json changed, restarting | |
|
|
45
|
+
* | `config-error` | Config invalid (non-fatal) | `message` |
|
|
46
|
+
* | `command-error` | Stdin command failed (non-fatal) | `message` |
|
|
47
|
+
* | `error` | Fatal startup error | `message` |
|
|
48
|
+
*
|
|
49
|
+
* ## Usage
|
|
50
|
+
*
|
|
51
|
+
* CLI:
|
|
52
|
+
* ```bash
|
|
53
|
+
* mindstudio-local --headless --port 5173 --bind 0.0.0.0
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* Programmatic:
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { startHeadless } from '@mindstudio-ai/local-model-tunnel';
|
|
59
|
+
*
|
|
60
|
+
* await startHeadless({
|
|
61
|
+
* cwd: '/path/to/project',
|
|
62
|
+
* devPort: 5173,
|
|
63
|
+
* bindAddress: '0.0.0.0',
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @module
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Options for headless dev mode.
|
|
72
|
+
*/
|
|
73
|
+
interface HeadlessOptions {
|
|
74
|
+
/** Working directory containing mindstudio.json. Defaults to process.cwd(). */
|
|
75
|
+
cwd?: string;
|
|
76
|
+
/** Port the dev server is running on. If omitted, reads from web.json. If neither, proxy is skipped. */
|
|
77
|
+
devPort?: number;
|
|
78
|
+
/** Preferred port for the local proxy. Defaults to a stable port derived from the app ID. */
|
|
79
|
+
proxyPort?: number;
|
|
80
|
+
/** Bind address for the proxy server. Use '0.0.0.0' for hosted sandboxes. Defaults to '127.0.0.1'. */
|
|
81
|
+
bindAddress?: string;
|
|
82
|
+
/** Log level for stderr output. Defaults to 'info'. */
|
|
83
|
+
logLevel?: LogLevel;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Start the dev tunnel in headless mode.
|
|
87
|
+
*
|
|
88
|
+
* Reads mindstudio.json, starts a platform session, syncs schema,
|
|
89
|
+
* starts the local proxy, and enters the poll loop. Outputs JSON
|
|
90
|
+
* events to stdout. Does not return until shutdown (SIGTERM/SIGINT).
|
|
91
|
+
*
|
|
92
|
+
* Watches mindstudio.json for changes and automatically restarts the
|
|
93
|
+
* session when the config is updated (same behavior as the TUI).
|
|
94
|
+
*
|
|
95
|
+
* Does NOT start a dev server — the caller is responsible for that.
|
|
96
|
+
*
|
|
97
|
+
* @param opts - Configuration options
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* // From a C&C server — spawn and read events
|
|
102
|
+
* import { startHeadless } from '@mindstudio-ai/local-model-tunnel';
|
|
103
|
+
*
|
|
104
|
+
* await startHeadless({
|
|
105
|
+
* cwd: '/workspace/my-app',
|
|
106
|
+
* devPort: 5173,
|
|
107
|
+
* bindAddress: '0.0.0.0',
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare function startHeadless(opts?: HeadlessOptions): Promise<void>;
|
|
112
|
+
|
|
113
|
+
export { type HeadlessOptions, startHeadless };
|
package/dist/headless.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
startHeadless
|
|
3
|
+
} from "./chunk-WFQXIMTS.js";
|
|
1
4
|
import {
|
|
2
5
|
TunnelRunner
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QALGC7T7.js";
|
|
7
|
+
import "./chunk-C3JPRLSS.js";
|
|
4
8
|
export {
|
|
5
|
-
TunnelRunner
|
|
9
|
+
TunnelRunner,
|
|
10
|
+
startHeadless
|
|
6
11
|
};
|
|
7
12
|
//# sourceMappingURL=index.js.map
|