🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@fluojs/cli

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fluojs/cli - npm Package Compare versions

Comparing version
1.0.4
to
1.0.5
+20
dist/studio/runtime-config.d.ts
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

@@ -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"}

@@ -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];

@@ -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": {

@@ -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 @@

@@ -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 @@