@fluojs/cli 1.0.0-beta.5 → 1.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,12 +5,18 @@ import { fileURLToPath } from 'node:url';
5
5
  import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js';
6
6
  const EMPTY_ENV = {};
7
7
  const FAILURE_STDOUT_BUFFER_LIMIT = 16_384;
8
+ const PRETTY_TTY_COLOR_ENV = 'FLUO_DEV_PRETTY_TTY_COLOR';
9
+ const PRETTY_CHILD_OUTPUT_PREFIX = 'app │ ';
10
+ const SHOW_NODE_RESTART_NOTICE_ENV = 'FLUO_DEV_SHOW_RESTART_NOTICE';
8
11
  function getCliSourceRoot() {
9
12
  return dirname(dirname(fileURLToPath(import.meta.url)));
10
13
  }
11
14
  function getCliEntryPoint() {
12
15
  return join(getCliSourceRoot(), 'cli.js');
13
16
  }
17
+ function getPreserveColorTtyImport() {
18
+ return join(getCliSourceRoot(), 'dev-runner', 'preserve-color-tty.js');
19
+ }
14
20
  function isRecord(value) {
15
21
  return typeof value === 'object' && value !== null;
16
22
  }
@@ -87,6 +93,19 @@ function withProjectLocalBin(env, projectDirectory) {
87
93
  [pathKey]: existingPath ? `${localBin}${delimiter}${existingPath}` : localBin
88
94
  };
89
95
  }
96
+ function withPipedReporterColorEnv(env, mode, stdout, stderr) {
97
+ if (!(mode === 'app' || mode === 'pretty') || env.NO_COLOR !== undefined) {
98
+ return env;
99
+ }
100
+ if (!stdout.isTTY || !stderr.isTTY) {
101
+ return env;
102
+ }
103
+ return {
104
+ ...env,
105
+ FORCE_COLOR: env.FORCE_COLOR ?? '1',
106
+ [PRETTY_TTY_COLOR_ENV]: '1'
107
+ };
108
+ }
90
109
  function defaultSpawnCommand(command, args, options) {
91
110
  return new Promise((resolveExitCode, reject) => {
92
111
  const child = spawn(command, args, options);
@@ -105,6 +124,39 @@ function buildNativeNodeWatchStep(passThrough) {
105
124
  mode: 'native-watch'
106
125
  };
107
126
  }
127
+ function buildFluoDevRunnerStep(runtime, passThrough) {
128
+ return {
129
+ command: 'node',
130
+ args: ['--import', 'tsx', getCliEntryPoint(), '__dev-runner', '--runtime', runtime, '--', ...passThrough],
131
+ mode: 'fluo-restart'
132
+ };
133
+ }
134
+ function buildNativeRuntimeDevStep(runtime, passThrough) {
135
+ switch (runtime) {
136
+ case 'node':
137
+ return buildNativeNodeWatchStep(passThrough);
138
+ case 'bun':
139
+ return {
140
+ command: 'bun',
141
+ args: ['--watch', 'src/main.ts', ...passThrough],
142
+ mode: 'runtime-native-watch'
143
+ };
144
+ case 'deno':
145
+ return {
146
+ command: 'deno',
147
+ args: ['run', '--watch', '--allow-env', '--allow-net', 'src/main.ts', ...passThrough],
148
+ mode: 'runtime-native-watch'
149
+ };
150
+ case 'cloudflare-workers':
151
+ return {
152
+ command: 'wrangler',
153
+ args: ['dev', '--show-interactive-dev-session=false', ...passThrough],
154
+ mode: 'runtime-native-watch'
155
+ };
156
+ default:
157
+ return undefined;
158
+ }
159
+ }
108
160
  function buildProjectRunner(command, runtime, passThrough, options) {
109
161
  if (command === 'build') {
110
162
  switch (runtime) {
@@ -136,32 +188,24 @@ function buildProjectRunner(command, runtime, passThrough, options) {
136
188
  }
137
189
  }
138
190
  if (command === 'dev') {
191
+ if (options.devRunner === 'native') {
192
+ const nativeStep = buildNativeRuntimeDevStep(runtime, passThrough);
193
+ if (nativeStep) {
194
+ return [nativeStep];
195
+ }
196
+ }
139
197
  switch (runtime) {
140
198
  case 'bun':
141
- return [{
142
- command: 'bun',
143
- args: ['--watch', 'src/main.ts', ...passThrough]
144
- }];
199
+ return [buildFluoDevRunnerStep(runtime, passThrough)];
145
200
  case 'deno':
146
- return [{
147
- command: 'deno',
148
- args: ['run', '--allow-env', '--allow-net', '--watch', 'src/main.ts', ...passThrough]
149
- }];
201
+ return [buildFluoDevRunnerStep(runtime, passThrough)];
150
202
  case 'cloudflare-workers':
151
- return [{
152
- command: 'wrangler',
153
- args: ['dev', ...passThrough],
154
- mode: 'native-watch'
155
- }];
203
+ return [buildFluoDevRunnerStep(runtime, passThrough)];
156
204
  default:
157
205
  if (options.rawWatch) {
158
206
  return [buildNativeNodeWatchStep(passThrough)];
159
207
  }
160
- return [{
161
- command: 'node',
162
- args: ['--import', 'tsx', getCliEntryPoint(), '__node-dev-runner', '--', ...passThrough],
163
- mode: 'fluo-node-restart'
164
- }];
208
+ return [buildFluoDevRunnerStep(runtime, passThrough)];
165
209
  }
166
210
  }
167
211
  switch (runtime) {
@@ -178,7 +222,7 @@ function buildProjectRunner(command, runtime, passThrough, options) {
178
222
  case 'cloudflare-workers':
179
223
  return [{
180
224
  command: 'wrangler',
181
- args: ['dev', '--remote', ...passThrough]
225
+ args: ['dev', '--remote', '--show-interactive-dev-session=false', ...passThrough]
182
226
  }];
183
227
  default:
184
228
  return [{
@@ -197,7 +241,29 @@ async function runProjectRunnerSteps(steps, runtime, options) {
197
241
  }
198
242
  return 0;
199
243
  }
244
+ function withPipedAppColorTtyBootstrap(steps, env) {
245
+ if (env[PRETTY_TTY_COLOR_ENV] !== '1') {
246
+ return steps;
247
+ }
248
+ return steps.map(step => {
249
+ const preserveColorTtyImport = getPreserveColorTtyImport();
250
+ if (step.command === 'node' && step.mode !== 'fluo-restart') {
251
+ return {
252
+ ...step,
253
+ args: ['--import', preserveColorTtyImport, ...step.args]
254
+ };
255
+ }
256
+ if (step.command === 'bun' && (step.mode === 'runtime-native-watch' || step.args[0] === 'dist/main.js')) {
257
+ return {
258
+ ...step,
259
+ args: ['--preload', preserveColorTtyImport, ...step.args]
260
+ };
261
+ }
262
+ return step;
263
+ });
264
+ }
200
265
  function parseScriptArgs(argv) {
266
+ let devRunner;
201
267
  let dryRun = false;
202
268
  let packageManager;
203
269
  let rawWatch = false;
@@ -218,13 +284,25 @@ function parseScriptArgs(argv) {
218
284
  rawWatch = true;
219
285
  continue;
220
286
  }
287
+ if (arg === '--runner') {
288
+ const value = argv[index + 1];
289
+ if (!value || value.startsWith('-')) {
290
+ throw new Error('Expected --runner to have a value.');
291
+ }
292
+ if (!(value === 'fluo' || value === 'native')) {
293
+ throw new Error(`Invalid --runner value "${value}". Use one of: fluo, native.`);
294
+ }
295
+ devRunner = value;
296
+ index += 1;
297
+ continue;
298
+ }
221
299
  if (arg === '--reporter') {
222
300
  const value = argv[index + 1];
223
301
  if (!value || value.startsWith('-')) {
224
302
  throw new Error('Expected --reporter to have a value.');
225
303
  }
226
- if (!(value === 'auto' || value === 'stream' || value === 'silent')) {
227
- throw new Error(`Invalid --reporter value "${value}". Use one of: auto, stream, silent.`);
304
+ if (!(value === 'auto' || value === 'pretty' || value === 'stream' || value === 'silent')) {
305
+ throw new Error(`Invalid --reporter value "${value}". Use one of: auto, pretty, stream, silent.`);
228
306
  }
229
307
  reporter = value;
230
308
  index += 1;
@@ -249,6 +327,7 @@ function parseScriptArgs(argv) {
249
327
  passThrough.push(arg);
250
328
  }
251
329
  return {
330
+ devRunner,
252
331
  dryRun,
253
332
  packageManager,
254
333
  passThrough,
@@ -260,20 +339,24 @@ function parseScriptArgs(argv) {
260
339
  function isEnabledEnvironmentFlag(value) {
261
340
  return value === '1' || value === 'true' || value === 'yes' || value === 'on';
262
341
  }
263
- function resolveReporterMode(command, parsed, runtime) {
342
+ function resolveReporterMode(parsed, runtime) {
264
343
  if (parsed.reporter !== 'auto') {
265
344
  return parsed.reporter;
266
345
  }
267
346
  if (parsed.verbose || isEnabledEnvironmentFlag(runtime.env?.FLUO_VERBOSE)) {
268
347
  return 'stream';
269
348
  }
270
- if (runtime.ci || isEnabledEnvironmentFlag(runtime.env?.CI) || isEnabledEnvironmentFlag(runtime.env?.GITHUB_ACTIONS)) {
271
- return 'stream';
349
+ return 'app';
350
+ }
351
+ function resolveDevRunnerPreference(parsed, env, runtime) {
352
+ const configured = parsed.devRunner ?? env.FLUO_DEV_RUNNER;
353
+ if (configured === undefined || configured === '') {
354
+ return runtime === 'node' ? 'fluo' : 'native';
272
355
  }
273
- if (command !== 'dev') {
274
- return 'stream';
356
+ if (configured === 'fluo' || configured === 'native') {
357
+ return configured;
275
358
  }
276
- return (runtime.stdout ?? process.stdout).isTTY ? 'pretty' : 'stream';
359
+ throw new Error(`Invalid FLUO_DEV_RUNNER value "${configured}". Use one of: fluo, native.`);
277
360
  }
278
361
  function renderStep(step) {
279
362
  return `${step.command} ${step.args.join(' ')}`.trim();
@@ -297,16 +380,73 @@ function createBoundedBufferStream(limit) {
297
380
  }
298
381
  };
299
382
  }
383
+ function createLinePrefixedStream(target, prefix) {
384
+ let atLineStart = true;
385
+ return {
386
+ finalizeLine() {
387
+ if (!atLineStart) {
388
+ target.write('\n');
389
+ atLineStart = true;
390
+ }
391
+ },
392
+ write(message) {
393
+ for (const character of message) {
394
+ if (atLineStart && character !== '\n') {
395
+ target.write(prefix);
396
+ atLineStart = false;
397
+ }
398
+ target.write(character);
399
+ if (character === '\n') {
400
+ atLineStart = true;
401
+ }
402
+ }
403
+ }
404
+ };
405
+ }
300
406
  function createReporterStreams(mode, verbose, stdout, stderr) {
301
407
  if (mode === 'stream') {
302
408
  return {
409
+ finalizeChildOutputBeforeStatus() {},
303
410
  flushBufferedStdoutOnFailure() {},
304
411
  stdio: 'inherit'
305
412
  };
306
413
  }
307
- if (mode === 'silent' || mode === 'pretty') {
414
+ if (mode === 'app') {
415
+ return {
416
+ finalizeChildOutputBeforeStatus() {},
417
+ flushBufferedStdoutOnFailure() {},
418
+ stderr,
419
+ stdio: 'pipe',
420
+ stdout
421
+ };
422
+ }
423
+ if (mode === 'pretty') {
424
+ if (verbose) {
425
+ return {
426
+ finalizeChildOutputBeforeStatus() {},
427
+ flushBufferedStdoutOnFailure() {},
428
+ stderr,
429
+ stdio: 'pipe',
430
+ stdout
431
+ };
432
+ }
433
+ const prefixedStdout = createLinePrefixedStream(stdout, PRETTY_CHILD_OUTPUT_PREFIX);
434
+ const prefixedStderr = createLinePrefixedStream(stderr, PRETTY_CHILD_OUTPUT_PREFIX);
435
+ return {
436
+ finalizeChildOutputBeforeStatus() {
437
+ prefixedStdout.finalizeLine();
438
+ prefixedStderr.finalizeLine();
439
+ },
440
+ flushBufferedStdoutOnFailure() {},
441
+ stderr: prefixedStderr,
442
+ stdio: 'pipe',
443
+ stdout: prefixedStdout
444
+ };
445
+ }
446
+ if (mode === 'silent') {
308
447
  if (verbose) {
309
448
  return {
449
+ finalizeChildOutputBeforeStatus() {},
310
450
  flushBufferedStdoutOnFailure() {},
311
451
  stderr,
312
452
  stdio: 'pipe',
@@ -315,6 +455,7 @@ function createReporterStreams(mode, verbose, stdout, stderr) {
315
455
  }
316
456
  const bufferedStdout = createBoundedBufferStream(FAILURE_STDOUT_BUFFER_LIMIT);
317
457
  return {
458
+ finalizeChildOutputBeforeStatus() {},
318
459
  flushBufferedStdoutOnFailure() {
319
460
  if (bufferedStdout.hasContent()) {
320
461
  stderr.write('[fluo] child stdout before failure:\n');
@@ -328,6 +469,7 @@ function createReporterStreams(mode, verbose, stdout, stderr) {
328
469
  };
329
470
  }
330
471
  return {
472
+ finalizeChildOutputBeforeStatus() {},
331
473
  flushBufferedStdoutOnFailure() {},
332
474
  stdio: 'inherit'
333
475
  };
@@ -341,7 +483,7 @@ function createReporterStreams(mode, verbose, stdout, stderr) {
341
483
  */
342
484
  export function scriptUsage(command) {
343
485
  const nodeEnv = command === 'dev' ? 'development' : 'production';
344
- return [`Usage: fluo ${command} [options] [-- <args>]`, '', `Run the generated fluo project ${command} lifecycle with NODE_ENV defaulting to ${nodeEnv} when unset.`, '', 'Options', ' --dry-run Print the command without running it.', command === 'dev' ? ' --raw-watch Use the runtime-native watcher instead of the fluo Node restart runner.' : undefined, ' --reporter <auto|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');
486
+ 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');
345
487
  }
346
488
 
347
489
  /**
@@ -367,19 +509,28 @@ export async function runScriptCommand(command, argv, runtime = {}) {
367
509
  const parsed = parseScriptArgs(argv);
368
510
  const projectRuntime = detectProjectRuntime(project.manifest);
369
511
  const defaultNodeEnv = command === 'dev' ? 'development' : 'production';
370
- const childEnv = withProjectLocalBin(withDefaultNodeEnv(env, defaultNodeEnv), project.directory);
371
512
  const rawWatch = parsed.rawWatch || isEnabledEnvironmentFlag(env.FLUO_DEV_RAW_WATCH);
513
+ if (command !== 'dev' && parsed.devRunner) {
514
+ throw new Error('--runner is only supported for fluo dev. Use -- to forward --runner to the child command.');
515
+ }
516
+ const devRunner = command === 'dev' ? resolveDevRunnerPreference(parsed, env, projectRuntime) : 'fluo';
372
517
  const runnerSteps = buildProjectRunner(command, projectRuntime, parsed.passThrough, {
518
+ devRunner,
373
519
  rawWatch
374
520
  });
375
- const reporterMode = resolveReporterMode(command, parsed, {
521
+ const reporterMode = resolveReporterMode(parsed, {
376
522
  ...runtime,
377
523
  env,
378
524
  stdout
379
525
  });
380
526
  const verbose = parsed.verbose || isEnabledEnvironmentFlag(env.FLUO_VERBOSE);
527
+ const childEnv = withPipedReporterColorEnv(withProjectLocalBin(withDefaultNodeEnv(env, defaultNodeEnv), project.directory), reporterMode, stdout, stderr);
528
+ const colorAwareRunnerSteps = withPipedAppColorTtyBootstrap(runnerSteps, childEnv);
529
+ if (command === 'dev' && (reporterMode === 'pretty' || verbose)) {
530
+ childEnv[SHOW_NODE_RESTART_NOTICE_ENV] = '1';
531
+ }
381
532
  if (parsed.dryRun) {
382
- for (const step of runnerSteps) {
533
+ for (const step of colorAwareRunnerSteps) {
383
534
  stdout.write(`Would run: ${step.command} ${step.args.join(' ')}\n`);
384
535
  }
385
536
  stdout.write(`Project: ${project.path}\n`);
@@ -387,16 +538,16 @@ export async function runScriptCommand(command, argv, runtime = {}) {
387
538
  stdout.write(`NODE_ENV: ${childEnv.NODE_ENV ?? ''}\n`);
388
539
  stdout.write(`Reporter: ${reporterMode}\n`);
389
540
  if (command === 'dev') {
390
- stdout.write(`Watch mode: ${runnerSteps.map(step => step.mode ?? 'single-run').join(', ')}\n`);
541
+ stdout.write(`Watch mode: ${colorAwareRunnerSteps.map(step => step.mode ?? 'single-run').join(', ')}\n`);
391
542
  }
392
543
  return 0;
393
544
  }
394
545
  if (reporterMode === 'pretty') {
395
546
  stdout.write(`[fluo] ${command} ${projectRuntime} lifecycle starting\n`);
396
- stdout.write(`[fluo] ${runnerSteps.map(renderStep).join(' && ')}\n`);
547
+ stdout.write(`[fluo] ${colorAwareRunnerSteps.map(renderStep).join(' && ')}\n`);
397
548
  }
398
549
  const reporterStreams = createReporterStreams(reporterMode, verbose, stdout, stderr);
399
- const exitCode = await runProjectRunnerSteps(runnerSteps, {
550
+ const exitCode = await runProjectRunnerSteps(colorAwareRunnerSteps, {
400
551
  spawnCommand: runtime.spawnCommand ?? defaultSpawnCommand
401
552
  }, {
402
553
  cwd: project.directory,
@@ -404,6 +555,7 @@ export async function runScriptCommand(command, argv, runtime = {}) {
404
555
  ...reporterStreams
405
556
  });
406
557
  if (reporterMode === 'pretty') {
558
+ reporterStreams.finalizeChildOutputBeforeStatus();
407
559
  if (exitCode === 0) {
408
560
  stdout.write(`[fluo] ${command} lifecycle completed\n`);
409
561
  } else {
@@ -1,6 +1,7 @@
1
1
  import { type ChildProcess } from 'node:child_process';
2
2
  import { type FSWatcher } from 'node:fs';
3
3
  type RestartRunnerStream = {
4
+ isTTY?: boolean;
4
5
  write(message: string): unknown;
5
6
  };
6
7
  type RestartChildSpawner = (command: string, args: string[], options: {
@@ -8,6 +9,8 @@ type RestartChildSpawner = (command: string, args: string[], options: {
8
9
  env: NodeJS.ProcessEnv;
9
10
  stdio: 'inherit';
10
11
  }) => ChildProcess;
12
+ /** Runtime target handled by the fluo-owned development restart runner. */
13
+ export type DevRunnerRuntime = 'bun' | 'cloudflare-workers' | 'deno' | 'node';
11
14
  type RestartSignal = 'SIGINT' | 'SIGTERM';
12
15
  type RestartSignalTarget = {
13
16
  off(signal: RestartSignal, listener: () => void): unknown;
@@ -20,11 +23,13 @@ type ContentChangeGate = {
20
23
  commitBaseline(paths: Iterable<string>): void;
21
24
  hasMeaningfulChange(paths: Iterable<string>): boolean;
22
25
  };
26
+ /** Options used to configure the fluo-owned Node restart-on-watch process boundary. */
23
27
  export type NodeRestartRunnerOptions = {
24
28
  appArgs?: string[];
25
29
  debounceMs?: number;
26
30
  env: NodeJS.ProcessEnv;
27
31
  projectDirectory?: string;
32
+ runtime?: DevRunnerRuntime;
28
33
  signalTarget?: RestartSignalTarget;
29
34
  spawnChild?: RestartChildSpawner;
30
35
  stderr?: RestartRunnerStream;
@@ -1 +1 @@
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;AAGjG,KAAK,mBAAmB,GAAG;IACzB,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;AAEjJ,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,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,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;AA6FF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,EAAoB,GAAG,iBAAiB,CAuB/H;AAYD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqJ7F"}
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;AAgGF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,EAAoB,GAAG,iBAAiB,CAuB/H;AAqED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2J7F"}
@@ -1,10 +1,19 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { existsSync, readdirSync, readFileSync, statSync, watch } from 'node:fs';
4
- import { basename, join, relative, sep } from 'node:path';
4
+ import { basename, dirname, join, relative, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ /** Runtime target handled by the fluo-owned development restart runner. */
8
+
9
+ /** Options used to configure the fluo-owned Node restart-on-watch process boundary. */
10
+
5
11
  const DEFAULT_DEBOUNCE_MS = 100;
12
+ const PRETTY_TTY_COLOR_ENV = 'FLUO_DEV_PRETTY_TTY_COLOR';
6
13
  const DEFAULT_IGNORES = ['.cache', '.fluo', '.git', '.turbo', 'coverage', 'dist', 'node_modules', '*.swp', '*.swo', '*~', '.#*'];
7
14
  const WATCH_FILES = ['.env', 'package.json', 'tsconfig.json', 'tsconfig.build.json'];
15
+ const SHOW_NODE_RESTART_NOTICE_ENV = 'FLUO_DEV_SHOW_RESTART_NOTICE';
16
+ const CLEAR_SCREEN = '\u001B[2J\u001B[3J\u001B[H';
8
17
  function normalizeIgnorePatterns(patterns) {
9
18
  return patterns.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0);
10
19
  }
@@ -103,6 +112,60 @@ function stopChild(child) {
103
112
  child.kill('SIGTERM');
104
113
  }
105
114
  }
115
+ function getPreserveColorTtyImport() {
116
+ return join(dirname(dirname(fileURLToPath(import.meta.url))), 'dev-runner', 'preserve-color-tty.js');
117
+ }
118
+ function buildNodeAppArgs(env, appArgs) {
119
+ const colorTtyImport = env[PRETTY_TTY_COLOR_ENV] === '1' ? ['--import', getPreserveColorTtyImport()] : [];
120
+ return ['--env-file=.env', ...colorTtyImport, '--import', 'tsx', 'src/main.ts', ...appArgs];
121
+ }
122
+ function buildBunAppArgs(env, appArgs) {
123
+ const colorTtyPreload = env[PRETTY_TTY_COLOR_ENV] === '1' ? ['--preload', getPreserveColorTtyImport()] : [];
124
+ return [...colorTtyPreload, 'src/main.ts', ...appArgs];
125
+ }
126
+ function buildAppCommand(runtime, env, appArgs) {
127
+ switch (runtime) {
128
+ case 'bun':
129
+ return {
130
+ command: 'bun',
131
+ args: buildBunAppArgs(env, appArgs)
132
+ };
133
+ case 'cloudflare-workers':
134
+ return {
135
+ command: 'wrangler',
136
+ args: ['dev', '--show-interactive-dev-session=false', ...appArgs]
137
+ };
138
+ case 'deno':
139
+ return {
140
+ command: 'deno',
141
+ args: ['run', '--allow-env', '--allow-net', 'src/main.ts', ...appArgs]
142
+ };
143
+ default:
144
+ return {
145
+ command: process.execPath,
146
+ args: buildNodeAppArgs(env, appArgs)
147
+ };
148
+ }
149
+ }
150
+ function readDevScriptHeader(projectDirectory) {
151
+ const fallbackName = basename(projectDirectory);
152
+ try {
153
+ const manifest = JSON.parse(readFileSync(join(projectDirectory, 'package.json'), 'utf8'));
154
+ const name = typeof manifest.name === 'string' && manifest.name.length > 0 ? manifest.name : fallbackName;
155
+ const version = typeof manifest.version === 'string' && manifest.version.length > 0 ? `@${manifest.version}` : '';
156
+ const devScript = typeof manifest.scripts?.dev === 'string' && manifest.scripts.dev.length > 0 ? manifest.scripts.dev : 'fluo dev';
157
+ return `> ${name}${version} dev ${projectDirectory}\n> ${devScript}\n\n`;
158
+ } catch (_error) {
159
+ return `> ${fallbackName} dev ${projectDirectory}\n> fluo dev\n\n`;
160
+ }
161
+ }
162
+ function redrawDevScriptHeader(stdout, projectDirectory, env) {
163
+ if (env[SHOW_NODE_RESTART_NOTICE_ENV] !== '1') {
164
+ return;
165
+ }
166
+ stdout.write(CLEAR_SCREEN);
167
+ stdout.write(readDevScriptHeader(projectDirectory));
168
+ }
106
169
 
107
170
  /**
108
171
  * Runs the Node.js development lifecycle through fluo-owned restart supervision.
@@ -113,6 +176,7 @@ function stopChild(child) {
113
176
  export async function runNodeRestartRunner(options) {
114
177
  const projectDirectory = options.projectDirectory ?? process.cwd();
115
178
  const env = options.env;
179
+ const runnerRuntime = options.runtime ?? 'node';
116
180
  const signalTarget = options.signalTarget ?? process;
117
181
  const spawnChild = options.spawnChild ?? spawn;
118
182
  const stdout = options.stdout ?? process.stdout;
@@ -129,7 +193,8 @@ export async function runNodeRestartRunner(options) {
129
193
  let restarting = false;
130
194
  let stopping = false;
131
195
  const startChild = (resolveExitCode, cleanup) => {
132
- child = spawnChild(process.execPath, ['--env-file=.env', '--import', 'tsx', 'src/main.ts', ...appArgs], {
196
+ const appCommand = buildAppCommand(runnerRuntime, env, appArgs);
197
+ child = spawnChild(appCommand.command, appCommand.args, {
133
198
  cwd: projectDirectory,
134
199
  env,
135
200
  stdio: 'inherit'
@@ -159,9 +224,12 @@ export async function runNodeRestartRunner(options) {
159
224
  if (!gate.hasMeaningfulChange(restartPaths)) {
160
225
  return;
161
226
  }
162
- stdout.write(`[fluo] restarting after content change: ${relative(projectDirectory, restartPaths[restartPaths.length - 1] ?? projectDirectory)}\n`);
227
+ if (env[SHOW_NODE_RESTART_NOTICE_ENV] === '1') {
228
+ stdout.write(`[fluo] restarting after content change: ${relative(projectDirectory, restartPaths[restartPaths.length - 1] ?? projectDirectory)}\n`);
229
+ }
163
230
  const previousChild = child;
164
231
  const startReplacementChild = () => {
232
+ redrawDevScriptHeader(stdout, projectDirectory, env);
165
233
  startChild(resolveExitCode, cleanup);
166
234
  gate.commitBaseline(restartPaths);
167
235
  };
@@ -180,6 +248,7 @@ export async function runNodeRestartRunner(options) {
180
248
  if (stopping) {
181
249
  return;
182
250
  }
251
+ redrawDevScriptHeader(stdout, projectDirectory, env);
183
252
  startChild(resolveExitCode, cleanup);
184
253
  gate.commitBaseline(committedRestartPaths);
185
254
  });
@@ -0,0 +1,2 @@
1
+ declare function preserveTtyColorDetection(stream: NodeJS.WriteStream): void;
2
+ //# sourceMappingURL=preserve-color-tty.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preserve-color-tty.d.ts","sourceRoot":"","sources":["../../src/dev-runner/preserve-color-tty.ts"],"names":[],"mappings":"AAAA,iBAAS,yBAAyB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,IAAI,CASnE"}
@@ -0,0 +1,12 @@
1
+ function preserveTtyColorDetection(stream) {
2
+ try {
3
+ Object.defineProperty(stream, 'isTTY', {
4
+ configurable: true,
5
+ value: true
6
+ });
7
+ } catch (_error) {
8
+ return;
9
+ }
10
+ }
11
+ preserveTtyColorDetection(process.stdout);
12
+ preserveTtyColorDetection(process.stderr);
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/new/scaffold.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,YAAY,CAAC;AAqnEnE;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAkB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,6BAAuB,CAAC"}
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/new/scaffold.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,YAAY,CAAC;AA2oEnE;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAkB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,6BAAuB,CAAC"}