@fluojs/cli
Advanced tools
| export declare const STUDIO_DEVTOOLS_GLOBAL_CONFIG_KEY = "__FLUO_STUDIO_DEVTOOLS_CONFIG__"; | ||
| export interface StudioDevtoolsInjectedConfig { | ||
| FLUO_STUDIO: '1'; | ||
| FLUO_STUDIO_APP_ID?: string; | ||
| FLUO_STUDIO_ENDPOINT?: string; | ||
| FLUO_STUDIO_EPOCH?: string; | ||
| FLUO_STUDIO_RUNTIME?: string; | ||
| FLUO_STUDIO_TOKEN: string; | ||
| FLUO_STUDIO_URL?: string; | ||
| } | ||
| /** | ||
| * Builds the explicit Studio config that CLI-owned dev boundaries inject into app children. | ||
| * | ||
| * Package runtime code must not read ambient environment state directly. The CLI sidecar still stores the | ||
| * generated app id/token/URL in child environment for process supervision, then this helper converts | ||
| * those values into a typed process-local config object before the app imports `@fluojs/runtime`. | ||
| */ | ||
| export declare function resolveStudioDevtoolsInjectedConfig(env: NodeJS.ProcessEnv): StudioDevtoolsInjectedConfig | undefined; | ||
| export declare function createStudioDevtoolsNodeImport(env: NodeJS.ProcessEnv): string[]; | ||
| //# sourceMappingURL=runtime-config.d.ts.map |
| {"version":3,"file":"runtime-config.d.ts","sourceRoot":"","sources":["../../src/studio/runtime-config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iCAAiC,oCAAoC,CAAC;AAEnF,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,GAAG,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAsBD;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,4BAA4B,GAAG,SAAS,CAmBpH;AAED,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,EAAE,CAS/E"} |
| export const STUDIO_DEVTOOLS_GLOBAL_CONFIG_KEY = '__FLUO_STUDIO_DEVTOOLS_CONFIG__'; | ||
| function isEnabledEnvironmentFlag(value) { | ||
| return value === '1' || value === 'true' || value === 'yes' || value === 'on'; | ||
| } | ||
| function resolveStudioIngestEndpoint(env) { | ||
| if (env.FLUO_STUDIO_ENDPOINT) { | ||
| return env.FLUO_STUDIO_ENDPOINT; | ||
| } | ||
| if (!env.FLUO_STUDIO_URL) { | ||
| return undefined; | ||
| } | ||
| try { | ||
| return new URL('/api/runtime/events', env.FLUO_STUDIO_URL).toString(); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
| /** | ||
| * Builds the explicit Studio config that CLI-owned dev boundaries inject into app children. | ||
| * | ||
| * Package runtime code must not read ambient environment state directly. The CLI sidecar still stores the | ||
| * generated app id/token/URL in child environment for process supervision, then this helper converts | ||
| * those values into a typed process-local config object before the app imports `@fluojs/runtime`. | ||
| */ | ||
| export function resolveStudioDevtoolsInjectedConfig(env) { | ||
| if (!isEnabledEnvironmentFlag(env.FLUO_STUDIO) || !env.FLUO_STUDIO_TOKEN) { | ||
| return undefined; | ||
| } | ||
| const endpoint = resolveStudioIngestEndpoint(env); | ||
| if (!endpoint) { | ||
| return undefined; | ||
| } | ||
| return { | ||
| FLUO_STUDIO: '1', | ||
| FLUO_STUDIO_APP_ID: env.FLUO_STUDIO_APP_ID, | ||
| FLUO_STUDIO_ENDPOINT: endpoint, | ||
| FLUO_STUDIO_EPOCH: env.FLUO_STUDIO_EPOCH, | ||
| FLUO_STUDIO_RUNTIME: env.FLUO_STUDIO_RUNTIME, | ||
| FLUO_STUDIO_TOKEN: env.FLUO_STUDIO_TOKEN, | ||
| FLUO_STUDIO_URL: env.FLUO_STUDIO_URL | ||
| }; | ||
| } | ||
| export function createStudioDevtoolsNodeImport(env) { | ||
| const config = resolveStudioDevtoolsInjectedConfig(env); | ||
| if (!config) { | ||
| return []; | ||
| } | ||
| const source = `Object.defineProperty(globalThis, ${JSON.stringify(STUDIO_DEVTOOLS_GLOBAL_CONFIG_KEY)}, { configurable: true, enumerable: false, writable: true, value: ${JSON.stringify(config)} });`; | ||
| return ['--import', `data:text/javascript,${encodeURIComponent(source)}`]; | ||
| } |
| /** | ||
| * Defines Studio Sidecar Runtime values used by the Studio devtool. | ||
| */ | ||
| export type StudioSidecarRuntime = 'bun' | 'deno' | 'node' | 'unknown'; | ||
| /** | ||
| * Describes Studio Sidecar Options data used by the Studio devtool. | ||
| */ | ||
| export interface StudioSidecarOptions { | ||
| appId?: string; | ||
| heartbeatMs?: number; | ||
| host?: string; | ||
| port?: number; | ||
| runtime?: StudioSidecarRuntime; | ||
| } | ||
| /** | ||
| * Describes Studio Sidecar data used by the Studio devtool. | ||
| */ | ||
| export interface StudioSidecar { | ||
| readonly appId: string; | ||
| readonly epoch: string; | ||
| readonly env: NodeJS.ProcessEnv; | ||
| readonly host: string; | ||
| readonly port: number; | ||
| readonly token: string; | ||
| readonly url: string; | ||
| close(): Promise<void>; | ||
| } | ||
| /** | ||
| * Provides start Studio Sidecar behavior for the Studio devtool. | ||
| * | ||
| * @param options options value used by start Studio Sidecar. | ||
| * @returns The start Studio Sidecar result. | ||
| */ | ||
| export declare function startStudioSidecar(options?: StudioSidecarOptions): Promise<StudioSidecar>; | ||
| //# sourceMappingURL=sidecar.d.ts.map |
| {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../../src/studio/sidecar.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAkPD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,aAAa,CAAC,CAiLnG"} |
| import { randomBytes, randomUUID } from 'node:crypto'; | ||
| import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs'; | ||
| import { createServer } from 'node:http'; | ||
| import { createRequire } from 'node:module'; | ||
| import { dirname, extname, join, normalize, relative, sep } from 'node:path'; | ||
| import { URL } from 'node:url'; | ||
| /** | ||
| * Defines Studio Sidecar Runtime values used by the Studio devtool. | ||
| */ | ||
| /** | ||
| * Describes Studio Sidecar Options data used by the Studio devtool. | ||
| */ | ||
| /** | ||
| * Describes Studio Sidecar data used by the Studio devtool. | ||
| */ | ||
| const DEFAULT_HOST = '127.0.0.1'; | ||
| const DEFAULT_HEARTBEAT_MS = 15_000; | ||
| const MAX_EVENT_REPLAY = 1_000; | ||
| const MAX_REQUEST_BYTES = 1_048_576; | ||
| const require = createRequire(import.meta.url); | ||
| function isRecord(value) { | ||
| return typeof value === 'object' && value !== null; | ||
| } | ||
| function isRestartEpochBoundary(incoming) { | ||
| if (incoming.type !== 'restart' || !isRecord(incoming.payload)) { | ||
| return false; | ||
| } | ||
| return incoming.payload.phase === 'scheduled' || incoming.payload.phase === 'starting'; | ||
| } | ||
| function createToken() { | ||
| return randomBytes(24).toString('base64url'); | ||
| } | ||
| function createEpoch() { | ||
| return randomUUID(); | ||
| } | ||
| function createDefaultAppId() { | ||
| return `fluo-app-${process.pid}`; | ||
| } | ||
| function readBody(request) { | ||
| return new Promise((resolve, reject) => { | ||
| let body = ''; | ||
| request.setEncoding('utf8'); | ||
| request.on('data', chunk => { | ||
| body += chunk; | ||
| if (body.length > MAX_REQUEST_BYTES) { | ||
| reject(new Error('Studio event payload is too large.')); | ||
| request.destroy(); | ||
| } | ||
| }); | ||
| request.on('end', () => resolve(body)); | ||
| request.on('error', reject); | ||
| }); | ||
| } | ||
| function writeJson(response, statusCode, payload) { | ||
| response.writeHead(statusCode, { | ||
| 'cache-control': 'no-store', | ||
| 'content-type': 'application/json; charset=utf-8' | ||
| }); | ||
| response.end(JSON.stringify(payload)); | ||
| } | ||
| function writeText(response, statusCode, body, contentType = 'text/plain; charset=utf-8') { | ||
| response.writeHead(statusCode, { | ||
| 'cache-control': 'no-store', | ||
| 'content-type': contentType | ||
| }); | ||
| response.end(body); | ||
| } | ||
| function contentTypeForPath(pathname) { | ||
| switch (extname(pathname)) { | ||
| case '.css': | ||
| return 'text/css; charset=utf-8'; | ||
| case '.html': | ||
| return 'text/html; charset=utf-8'; | ||
| case '.js': | ||
| return 'text/javascript; charset=utf-8'; | ||
| case '.json': | ||
| return 'application/json; charset=utf-8'; | ||
| case '.map': | ||
| return 'application/json; charset=utf-8'; | ||
| case '.svg': | ||
| return 'image/svg+xml'; | ||
| case '.woff2': | ||
| return 'font/woff2'; | ||
| default: | ||
| return 'application/octet-stream'; | ||
| } | ||
| } | ||
| function resolveStudioViewerPath() { | ||
| try { | ||
| const viewerPath = require.resolve('@fluojs/studio/viewer'); | ||
| return existsSync(viewerPath) ? viewerPath : undefined; | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
| function injectStudioConfig(html, options) { | ||
| const configScript = `<script>window.__FLUO_STUDIO__ = ${JSON.stringify(options).replaceAll('<', '\\u003c')};</script>`; | ||
| if (html.includes('</head>')) { | ||
| return html.replace('</head>', ` ${configScript}\n</head>`); | ||
| } | ||
| return `${configScript}\n${html}`; | ||
| } | ||
| function safeAssetPath(rootDirectory, pathname) { | ||
| let decodedPath; | ||
| try { | ||
| decodedPath = decodeURIComponent(pathname); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| const normalized = normalize(decodedPath).replace(/^[/\\]+/, ''); | ||
| const candidate = join(rootDirectory, normalized); | ||
| const relativePath = relative(rootDirectory, candidate); | ||
| if (relativePath.startsWith('..') || relativePath.split(sep).includes('..')) { | ||
| return undefined; | ||
| } | ||
| return candidate; | ||
| } | ||
| function serveStudioAsset(response, rootDirectory, pathname) { | ||
| const assetPath = safeAssetPath(rootDirectory, pathname); | ||
| if (!assetPath || !existsSync(assetPath)) { | ||
| return false; | ||
| } | ||
| const stats = statSync(assetPath); | ||
| if (!stats.isFile()) { | ||
| return false; | ||
| } | ||
| response.writeHead(200, { | ||
| 'cache-control': 'no-store', | ||
| 'content-length': String(stats.size), | ||
| 'content-type': contentTypeForPath(assetPath) | ||
| }); | ||
| createReadStream(assetPath).pipe(response); | ||
| return true; | ||
| } | ||
| function extractBearerToken(request) { | ||
| const authorization = request.headers.authorization; | ||
| if (typeof authorization !== 'string') { | ||
| return undefined; | ||
| } | ||
| const [scheme, token] = authorization.split(' '); | ||
| return scheme.toLowerCase() === 'bearer' && token ? token : undefined; | ||
| } | ||
| function requestToken(request, url) { | ||
| return extractBearerToken(request) ?? url.searchParams.get('token') ?? undefined; | ||
| } | ||
| function isAuthorized(request, url, token) { | ||
| return requestToken(request, url) === token; | ||
| } | ||
| function parseAfterSequence(url, request, epoch) { | ||
| const after = url.searchParams.get('after') ?? request.headers['last-event-id']; | ||
| const value = Array.isArray(after) ? after[0] : after; | ||
| if (!value) { | ||
| return 0; | ||
| } | ||
| if (/^\d+$/.test(value)) { | ||
| return Number(value); | ||
| } | ||
| const [eventEpoch, sequence] = value.split(':'); | ||
| if (eventEpoch === epoch && /^\d+$/.test(sequence)) { | ||
| return Number(sequence); | ||
| } | ||
| return 0; | ||
| } | ||
| function writeSseEvent(response, event) { | ||
| response.write(`id: ${event.eventId}\n`); | ||
| response.write(`event: ${event.type}\n`); | ||
| response.write(`data: ${JSON.stringify(event)}\n\n`); | ||
| } | ||
| function renderStudioShell(options) { | ||
| const viewerPath = resolveStudioViewerPath(); | ||
| if (viewerPath) { | ||
| return injectStudioConfig(readFileSync(viewerPath, 'utf8'), options); | ||
| } | ||
| const config = JSON.stringify(options).replaceAll('<', '\\u003c'); | ||
| return `<!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>Fluo Studio</title> | ||
| <style> | ||
| :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } | ||
| body { margin: 0; background: #0a0f1d; color: #dbeafe; } | ||
| main { min-height: 100vh; display: grid; place-items: center; padding: 32px; } | ||
| section { max-width: 760px; border: 1px solid rgba(148, 163, 184, 0.22); border-radius: 24px; padding: 32px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.96), rgba(30, 41, 59, 0.82)); box-shadow: 0 24px 80px rgba(0, 0, 0, 0.28); } | ||
| h1 { margin: 0 0 12px; font-size: 34px; letter-spacing: -0.04em; } | ||
| p { color: #94a3b8; line-height: 1.7; } | ||
| code { color: #93c5fd; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <main> | ||
| <section> | ||
| <h1>Fluo Studio sidecar is live</h1> | ||
| <p>This token-protected local sidecar is receiving runtime events. The React Studio UI will attach to <code>/api/events</code> in the next package layer.</p> | ||
| <p>Config: <code id="config"></code></p> | ||
| </section> | ||
| </main> | ||
| <script>window.__FLUO_STUDIO__ = ${config}; document.getElementById('config').textContent = JSON.stringify(window.__FLUO_STUDIO__);</script> | ||
| </body> | ||
| </html>`; | ||
| } | ||
| /** | ||
| * Provides start Studio Sidecar behavior for the Studio devtool. | ||
| * | ||
| * @param options options value used by start Studio Sidecar. | ||
| * @returns The start Studio Sidecar result. | ||
| */ | ||
| export async function startStudioSidecar(options = {}) { | ||
| const host = options.host ?? DEFAULT_HOST; | ||
| const appId = options.appId ?? createDefaultAppId(); | ||
| const runtime = options.runtime ?? 'node'; | ||
| let epoch = createEpoch(); | ||
| const token = createToken(); | ||
| const events = []; | ||
| const clients = new Set(); | ||
| let sequence = 0; | ||
| const startedAt = performance.now(); | ||
| const publish = incoming => { | ||
| if (isRestartEpochBoundary(incoming)) { | ||
| epoch = createEpoch(); | ||
| } | ||
| sequence += 1; | ||
| const source = isRecord(incoming.source) ? incoming.source : undefined; | ||
| const sourceAppId = typeof source?.appId === 'string' && source.appId.length > 0 ? source.appId : appId; | ||
| const sourceRuntime = source?.runtime === 'bun' || source?.runtime === 'deno' || source?.runtime === 'node' ? source.runtime : runtime; | ||
| const event = { | ||
| emittedAt: new Date().toISOString(), | ||
| epoch, | ||
| eventId: `${epoch}:${String(sequence)}`, | ||
| payload: incoming.payload ?? {}, | ||
| sequence, | ||
| source: { | ||
| appId: sourceAppId, | ||
| runtime: sourceRuntime | ||
| }, | ||
| type: typeof incoming.type === 'string' && incoming.type.length > 0 ? incoming.type : 'diagnostic', | ||
| version: 1 | ||
| }; | ||
| events.push(event); | ||
| if (events.length > MAX_EVENT_REPLAY) { | ||
| events.splice(0, events.length - MAX_EVENT_REPLAY); | ||
| } | ||
| for (const client of clients) { | ||
| writeSseEvent(client.response, event); | ||
| } | ||
| return event; | ||
| }; | ||
| const server = createServer(async (request, response) => { | ||
| const requestUrl = new URL(request.url ?? '/', `http://${host}`); | ||
| const viewerPath = resolveStudioViewerPath(); | ||
| if (request.method === 'GET' && requestUrl.pathname.startsWith('/assets/') && viewerPath) { | ||
| if (serveStudioAsset(response, dirname(viewerPath), requestUrl.pathname)) { | ||
| return; | ||
| } | ||
| } | ||
| if (!isAuthorized(request, requestUrl, token)) { | ||
| writeJson(response, 401, { | ||
| error: 'Unauthorized Studio sidecar request.' | ||
| }); | ||
| return; | ||
| } | ||
| if (request.method === 'GET' && requestUrl.pathname === '/') { | ||
| const tokenQuery = encodeURIComponent(token); | ||
| writeText(response, 200, renderStudioShell({ | ||
| eventsUrl: `/api/events?token=${tokenQuery}`, | ||
| stateUrl: `/api/state?token=${tokenQuery}` | ||
| }), 'text/html; charset=utf-8'); | ||
| return; | ||
| } | ||
| if (request.method === 'GET' && requestUrl.pathname === '/api/state') { | ||
| writeJson(response, 200, { | ||
| appId, | ||
| clientCount: clients.size, | ||
| epoch, | ||
| events, | ||
| sequence | ||
| }); | ||
| return; | ||
| } | ||
| if (request.method === 'GET' && requestUrl.pathname === '/api/events') { | ||
| response.writeHead(200, { | ||
| 'cache-control': 'no-cache, no-transform', | ||
| connection: 'keep-alive', | ||
| 'content-type': 'text/event-stream; charset=utf-8', | ||
| 'x-accel-buffering': 'no' | ||
| }); | ||
| response.write(': fluo studio stream ready\n\n'); | ||
| const afterSequence = requestUrl.searchParams.get('replay') === '0' ? sequence : parseAfterSequence(requestUrl, request, epoch); | ||
| for (const event of events) { | ||
| if (event.sequence > afterSequence) { | ||
| writeSseEvent(response, event); | ||
| } | ||
| } | ||
| const client = { | ||
| response | ||
| }; | ||
| clients.add(client); | ||
| request.on('close', () => { | ||
| clients.delete(client); | ||
| }); | ||
| return; | ||
| } | ||
| if (request.method === 'POST' && requestUrl.pathname === '/api/runtime/events') { | ||
| try { | ||
| const body = await readBody(request); | ||
| const parsed = body ? JSON.parse(body) : {}; | ||
| if (!isRecord(parsed)) { | ||
| writeJson(response, 400, { | ||
| error: 'Studio runtime event must be a JSON object.' | ||
| }); | ||
| return; | ||
| } | ||
| const event = publish(parsed); | ||
| writeJson(response, 202, { | ||
| accepted: true, | ||
| epoch: event.epoch, | ||
| sequence: event.sequence | ||
| }); | ||
| } catch (error) { | ||
| writeJson(response, 400, { | ||
| error: error instanceof Error ? error.message : String(error) | ||
| }); | ||
| } | ||
| return; | ||
| } | ||
| writeJson(response, 404, { | ||
| error: 'Unknown Studio sidecar route.' | ||
| }); | ||
| }); | ||
| const heartbeat = options.heartbeatMs === 0 ? undefined : setInterval(() => { | ||
| publish({ | ||
| payload: { | ||
| uptimeMs: Number((performance.now() - startedAt).toFixed(3)) | ||
| }, | ||
| source: { | ||
| appId, | ||
| runtime | ||
| }, | ||
| type: 'heartbeat' | ||
| }); | ||
| }, options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS); | ||
| heartbeat?.unref(); | ||
| await new Promise((resolve, reject) => { | ||
| server.once('error', reject); | ||
| server.listen(options.port ?? 0, host, () => { | ||
| server.off('error', reject); | ||
| resolve(); | ||
| }); | ||
| }); | ||
| const address = server.address(); | ||
| if (!address || typeof address === 'string') { | ||
| await closeServer(server, clients, heartbeat); | ||
| throw new Error('Failed to resolve Studio sidecar address.'); | ||
| } | ||
| const url = `http://${host}:${String(address.port)}`; | ||
| return { | ||
| appId, | ||
| get epoch() { | ||
| return epoch; | ||
| }, | ||
| env: { | ||
| FLUO_STUDIO: '1', | ||
| FLUO_STUDIO_APP_ID: appId, | ||
| FLUO_STUDIO_EPOCH: epoch, | ||
| FLUO_STUDIO_RUNTIME: runtime, | ||
| FLUO_STUDIO_TOKEN: token, | ||
| FLUO_STUDIO_URL: url | ||
| }, | ||
| host, | ||
| port: address.port, | ||
| token, | ||
| url, | ||
| async close() { | ||
| await closeServer(server, clients, heartbeat); | ||
| } | ||
| }; | ||
| } | ||
| async function closeServer(server, clients, heartbeat) { | ||
| if (heartbeat) { | ||
| clearInterval(heartbeat); | ||
| } | ||
| for (const client of clients) { | ||
| client.response.end(); | ||
| } | ||
| clients.clear(); | ||
| await new Promise((resolve, reject) => { | ||
| server.close(error => { | ||
| if (error) { | ||
| reject(error); | ||
| return; | ||
| } | ||
| resolve(); | ||
| }); | ||
| }); | ||
| } |
+2
-0
| import { type InspectCommandRuntimeOptions } from './commands/inspect.js'; | ||
| import { type NewCommandRuntimeOptions } from './commands/new.js'; | ||
| import type { startStudioSidecar } from './studio/sidecar.js'; | ||
| import { type CliUpdateCheckRuntimeOptions } from './update-check.js'; | ||
@@ -26,2 +27,3 @@ type CliStream = { | ||
| }) => Promise<number>; | ||
| startStudioSidecar?: typeof startStudioSidecar; | ||
| stderr?: CliStream; | ||
@@ -28,0 +30,0 @@ stdin?: CliReadableStream; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,4BAA4B,EAAmC,MAAM,uBAAuB,CAAC;AAE3G,OAAO,EAAE,KAAK,wBAAwB,EAA2B,MAAM,mBAAmB,CAAC;AAO3F,OAAO,EAAE,KAAK,4BAA4B,EAA6C,MAAM,mBAAmB,CAAC;AAEjH,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;IACrF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzL,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,WAAW,CAAC,EAAE,KAAK,GAAG,4BAA4B,CAAC;CACpD;AA4ZD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,iBAAiB,GAAG,wBAAwB,GAAG,4BAAiC,GACxF,OAAO,CAAC,MAAM,CAAC,CAuNjB"} | ||
| {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,4BAA4B,EAAmC,MAAM,uBAAuB,CAAC;AAE3G,OAAO,EAAE,KAAK,wBAAwB,EAA2B,MAAM,mBAAmB,CAAC;AAI3F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAI9D,OAAO,EAAE,KAAK,4BAA4B,EAA6C,MAAM,mBAAmB,CAAC;AAEjH,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;IACrF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzL,kBAAkB,CAAC,EAAE,OAAO,kBAAkB,CAAC;IAC/C,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,WAAW,CAAC,EAAE,KAAK,GAAG,4BAA4B,CAAC;CACpD;AA4ZD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,iBAAiB,GAAG,wBAAwB,GAAG,4BAAiC,GACxF,OAAO,CAAC,MAAM,CAAC,CAuNjB"} |
@@ -0,1 +1,2 @@ | ||
| import { startStudioSidecar } from '../studio/sidecar.js'; | ||
| type CliStream = { | ||
@@ -17,2 +18,3 @@ isTTY?: boolean; | ||
| spawnCommand?: (command: string, args: string[], options: SpawnCommandOptions) => Promise<number>; | ||
| startStudioSidecar?: typeof startStudioSidecar; | ||
| stderr?: CliStream; | ||
@@ -19,0 +21,0 @@ stdout?: CliStream; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"scripts.d.ts","sourceRoot":"","sources":["../../src/commands/scripts.ts"],"names":[],"mappings":"AAMA,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAMF,KAAK,mBAAmB,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClG,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAGF,KAAK,aAAa,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AA4c/C;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAmB1D;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyElI"} | ||
| {"version":3,"file":"scripts.d.ts","sourceRoot":"","sources":["../../src/commands/scripts.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,kBAAkB,EAAiD,MAAM,sBAAsB,CAAC;AAGzG,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAMF,KAAK,mBAAmB,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClG,kBAAkB,CAAC,EAAE,OAAO,kBAAkB,CAAC;IAC/C,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAGF,KAAK,aAAa,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AAulB/C;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAqB1D;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsFlI"} |
+150
-34
@@ -5,2 +5,4 @@ import { spawn } from 'node:child_process'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { createStudioDevtoolsNodeImport } from '../studio/runtime-config.js'; | ||
| import { startStudioSidecar } from '../studio/sidecar.js'; | ||
| import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js'; | ||
@@ -245,15 +247,14 @@ const EMPTY_ENV = {}; | ||
| } | ||
| function withPipedAppColorTtyBootstrap(steps, env) { | ||
| if (env[PRETTY_TTY_COLOR_ENV] !== '1') { | ||
| return steps; | ||
| } | ||
| function withPipedAppBootstrapImports(steps, env) { | ||
| return steps.map(step => { | ||
| const preserveColorTtyImport = getPreserveColorTtyImport(); | ||
| if (step.command === 'node' && step.mode !== 'fluo-restart') { | ||
| const colorImport = env[PRETTY_TTY_COLOR_ENV] === '1' ? ['--import', preserveColorTtyImport] : []; | ||
| const studioDevtoolsImport = step.mode === 'fluo-restart' ? [] : createStudioDevtoolsNodeImport(env); | ||
| if (step.command === 'node') { | ||
| return { | ||
| ...step, | ||
| args: ['--import', preserveColorTtyImport, ...step.args] | ||
| args: [...colorImport, ...studioDevtoolsImport, ...step.args] | ||
| }; | ||
| } | ||
| if (step.command === 'bun' && (step.mode === 'runtime-native-watch' || step.args[0] === 'dist/main.js')) { | ||
| if (env[PRETTY_TTY_COLOR_ENV] === '1' && step.command === 'bun' && (step.mode === 'runtime-native-watch' || step.args[0] === 'dist/main.js')) { | ||
| return { | ||
@@ -273,2 +274,4 @@ ...step, | ||
| let reporter = 'auto'; | ||
| let studio = false; | ||
| let studioPort; | ||
| let verbose = false; | ||
@@ -314,2 +317,20 @@ const passThrough = []; | ||
| } | ||
| if (arg === '--studio') { | ||
| studio = true; | ||
| continue; | ||
| } | ||
| if (arg === '--studio-port') { | ||
| const value = argv[index + 1]; | ||
| if (!value || value.startsWith('-')) { | ||
| throw new Error('Expected --studio-port to have a value.'); | ||
| } | ||
| const parsedPort = Number(value); | ||
| if (!Number.isInteger(parsedPort) || parsedPort < 0 || parsedPort > 65_535) { | ||
| throw new Error('Invalid --studio-port value. Use a TCP port between 0 and 65535.'); | ||
| } | ||
| studio = true; | ||
| studioPort = parsedPort; | ||
| index += 1; | ||
| continue; | ||
| } | ||
| if (arg === '--package-manager') { | ||
@@ -340,2 +361,4 @@ const value = argv[index + 1]; | ||
| reporter, | ||
| studio, | ||
| studioPort, | ||
| verbose | ||
@@ -366,2 +389,33 @@ }; | ||
| } | ||
| function projectRuntimeToStudioRuntime(runtime) { | ||
| if (runtime === 'bun' || runtime === 'deno' || runtime === 'node') { | ||
| return runtime; | ||
| } | ||
| return 'unknown'; | ||
| } | ||
| function projectDisplayName(project) { | ||
| return typeof project.manifest.name === 'string' && project.manifest.name.length > 0 ? project.manifest.name : project.directory.split(/[\\/]/).filter(Boolean).at(-1) ?? 'fluo-app'; | ||
| } | ||
| function assertStudioSupport(command, studio, projectRuntime, _devRunner) { | ||
| if (!studio) { | ||
| return; | ||
| } | ||
| if (command !== 'dev') { | ||
| throw new Error('--studio is only supported for fluo dev.'); | ||
| } | ||
| if (projectRuntime !== 'node') { | ||
| throw new Error(`fluo dev --studio currently supports Node dev runner projects only. ${projectRuntime} Studio support remains experimental until a dedicated bridge is implemented and verified.`); | ||
| } | ||
| } | ||
| function withStudioDryRunEnv(env, project, projectRuntime) { | ||
| return { | ||
| ...env, | ||
| FLUO_STUDIO: '1', | ||
| FLUO_STUDIO_APP_ID: projectDisplayName(project), | ||
| FLUO_STUDIO_EPOCH: '<generated-at-runtime>', | ||
| FLUO_STUDIO_RUNTIME: projectRuntimeToStudioRuntime(projectRuntime), | ||
| FLUO_STUDIO_TOKEN: '<generated-at-runtime>', | ||
| FLUO_STUDIO_URL: 'http://127.0.0.1:<auto>' | ||
| }; | ||
| } | ||
| function renderStep(step) { | ||
@@ -483,2 +537,53 @@ return `${step.command} ${step.args.join(' ')}`.trim(); | ||
| } | ||
| function colorizeRunnerSteps(steps, env) { | ||
| return withPipedAppBootstrapImports(steps, env); | ||
| } | ||
| async function executeRunnerStepsWithReporter(options) { | ||
| if (options.command === 'dev' && (options.reporterMode === 'pretty' || options.verbose)) { | ||
| options.childEnv[SHOW_NODE_RESTART_NOTICE_ENV] = '1'; | ||
| } | ||
| if (options.reporterMode === 'pretty') { | ||
| options.stdout.write(`[fluo] ${options.command} ${options.projectRuntime} lifecycle starting\n`); | ||
| options.stdout.write(`[fluo] ${options.runnerSteps.map(renderStep).join(' && ')}\n`); | ||
| } | ||
| const reporterStreams = createReporterStreams(options.reporterMode, options.verbose, options.stdout, options.stderr); | ||
| const exitCode = await runProjectRunnerSteps(options.runnerSteps, { | ||
| spawnCommand: options.runtime.spawnCommand ?? defaultSpawnCommand | ||
| }, { | ||
| cwd: options.projectDirectory, | ||
| env: options.childEnv, | ||
| ...reporterStreams | ||
| }); | ||
| if (options.reporterMode === 'pretty') { | ||
| reporterStreams.finalizeChildOutputBeforeStatus(); | ||
| if (exitCode === 0) { | ||
| options.stdout.write(`[fluo] ${options.command} lifecycle completed\n`); | ||
| } else { | ||
| reporterStreams.flushBufferedStdoutOnFailure(); | ||
| options.stderr.write(`[fluo] ${options.command} lifecycle failed with exit code ${exitCode}\n`); | ||
| } | ||
| } else if (options.reporterMode === 'silent' && exitCode !== 0) { | ||
| reporterStreams.flushBufferedStdoutOnFailure(); | ||
| options.stderr.write(`[fluo] ${options.command} lifecycle failed with exit code ${exitCode}\n`); | ||
| } | ||
| return exitCode; | ||
| } | ||
| async function runScriptWithStudioSidecar(command, projectDirectory, projectRuntime, runnerSteps, childEnv, runtime, reporterMode, verbose, stdout, stderr, studioSidecar) { | ||
| try { | ||
| return await executeRunnerStepsWithReporter({ | ||
| childEnv, | ||
| command, | ||
| projectDirectory, | ||
| projectRuntime, | ||
| reporterMode, | ||
| runnerSteps, | ||
| runtime, | ||
| stderr, | ||
| stdout, | ||
| verbose | ||
| }); | ||
| } finally { | ||
| await studioSidecar.close(); | ||
| } | ||
| } | ||
@@ -493,3 +598,3 @@ /** | ||
| const nodeEnv = command === 'dev' ? 'development' : 'production'; | ||
| return [`Usage: fluo ${command} [options] [-- <args>]`, '', `Run the generated fluo project ${command} lifecycle with NODE_ENV defaulting to ${nodeEnv} when unset.`, '', 'Default output forwards child stdout/stderr without fluo lifecycle UI.', 'Use --reporter pretty for fluo lifecycle status + app │ prefixes.', 'Use --verbose (or FLUO_VERBOSE=1) to expose raw runtime/tooling output.', '', 'Options', ' --dry-run Print the command without running it.', command === 'dev' ? ' --raw-watch Use the runtime-native Node watcher instead of the fluo restart runner.' : undefined, command === 'dev' ? ' --runner <fluo|native> Select fluo restart supervision or runtime-native watch (default: fluo for Node, native for non-Node runtimes).' : undefined, ' --reporter <auto|pretty|stream|silent> Choose lifecycle reporter output mode (default: auto).', ' --verbose Expose raw child process output; also honored by FLUO_VERBOSE=1.', ` --help Show help for the ${command} command.`].filter(line => typeof line === 'string').join('\n'); | ||
| return [`Usage: fluo ${command} [options] [-- <args>]`, '', `Run the generated fluo project ${command} lifecycle with NODE_ENV defaulting to ${nodeEnv} when unset.`, '', 'Default output forwards child stdout/stderr without fluo lifecycle UI.', 'Use --reporter pretty for fluo lifecycle status + app │ prefixes.', 'Use --verbose (or FLUO_VERBOSE=1) to expose raw runtime/tooling output.', '', 'Options', ' --dry-run Print the command without running it.', command === 'dev' ? ' --raw-watch Use the runtime-native Node watcher instead of the fluo restart runner.' : undefined, command === 'dev' ? ' --runner <fluo|native> Select fluo restart supervision or runtime-native watch (default: fluo for Node, native for non-Node runtimes).' : undefined, command === 'dev' ? ' --studio Start the local Fluo Studio sidecar and inject runtime devtool env.' : undefined, command === 'dev' ? ' --studio-port <port> Bind the Studio sidecar to a specific local port (default: 0).' : undefined, ' --reporter <auto|pretty|stream|silent> Choose lifecycle reporter output mode (default: auto).', ' --verbose Expose raw child process output; also honored by FLUO_VERBOSE=1.', ` --help Show help for the ${command} command.`].filter(line => typeof line === 'string').join('\n'); | ||
| } | ||
@@ -525,2 +630,3 @@ | ||
| const devRunner = command === 'dev' ? resolveDevRunnerPreference(parsed, env, projectRuntime) : 'fluo'; | ||
| assertStudioSupport(command, parsed.studio, projectRuntime, devRunner); | ||
| const runnerSteps = buildProjectRunner(command, projectRuntime, parsed.passThrough, { | ||
@@ -536,4 +642,22 @@ devRunner, | ||
| const verbose = parsed.verbose || isEnabledEnvironmentFlag(env.FLUO_VERBOSE); | ||
| const childEnv = withPipedReporterColorEnv(withProjectLocalBin(withDefaultNodeEnv(env, defaultNodeEnv), project.directory), reporterMode, stdout, stderr); | ||
| const colorAwareRunnerSteps = withPipedAppColorTtyBootstrap(runnerSteps, childEnv); | ||
| let childEnv = withPipedReporterColorEnv(withProjectLocalBin(withDefaultNodeEnv(env, defaultNodeEnv), project.directory), reporterMode, stdout, stderr); | ||
| if (parsed.studio && parsed.dryRun) { | ||
| childEnv = withStudioDryRunEnv(childEnv, project, projectRuntime); | ||
| } | ||
| if (command === 'dev' && parsed.studio && !parsed.dryRun) { | ||
| const studioSidecarFactory = runtime.startStudioSidecar ?? startStudioSidecar; | ||
| const studioSidecar = await studioSidecarFactory({ | ||
| appId: projectDisplayName(project), | ||
| port: parsed.studioPort, | ||
| runtime: projectRuntimeToStudioRuntime(projectRuntime) | ||
| }); | ||
| childEnv = { | ||
| ...childEnv, | ||
| ...studioSidecar.env | ||
| }; | ||
| const studioUrl = `${studioSidecar.url}/?token=${encodeURIComponent(studioSidecar.token)}`; | ||
| stdout.write(`[fluo] Studio listening at ${studioUrl}\n`); | ||
| return await runScriptWithStudioSidecar(command, project.directory, projectRuntime, colorizeRunnerSteps(runnerSteps, childEnv), childEnv, runtime, reporterMode, verbose, stdout, stderr, studioSidecar); | ||
| } | ||
| const colorAwareRunnerSteps = colorizeRunnerSteps(runnerSteps, childEnv); | ||
| if (command === 'dev' && (reporterMode === 'pretty' || verbose)) { | ||
@@ -552,30 +676,22 @@ childEnv[SHOW_NODE_RESTART_NOTICE_ENV] = '1'; | ||
| stdout.write(`Watch mode: ${colorAwareRunnerSteps.map(step => step.mode ?? 'single-run').join(', ')}\n`); | ||
| if (parsed.studio) { | ||
| stdout.write('Studio: enabled (sidecar binds 127.0.0.1 at runtime)\n'); | ||
| stdout.write(`FLUO_STUDIO: ${childEnv.FLUO_STUDIO ?? ''}\n`); | ||
| stdout.write(`FLUO_STUDIO_URL: ${childEnv.FLUO_STUDIO_URL ?? ''}\n`); | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
| if (reporterMode === 'pretty') { | ||
| stdout.write(`[fluo] ${command} ${projectRuntime} lifecycle starting\n`); | ||
| stdout.write(`[fluo] ${colorAwareRunnerSteps.map(renderStep).join(' && ')}\n`); | ||
| } | ||
| const reporterStreams = createReporterStreams(reporterMode, verbose, stdout, stderr); | ||
| const exitCode = await runProjectRunnerSteps(colorAwareRunnerSteps, { | ||
| spawnCommand: runtime.spawnCommand ?? defaultSpawnCommand | ||
| }, { | ||
| cwd: project.directory, | ||
| env: childEnv, | ||
| ...reporterStreams | ||
| return await executeRunnerStepsWithReporter({ | ||
| childEnv, | ||
| command, | ||
| projectDirectory: project.directory, | ||
| projectRuntime, | ||
| reporterMode, | ||
| runnerSteps: colorAwareRunnerSteps, | ||
| runtime, | ||
| stderr, | ||
| stdout, | ||
| verbose | ||
| }); | ||
| if (reporterMode === 'pretty') { | ||
| reporterStreams.finalizeChildOutputBeforeStatus(); | ||
| if (exitCode === 0) { | ||
| stdout.write(`[fluo] ${command} lifecycle completed\n`); | ||
| } else { | ||
| reporterStreams.flushBufferedStdoutOnFailure(); | ||
| stderr.write(`[fluo] ${command} lifecycle failed with exit code ${exitCode}\n`); | ||
| } | ||
| } else if (reporterMode === 'silent' && exitCode !== 0) { | ||
| reporterStreams.flushBufferedStdoutOnFailure(); | ||
| stderr.write(`[fluo] ${command} lifecycle failed with exit code ${exitCode}\n`); | ||
| } | ||
| return exitCode; | ||
| } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"node-restart-runner.d.ts","sourceRoot":"","sources":["../../src/dev-runner/node-restart-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,EAA0D,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAIjG,KAAK,mBAAmB,GAAG;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,KAAK,YAAY,CAAC;AACjJ,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,oBAAoB,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9E,KAAK,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1C,KAAK,mBAAmB,GAAG;IACzB,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;IAC1D,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CAC5D,CAAC;AAEF,KAAK,qBAAqB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,KAAK,SAAS,CAAC;AAE1O,KAAK,iBAAiB,GAAG;IACvB,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9C,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;CACvD,CAAC;AAEF,uFAAuF;AACvF,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC,CAAC;AA4HF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,EAAoB,GAAG,iBAAiB,CAuB/H;AAiFD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2N7F"} | ||
| {"version":3,"file":"node-restart-runner.d.ts","sourceRoot":"","sources":["../../src/dev-runner/node-restart-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,EAA0D,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAKjG,KAAK,mBAAmB,GAAG;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,KAAK,YAAY,CAAC;AACjJ,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,oBAAoB,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9E,KAAK,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1C,KAAK,mBAAmB,GAAG;IACzB,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;IAC1D,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CAC5D,CAAC;AAEF,KAAK,qBAAqB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,KAAK,SAAS,CAAC;AAE1O,KAAK,iBAAiB,GAAG;IACvB,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9C,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;CACvD,CAAC;AAEF,uFAAuF;AACvF,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC,CAAC;AA2LF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,EAAoB,GAAG,iBAAiB,CAuB/H;AAkFD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAiP7F"} |
@@ -6,2 +6,3 @@ import { spawn } from 'node:child_process'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { createStudioDevtoolsNodeImport } from '../studio/runtime-config.js'; | ||
@@ -19,2 +20,49 @@ /** Runtime target handled by the fluo-owned development restart runner. */ | ||
| const CLEAR_SCREEN = '\u001B[2J\u001B[3J\u001B[H'; | ||
| function isEnabledEnvironmentFlag(value) { | ||
| return value === '1' || value === 'true' || value === 'yes'; | ||
| } | ||
| function studioRuntimeName(runtime) { | ||
| if (runtime === 'cloudflare-workers') { | ||
| return 'worker'; | ||
| } | ||
| return runtime; | ||
| } | ||
| function resolveStudioIngestEndpoint(env) { | ||
| if (!isEnabledEnvironmentFlag(env.FLUO_STUDIO) || !env.FLUO_STUDIO_TOKEN) { | ||
| return undefined; | ||
| } | ||
| if (env.FLUO_STUDIO_ENDPOINT) { | ||
| return env.FLUO_STUDIO_ENDPOINT; | ||
| } | ||
| if (!env.FLUO_STUDIO_URL) { | ||
| return undefined; | ||
| } | ||
| try { | ||
| return new URL('/api/runtime/events', env.FLUO_STUDIO_URL).toString(); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
| function publishStudioLifecycleEvent(env, runtime, type, payload) { | ||
| const endpoint = resolveStudioIngestEndpoint(env); | ||
| if (!endpoint || typeof globalThis.fetch !== 'function') { | ||
| return; | ||
| } | ||
| void globalThis.fetch(endpoint, { | ||
| body: JSON.stringify({ | ||
| payload, | ||
| source: { | ||
| appId: env.FLUO_STUDIO_APP_ID ?? basename(process.cwd()), | ||
| runtime: studioRuntimeName(runtime) | ||
| }, | ||
| type, | ||
| version: 1 | ||
| }), | ||
| headers: { | ||
| authorization: `Bearer ${env.FLUO_STUDIO_TOKEN}`, | ||
| 'content-type': 'application/json' | ||
| }, | ||
| method: 'POST' | ||
| }).catch(() => undefined); | ||
| } | ||
| function normalizeIgnorePatterns(patterns) { | ||
@@ -156,3 +204,4 @@ return patterns.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0); | ||
| const colorTtyImport = env[PRETTY_TTY_COLOR_ENV] === '1' ? ['--import', getPreserveColorTtyImport()] : []; | ||
| return ['--env-file=.env', ...colorTtyImport, '--import', 'tsx', 'src/main.ts', ...appArgs]; | ||
| const studioDevtoolsImport = createStudioDevtoolsNodeImport(env); | ||
| return ['--env-file=.env', ...colorTtyImport, ...studioDevtoolsImport, '--import', 'tsx', 'src/main.ts', ...appArgs]; | ||
| } | ||
@@ -236,2 +285,6 @@ function buildBunAppArgs(env, appArgs) { | ||
| const appCommand = buildAppCommand(runnerRuntime, env, appArgs); | ||
| publishStudioLifecycleEvent(env, runnerRuntime, 'restart', { | ||
| phase: 'starting', | ||
| reason: 'fluo dev runner starting app child' | ||
| }); | ||
| child = spawnChild(appCommand.command, appCommand.args, { | ||
@@ -242,2 +295,6 @@ cwd: projectDirectory, | ||
| }); | ||
| publishStudioLifecycleEvent(env, runnerRuntime, 'restart', { | ||
| phase: 'started', | ||
| reason: 'fluo dev runner spawned app child' | ||
| }); | ||
| let childSettled = false; | ||
@@ -263,2 +320,5 @@ child.once('error', error => { | ||
| if (stopping) { | ||
| publishStudioLifecycleEvent(env, runnerRuntime, 'disconnect', { | ||
| reason: 'fluo dev runner stopped' | ||
| }); | ||
| cleanup(); | ||
@@ -268,2 +328,5 @@ resolveExitCode(code ?? 0); | ||
| } | ||
| publishStudioLifecycleEvent(env, runnerRuntime, 'disconnect', { | ||
| reason: `app child exited with code ${String(code ?? 1)}` | ||
| }); | ||
| cleanup(); | ||
@@ -288,2 +351,6 @@ resolveExitCode(code ?? 1); | ||
| } | ||
| publishStudioLifecycleEvent(env, runnerRuntime, 'restart', { | ||
| phase: 'scheduled', | ||
| reason: `content changed: ${relative(projectDirectory, restartPaths[restartPaths.length - 1] ?? projectDirectory)}` | ||
| }); | ||
| const previousChild = child; | ||
@@ -303,2 +370,6 @@ const startReplacementChild = () => { | ||
| restarting = true; | ||
| publishStudioLifecycleEvent(env, runnerRuntime, 'restart', { | ||
| phase: 'stopping', | ||
| reason: 'stopping previous app child before restart' | ||
| }); | ||
| previousChild.once('close', () => { | ||
@@ -305,0 +376,0 @@ const committedRestartPaths = [...restartAfterClosePaths]; |
+3
-3
@@ -12,3 +12,3 @@ { | ||
| ], | ||
| "version": "1.0.4", | ||
| "version": "1.0.5", | ||
| "private": false, | ||
@@ -48,6 +48,6 @@ "license": "MIT", | ||
| "typescript": "^6.0.2", | ||
| "@fluojs/runtime": "^1.1.2" | ||
| "@fluojs/runtime": "^1.1.3" | ||
| }, | ||
| "peerDependencies": { | ||
| "@fluojs/studio": "^1.0.4" | ||
| "@fluojs/studio": "^1.0.5" | ||
| }, | ||
@@ -54,0 +54,0 @@ "peerDependenciesMeta": { |
+23
-0
@@ -187,2 +187,25 @@ # @fluojs/cli | ||
| ### Runtime-connected Studio devtool | ||
| 실행 중인 앱에 local React Studio devtool을 붙이고 싶을 때는 static HTML/JSON을 먼저 내보내지 말고 `fluo dev --studio`를 사용합니다. | ||
| ```bash | ||
| fluo dev --studio | ||
| fluo dev --studio --studio-port 51234 | ||
| fluo dev --studio --dry-run | ||
| ``` | ||
| CLI는 local Studio sidecar를 시작하고, tokenized URL을 출력하며, restart lifecycle event를 sidecar로 계속 전달하고, 앱이 `@fluojs/runtime`을 import하기 전에 명시적인 Studio config를 Node 앱 child에 주입합니다. Optional package인 `@fluojs/studio`가 설치되어 있으면 sidecar는 패키징된 `@fluojs/studio/viewer` React app을 제공합니다. Runtime package source는 `process.env`를 직접 읽지 않으며, CLI가 주입한 Studio config가 있을 때만 live graph/routes/request/timing/diagnostic event를 전송합니다. | ||
| 보안 기본값은 local-only입니다. Sidecar는 `127.0.0.1`에 bind되고, runtime ingestion 및 browser state/SSE API는 generated token을 요구하며, CORS는 기본적으로 활성화하지 않고, request body는 기본적으로 수집하지 않습니다. | ||
| MVP runtime support는 명시적으로 제한됩니다. | ||
| | Runtime target | `fluo dev --studio` status | | ||
| | --- | --- | | ||
| | Node dev runner | Full support target입니다. | | ||
| | Bun | 이번 MVP에서는 활성화하지 않습니다. Dedicated bridge를 구현하고 검증하기 전까지 `fluo dev --studio`는 Bun 프로젝트를 거부합니다. | | ||
| | Deno | 이번 MVP에서는 활성화하지 않습니다. Dedicated bridge를 구현하고 검증하기 전까지 `fluo dev --studio`는 Deno 프로젝트를 거부합니다. | | ||
| | Cloudflare Workers | worker bridge를 추가하고 테스트하지 않는 한 이번 MVP에서는 unsupported입니다. | | ||
| CLI process boundary를 조정해야 할 때는 런타임 앱 로깅이 아니라 reporter flag를 사용하세요: | ||
@@ -189,0 +212,0 @@ |
+23
-0
@@ -187,2 +187,25 @@ # @fluojs/cli | ||
| ### Runtime-connected Studio devtool | ||
| Use `fluo dev --studio` when you want the local React Studio devtool attached to the running app instead of exporting static HTML/JSON first: | ||
| ```bash | ||
| fluo dev --studio | ||
| fluo dev --studio --studio-port 51234 | ||
| fluo dev --studio --dry-run | ||
| ``` | ||
| The CLI starts a local Studio sidecar, prints a tokenized URL, keeps restart lifecycle events flowing through the sidecar, and injects an explicit Studio config into the Node app child before the app imports `@fluojs/runtime`. The sidecar serves the packaged `@fluojs/studio/viewer` React app when that optional package is installed. Runtime package source never reads `process.env` directly; it publishes live graph/routes/request/timing/diagnostic events only when CLI-injected Studio config is present. | ||
| Security defaults are local-only: the sidecar binds `127.0.0.1`, runtime ingestion and browser state/SSE APIs require generated tokens, CORS is not enabled by default, and request bodies are not captured by default. | ||
| Runtime support for the MVP is explicit: | ||
| | Runtime target | `fluo dev --studio` status | | ||
| | --- | --- | | ||
| | Node dev runner | Full support target. | | ||
| | Bun | Not enabled for this MVP; `fluo dev --studio` rejects Bun projects until a dedicated bridge is implemented and verified. | | ||
| | Deno | Not enabled for this MVP; `fluo dev --studio` rejects Deno projects until a dedicated bridge is implemented and verified. | | ||
| | Cloudflare Workers | Unsupported for this MVP unless a worker bridge is added and tested. | | ||
| Use reporter flags when you need to tune the CLI process boundary rather than runtime app logging: | ||
@@ -189,0 +212,0 @@ |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
520212
5.94%151
4.14%10790
6.82%333
7.42%14
7.69%5
66.67%Updated