@kitsy/cnos-cli 1.4.0 → 1.5.1

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.
Files changed (2) hide show
  1. package/dist/index.js +450 -137
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -203,9 +203,6 @@ function parseArgs(argv) {
203
203
  };
204
204
  }
205
205
 
206
- // src/commands/define.ts
207
- import path3 from "path";
208
-
209
206
  // src/cli/commandOptions.ts
210
207
  function consumeFlag(args, flag) {
211
208
  const index = args.indexOf(flag);
@@ -253,17 +250,9 @@ function printJson(value) {
253
250
  return JSON.stringify(value, null, 2);
254
251
  }
255
252
 
256
- // src/services/writes.ts
257
- import { mkdir, readFile, writeFile } from "fs/promises";
253
+ // src/services/envMaterialization.ts
254
+ import { mkdir, writeFile } from "fs/promises";
258
255
  import path2 from "path";
259
- import {
260
- getNamespaceDefinition,
261
- createSecretVaultProvider,
262
- parseYaml,
263
- resolveConfigDocumentPath,
264
- resolveVaultAuth,
265
- stringifyYaml
266
- } from "@kitsy/cnos/internal";
267
256
 
268
257
  // src/services/runtime.ts
269
258
  import { createCnos } from "@kitsy/cnos/configure";
@@ -279,7 +268,92 @@ async function createRuntimeService(options = {}) {
279
268
  });
280
269
  }
281
270
 
271
+ // src/services/envMaterialization.ts
272
+ function resolveEnvFromRuntime(runtime, cliArgs = []) {
273
+ const args = [...cliArgs];
274
+ const isPublic = consumeFlag(args, "--public");
275
+ const framework = consumeOption(args, "--framework");
276
+ const prefix = consumeOption(args, "--prefix");
277
+ return isPublic ? runtime.toPublicEnv({
278
+ ...framework ? { framework } : {},
279
+ ...prefix ? { prefix } : {}
280
+ }) : runtime.toEnv();
281
+ }
282
+ function formatEnvOutput(env) {
283
+ return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
284
+ }
285
+ async function resolveMaterializedEnv(options = {}) {
286
+ const runtime = await createRuntimeService({
287
+ ...options,
288
+ cliArgs: [...options.cliArgs ?? []]
289
+ });
290
+ const env = resolveEnvFromRuntime(runtime, options.cliArgs ?? []);
291
+ return {
292
+ runtime,
293
+ env,
294
+ output: formatEnvOutput(env)
295
+ };
296
+ }
297
+ function resolveMaterializedEnvTarget(to, root = process.cwd()) {
298
+ return path2.resolve(root, to);
299
+ }
300
+ async function writeMaterializedEnvFile(to, output, root = process.cwd()) {
301
+ const targetPath = resolveMaterializedEnvTarget(to, root);
302
+ await mkdir(path2.dirname(targetPath), { recursive: true });
303
+ await writeFile(targetPath, output, "utf8");
304
+ return targetPath;
305
+ }
306
+ async function materializeEnvToFile(to, options = {}) {
307
+ const result = await resolveMaterializedEnv(options);
308
+ const targetPath = await writeMaterializedEnvFile(to, result.output, options.root ?? process.cwd());
309
+ return {
310
+ ...result,
311
+ targetPath
312
+ };
313
+ }
314
+
315
+ // src/commands/build.ts
316
+ async function runBuild(subcommand, options = {}) {
317
+ if (subcommand !== "env") {
318
+ throw new Error(`Unsupported build target: ${subcommand ?? "(missing)"}`);
319
+ }
320
+ const infoArgs = [...options.cliArgs ?? []];
321
+ const isPublic = consumeFlag(infoArgs, "--public");
322
+ const framework = consumeOption(infoArgs, "--framework");
323
+ consumeOption(infoArgs, "--prefix");
324
+ const to = consumeOption(infoArgs, "--to");
325
+ if (!to) {
326
+ throw new Error("build env requires --to <path>");
327
+ }
328
+ const result = await materializeEnvToFile(to, {
329
+ ...options,
330
+ cliArgs: [...options.cliArgs ?? []]
331
+ });
332
+ if (options.json) {
333
+ return printJson({
334
+ to: result.targetPath,
335
+ count: Object.keys(result.env).length,
336
+ public: isPublic,
337
+ ...framework ? { framework } : {}
338
+ });
339
+ }
340
+ return `built env artifact at ${displayPath(result.targetPath, options.root ?? process.cwd())}`;
341
+ }
342
+
343
+ // src/commands/define.ts
344
+ import path4 from "path";
345
+
282
346
  // src/services/writes.ts
347
+ import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
348
+ import path3 from "path";
349
+ import {
350
+ getNamespaceDefinition,
351
+ createSecretVaultProvider,
352
+ parseYaml,
353
+ resolveConfigDocumentPath,
354
+ resolveVaultAuth,
355
+ stringifyYaml
356
+ } from "@kitsy/cnos/internal";
283
357
  function setNestedValue(target, pathSegments, value) {
284
358
  const [head, ...tail] = pathSegments;
285
359
  if (!head) {
@@ -379,8 +453,8 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
379
453
  const document = await readYamlDocument(filePath);
380
454
  const parsedValue = parseScalarValue(rawValue);
381
455
  setNestedValue(document, configPath.split("."), parsedValue);
382
- await mkdir(path2.dirname(filePath), { recursive: true });
383
- await writeFile(filePath, stringifyYaml(document), "utf8");
456
+ await mkdir2(path3.dirname(filePath), { recursive: true });
457
+ await writeFile2(filePath, stringifyYaml(document), "utf8");
384
458
  return {
385
459
  filePath,
386
460
  value: parsedValue
@@ -417,8 +491,8 @@ async function setSecret(configPath, rawValue, options = {}) {
417
491
  };
418
492
  }
419
493
  setNestedValue(document, configPath.split("."), reference);
420
- await mkdir(path2.dirname(filePath), { recursive: true });
421
- await writeFile(filePath, stringifyYaml(document), "utf8");
494
+ await mkdir2(path3.dirname(filePath), { recursive: true });
495
+ await writeFile2(filePath, stringifyYaml(document), "utf8");
422
496
  return {
423
497
  filePath,
424
498
  provider: reference.provider,
@@ -440,7 +514,7 @@ async function deleteSecret(configPath, options = {}) {
440
514
  deleted: false
441
515
  };
442
516
  }
443
- await writeFile(filePath, stringifyYaml(document), "utf8");
517
+ await writeFile2(filePath, stringifyYaml(document), "utf8");
444
518
  const secretRef = metadata?.secretRef;
445
519
  if (isSecretReference(secretRef) && secretRef.provider === "local") {
446
520
  const definition = runtime.manifest.vaults[secretRef.vault ?? "default"];
@@ -485,7 +559,7 @@ async function deleteValue(namespace, configPath, options = {}) {
485
559
  deleted: false
486
560
  };
487
561
  }
488
- await writeFile(filePath, stringifyYaml(document), "utf8");
562
+ await writeFile2(filePath, stringifyYaml(document), "utf8");
489
563
  return {
490
564
  filePath,
491
565
  deleted: true
@@ -495,7 +569,7 @@ async function deleteValue(namespace, configPath, options = {}) {
495
569
  // src/commands/define.ts
496
570
  async function runDefine(namespace, configPath, rawValue, options = {}) {
497
571
  const cliArgs = [...options.cliArgs ?? []];
498
- const root = path3.resolve(options.root ?? process.cwd());
572
+ const root = path4.resolve(options.root ?? process.cwd());
499
573
  const target = consumeOption(cliArgs, "--target") ?? "local";
500
574
  const local = consumeFlag(cliArgs, "--local");
501
575
  const remote = consumeFlag(cliArgs, "--remote");
@@ -524,6 +598,193 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
524
598
  return `defined ${namespace}.${configPath} in ${displayPath(result.filePath, root)}`;
525
599
  }
526
600
 
601
+ // src/services/spawn.ts
602
+ import { spawn } from "child_process";
603
+ function shouldUseShellForCommand(command) {
604
+ if (process.platform !== "win32") {
605
+ return false;
606
+ }
607
+ return !/[\\/]/.test(command);
608
+ }
609
+ function spawnCommand(command, options) {
610
+ const executable = command[0];
611
+ if (!executable) {
612
+ throw new Error("A command is required.");
613
+ }
614
+ return spawn(executable, command.slice(1), {
615
+ cwd: options.cwd,
616
+ env: options.env,
617
+ stdio: options.stdio ?? "inherit",
618
+ shell: shouldUseShellForCommand(executable)
619
+ });
620
+ }
621
+
622
+ // src/services/watchLoop.ts
623
+ import { watch } from "fs";
624
+ import { diffGraphs, watchFiles } from "@kitsy/cnos/internal";
625
+ async function startGraphWatchLoop(options) {
626
+ const debounceMs = options.debounceMs ?? 300;
627
+ let current = await createRuntimeService(options);
628
+ const watcherMap = /* @__PURE__ */ new Map();
629
+ let timer;
630
+ let closed = false;
631
+ const attachWatcher = (targetPath, recursive = false) => {
632
+ if (watcherMap.has(targetPath)) {
633
+ return;
634
+ }
635
+ try {
636
+ const watcher = watch(
637
+ targetPath,
638
+ recursive ? {
639
+ recursive: true
640
+ } : void 0,
641
+ () => {
642
+ if (timer) {
643
+ clearTimeout(timer);
644
+ }
645
+ timer = setTimeout(() => {
646
+ void handleChange();
647
+ }, debounceMs);
648
+ }
649
+ );
650
+ watcherMap.set(targetPath, watcher);
651
+ } catch {
652
+ if (recursive) {
653
+ attachWatcher(targetPath, false);
654
+ }
655
+ }
656
+ };
657
+ const refreshWatchers = async () => {
658
+ const nextTargets = await watchFiles(current, options.root);
659
+ attachWatcher(nextTargets.manifestPath, false);
660
+ for (const workspaceRoot of nextTargets.roots) {
661
+ attachWatcher(workspaceRoot, true);
662
+ }
663
+ for (const filePath of nextTargets.files) {
664
+ attachWatcher(filePath, false);
665
+ }
666
+ };
667
+ const handleChange = async () => {
668
+ if (closed) {
669
+ return;
670
+ }
671
+ const next = await createRuntimeService(options);
672
+ const changedKeys = diffGraphs(current.graph, next.graph);
673
+ current = next;
674
+ await refreshWatchers();
675
+ if (changedKeys.length === 0) {
676
+ return;
677
+ }
678
+ await options.onChange?.({
679
+ runtime: next,
680
+ changedKeys
681
+ });
682
+ };
683
+ await refreshWatchers();
684
+ return {
685
+ async close() {
686
+ closed = true;
687
+ if (timer) {
688
+ clearTimeout(timer);
689
+ }
690
+ for (const watcher of watcherMap.values()) {
691
+ watcher.close();
692
+ }
693
+ watcherMap.clear();
694
+ }
695
+ };
696
+ }
697
+
698
+ // src/commands/dev.ts
699
+ async function startDevEnvLoop(command, options = {}) {
700
+ if (command.length === 0) {
701
+ throw new Error("dev env requires a command after --");
702
+ }
703
+ const cliArgs = [...options.cliArgs ?? []];
704
+ const to = consumeOption(cliArgs, "--to");
705
+ const isSignal = consumeFlag(cliArgs, "--signal");
706
+ const debounceMs = Number(consumeOption(cliArgs, "--debounce") ?? "300");
707
+ if (!to) {
708
+ throw new Error("dev env requires --to <path>");
709
+ }
710
+ const root = options.root ?? process.cwd();
711
+ let child;
712
+ const writeCurrent = async () => {
713
+ await materializeEnvToFile(to, {
714
+ ...options,
715
+ cliArgs: [...cliArgs]
716
+ });
717
+ };
718
+ await writeCurrent();
719
+ if (!isSignal) {
720
+ child = spawnCommand(command, {
721
+ cwd: root,
722
+ env: process.env,
723
+ stdio: "inherit"
724
+ });
725
+ }
726
+ const watcher = await startGraphWatchLoop({
727
+ ...options,
728
+ cliArgs,
729
+ debounceMs,
730
+ async onChange(payload) {
731
+ await writeCurrent();
732
+ if (isSignal) {
733
+ process.stdout.write(`${printJson({ changedKeys: payload.changedKeys })}
734
+ `);
735
+ return;
736
+ }
737
+ if (child && !child.killed) {
738
+ await new Promise((resolve) => {
739
+ child?.once("close", () => resolve());
740
+ child?.kill();
741
+ });
742
+ }
743
+ child = spawnCommand(command, {
744
+ cwd: root,
745
+ env: process.env,
746
+ stdio: "inherit"
747
+ });
748
+ }
749
+ });
750
+ return {
751
+ async close() {
752
+ await watcher.close();
753
+ if (child && !child.killed) {
754
+ await new Promise((resolve) => {
755
+ child?.once("close", () => resolve());
756
+ child?.kill();
757
+ });
758
+ }
759
+ }
760
+ };
761
+ }
762
+ async function runDev(subcommand, command, options = {}) {
763
+ if (subcommand !== "env") {
764
+ throw new Error(`Unsupported dev target: ${subcommand ?? "(missing)"}`);
765
+ }
766
+ const cliArgs = [...options.cliArgs ?? []];
767
+ const to = consumeOption(cliArgs, "--to");
768
+ const isSignal = consumeFlag(cliArgs, "--signal");
769
+ if (!to) {
770
+ throw new Error("dev env requires --to <path>");
771
+ }
772
+ if (command.length === 0) {
773
+ throw new Error("dev env requires a command after --");
774
+ }
775
+ const handle = await startDevEnvLoop(command, {
776
+ ...options,
777
+ cliArgs
778
+ });
779
+ const closeLoop = () => {
780
+ void handle.close();
781
+ };
782
+ process.once("SIGINT", closeLoop);
783
+ process.once("SIGTERM", closeLoop);
784
+ const targetPath = displayPath(to, options.root ?? process.cwd());
785
+ return isSignal ? `watching config changes and rewriting ${targetPath} in signal mode` : `watching config changes, rewriting ${targetPath}, and restarting the child process`;
786
+ }
787
+
527
788
  // src/commands/drift.ts
528
789
  import { compareSchemaToGraph, formatDriftReport } from "@kitsy/cnos/internal";
529
790
  async function runDrift(options = {}) {
@@ -574,7 +835,7 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
574
835
 
575
836
  // src/services/doctor.ts
576
837
  import { readdir, readFile as readFile2 } from "fs/promises";
577
- import path4 from "path";
838
+ import path5 from "path";
578
839
  import {
579
840
  detectLegacyVaultFormat,
580
841
  isSecretReference as isSecretReference2,
@@ -596,7 +857,7 @@ async function createValidationSummary(options = {}) {
596
857
 
597
858
  // src/services/doctor.ts
598
859
  async function checkGitignore(root) {
599
- const gitignorePath = path4.join(root, ".gitignore");
860
+ const gitignorePath = path5.join(root, ".gitignore");
600
861
  const expected = [
601
862
  ".cnos/env/.env",
602
863
  ".cnos/env/.env.*",
@@ -631,12 +892,12 @@ async function collectYamlFiles(root) {
631
892
  const entries = await readdir(root, { withFileTypes: true });
632
893
  const results = [];
633
894
  for (const entry of entries) {
634
- const target = path4.join(root, entry.name);
895
+ const target = path5.join(root, entry.name);
635
896
  if (entry.isDirectory()) {
636
897
  results.push(...await collectYamlFiles(target));
637
898
  continue;
638
899
  }
639
- if (entry.isFile() && [".yml", ".yaml"].includes(path4.extname(entry.name).toLowerCase())) {
900
+ if (entry.isFile() && [".yml", ".yaml"].includes(path5.extname(entry.name).toLowerCase())) {
640
901
  results.push(target);
641
902
  }
642
903
  }
@@ -661,7 +922,7 @@ async function checkSecretSecurity(options, runtime) {
661
922
  );
662
923
  const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
663
924
  const secretFiles = await Promise.all(
664
- runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path4.join(root.path, "secrets")))
925
+ runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path5.join(root.path, "secrets")))
665
926
  );
666
927
  const plaintextFiles = [];
667
928
  for (const file of secretFiles.flat()) {
@@ -691,7 +952,7 @@ async function checkSecretSecurity(options, runtime) {
691
952
  };
692
953
  }
693
954
  async function evaluateDoctor(options = {}) {
694
- const root = path4.resolve(options.root ?? process.cwd());
955
+ const root = path5.resolve(options.root ?? process.cwd());
695
956
  const { runtime, summary } = await createValidationSummary(options);
696
957
  const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
697
958
  const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
@@ -811,44 +1072,33 @@ async function runCodegen(options = {}) {
811
1072
  }
812
1073
 
813
1074
  // src/commands/exportEnv.ts
814
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
815
- import path5 from "path";
816
- function formatEnvOutput(env) {
817
- return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
818
- }
819
1075
  async function runExportEnv(options = {}) {
820
- const cliArgs = [...options.cliArgs ?? []];
821
- const isPublic = consumeFlag(cliArgs, "--public");
822
- const framework = consumeOption(cliArgs, "--framework");
823
- const prefix = consumeOption(cliArgs, "--prefix");
824
- const to = consumeOption(cliArgs, "--to");
825
- const runtime = await createRuntimeService({
1076
+ const infoArgs = [...options.cliArgs ?? []];
1077
+ const isPublic = consumeFlag(infoArgs, "--public");
1078
+ const framework = consumeOption(infoArgs, "--framework");
1079
+ consumeOption(infoArgs, "--prefix");
1080
+ const to = consumeOption(infoArgs, "--to");
1081
+ const baseOptions = {
826
1082
  ...options,
827
- cliArgs
828
- });
829
- const env = isPublic ? runtime.toPublicEnv({
830
- ...framework ? { framework } : {},
831
- ...prefix ? { prefix } : {}
832
- }) : runtime.toEnv();
833
- const output = formatEnvOutput(env);
1083
+ cliArgs: [...options.cliArgs ?? []]
1084
+ };
834
1085
  if (to) {
835
- const targetPath = path5.resolve(options.root ?? process.cwd(), to);
836
- await mkdir2(path5.dirname(targetPath), { recursive: true });
837
- await writeFile2(targetPath, output, "utf8");
1086
+ const result2 = await materializeEnvToFile(to, baseOptions);
838
1087
  if (options.json) {
839
1088
  return printJson({
840
- to: targetPath,
841
- count: Object.keys(env).length,
1089
+ to: result2.targetPath,
1090
+ count: Object.keys(result2.env).length,
842
1091
  public: isPublic,
843
1092
  ...framework ? { framework } : {}
844
1093
  });
845
1094
  }
846
- return `Wrote ${Object.keys(env).length} env vars to ${targetPath}`;
1095
+ return `Wrote ${Object.keys(result2.env).length} env vars to ${displayPath(result2.targetPath, options.root ?? process.cwd())}`;
847
1096
  }
1097
+ const result = await resolveMaterializedEnv(baseOptions);
848
1098
  if (options.json) {
849
- return printJson(env);
1099
+ return printJson(result.env);
850
1100
  }
851
- return output;
1101
+ return result.output;
852
1102
  }
853
1103
 
854
1104
  // src/commands/export.ts
@@ -1309,6 +1559,52 @@ var COMMANDS = [
1309
1559
  "cnos export env --public --framework next --workspace webapp"
1310
1560
  ]
1311
1561
  },
1562
+ {
1563
+ id: "build",
1564
+ summary: "Build derived configuration artifacts from CNOS.",
1565
+ usage: "cnos build <subcommand> [options] [global-options]",
1566
+ description: "Builds deterministic derived outputs from the selected workspace. Currently supports env artifact generation.",
1567
+ arguments: [
1568
+ {
1569
+ name: "subcommand",
1570
+ description: "Supported value: env.",
1571
+ required: true
1572
+ }
1573
+ ],
1574
+ examples: [
1575
+ "cnos build env --profile local --to .env.local",
1576
+ "cnos build env --public --framework vite --profile prod --to .env.production"
1577
+ ]
1578
+ },
1579
+ {
1580
+ id: "build env",
1581
+ summary: "Build a flat env-file artifact from CNOS.",
1582
+ usage: "cnos build env --to <path> [--public] [--framework <name>] [--prefix <prefix>] [global-options]",
1583
+ description: "Builds a deterministic KEY=VALUE artifact for legacy build and runtime workflows. The target file is derived output, not the CNOS source of truth.",
1584
+ options: [
1585
+ {
1586
+ flag: "--to <path>",
1587
+ description: "Write the rendered KEY=VALUE output to a file. Required."
1588
+ },
1589
+ {
1590
+ flag: "--public",
1591
+ description: "Build only public values based on manifest promotion rules."
1592
+ },
1593
+ {
1594
+ flag: "--framework <name>",
1595
+ description: "Apply framework-specific public env conventions such as vite or next."
1596
+ },
1597
+ {
1598
+ flag: "--prefix <prefix>",
1599
+ description: "Override the generated public env prefix."
1600
+ }
1601
+ ],
1602
+ examples: [
1603
+ "cnos build env --profile local --to .env.local",
1604
+ "cnos build env --profile stage --to .env.stage",
1605
+ "cnos build env --public --framework vite --to .env.local"
1606
+ ]
1607
+ },
1312
1608
  {
1313
1609
  id: "export env",
1314
1610
  summary: "Render environment variables for the selected workspace.",
@@ -1338,6 +1634,60 @@ var COMMANDS = [
1338
1634
  "cnos export env --public --framework vite --to .env.local --workspace api"
1339
1635
  ]
1340
1636
  },
1637
+ {
1638
+ id: "dev",
1639
+ summary: "Run watched CNOS-driven development workflows.",
1640
+ usage: "cnos dev <subcommand> [options] [global-options] -- <command...>",
1641
+ description: "Runs higher-level development workflows that derive config artifacts from CNOS and keep them up to date while a child process is running.",
1642
+ arguments: [
1643
+ {
1644
+ name: "subcommand",
1645
+ description: "Supported value: env.",
1646
+ required: true
1647
+ }
1648
+ ],
1649
+ examples: [
1650
+ "cnos dev env --profile local --to .env.local -- pnpm dev",
1651
+ "cnos dev env --public --framework vite --to .env.local -- pnpm dev"
1652
+ ]
1653
+ },
1654
+ {
1655
+ id: "dev env",
1656
+ summary: "Watch CNOS config, rewrite an env file, and restart a child process.",
1657
+ usage: "cnos dev env --to <path> [--public] [--framework <name>] [--prefix <prefix>] [--debounce <ms>] [--signal] [global-options] -- <command...>",
1658
+ description: "Writes a derived env file before first launch, watches CNOS inputs, rewrites the file on change, and restarts the child process by default.",
1659
+ options: [
1660
+ {
1661
+ flag: "--to <path>",
1662
+ description: "Write the rendered KEY=VALUE output to a file. Required."
1663
+ },
1664
+ {
1665
+ flag: "--public",
1666
+ description: "Build only public values based on manifest promotion rules."
1667
+ },
1668
+ {
1669
+ flag: "--framework <name>",
1670
+ description: "Apply framework-specific public env conventions such as vite or next."
1671
+ },
1672
+ {
1673
+ flag: "--prefix <prefix>",
1674
+ description: "Override the generated public env prefix."
1675
+ },
1676
+ {
1677
+ flag: "--debounce <ms>",
1678
+ description: "Debounce config changes before rebuilding the env artifact. Defaults to 300ms."
1679
+ },
1680
+ {
1681
+ flag: "--signal",
1682
+ description: "Rewrite the env artifact and emit changed keys as JSON instead of restarting the child process."
1683
+ }
1684
+ ],
1685
+ examples: [
1686
+ "cnos dev env --profile local --to .env.local -- pnpm dev",
1687
+ "cnos dev env --profile stage --to .env.stage -- node server.js",
1688
+ "cnos dev env --public --framework vite --to .env.local --signal -- pnpm dev"
1689
+ ]
1690
+ },
1341
1691
  {
1342
1692
  id: "dump",
1343
1693
  summary: "Materialize the selected workspace into files.",
@@ -1542,6 +1892,8 @@ var HELP_DOCUMENT = {
1542
1892
  examples: [
1543
1893
  "cnos use --profile stage",
1544
1894
  "cnos doctor --workspace api",
1895
+ "cnos build env --profile stage --to .env.stage",
1896
+ "cnos dev env --profile local --to .env.local -- pnpm dev",
1545
1897
  "cnos export env --public --framework vite",
1546
1898
  "cnos export env --public --framework next",
1547
1899
  "cnos help-ai --format json"
@@ -2443,7 +2795,6 @@ async function runRead(key, options = {}) {
2443
2795
  }
2444
2796
 
2445
2797
  // src/commands/run.ts
2446
- import { spawn } from "child_process";
2447
2798
  import {
2448
2799
  CNOS_GRAPH_ENV_VAR,
2449
2800
  CNOS_SECRET_PAYLOAD_ENV_VAR,
@@ -2517,11 +2868,10 @@ async function runCommand(command, options = {}) {
2517
2868
  reject(new Error("run requires a command after --"));
2518
2869
  return;
2519
2870
  }
2520
- const child = spawn(executable, command.slice(1), {
2871
+ const child = spawnCommand(command, {
2521
2872
  cwd: options.root ?? process.cwd(),
2522
2873
  env,
2523
- stdio: options.stdio === "pipe" ? "pipe" : "inherit",
2524
- shell: false
2874
+ stdio: options.stdio === "pipe" ? "pipe" : "inherit"
2525
2875
  });
2526
2876
  let stdout = "";
2527
2877
  let stderr = "";
@@ -2993,7 +3343,7 @@ async function runValidate(options = {}) {
2993
3343
  // package.json
2994
3344
  var package_default = {
2995
3345
  name: "@kitsy/cnos-cli",
2996
- version: "1.4.0",
3346
+ version: "1.5.1",
2997
3347
  description: "CLI entry point and developer tooling for CNOS.",
2998
3348
  type: "module",
2999
3349
  main: "./dist/index.js",
@@ -3133,16 +3483,12 @@ async function runValue(argsOrPath, options = {}) {
3133
3483
  }
3134
3484
 
3135
3485
  // src/commands/watch.ts
3136
- import { watch } from "fs";
3137
- import { spawn as spawn2 } from "child_process";
3138
3486
  import {
3139
3487
  CNOS_GRAPH_ENV_VAR as CNOS_GRAPH_ENV_VAR2,
3140
3488
  CNOS_SECRET_PAYLOAD_ENV_VAR as CNOS_SECRET_PAYLOAD_ENV_VAR2,
3141
3489
  CNOS_SESSION_KEY_ENV_VAR as CNOS_SESSION_KEY_ENV_VAR2,
3142
- diffGraphs,
3143
3490
  serializeRuntimeGraph as serializeRuntimeGraph2,
3144
- serializeSecretPayload as serializeSecretPayload2,
3145
- watchFiles
3491
+ serializeSecretPayload as serializeSecretPayload2
3146
3492
  } from "@kitsy/cnos/internal";
3147
3493
  async function buildRunEnvironment(options) {
3148
3494
  const cliArgs = [...options.cliArgs ?? []];
@@ -3179,11 +3525,10 @@ function spawnWatchedChild(command, cwd, env) {
3179
3525
  if (!executable) {
3180
3526
  throw new Error("watch requires a command after -- unless --signal is used");
3181
3527
  }
3182
- return spawn2(executable, command.slice(1), {
3528
+ return spawnCommand(command, {
3183
3529
  cwd,
3184
3530
  env,
3185
- stdio: "inherit",
3186
- shell: false
3531
+ stdio: "inherit"
3187
3532
  });
3188
3533
  }
3189
3534
  async function startWatchLoop(options) {
@@ -3197,85 +3542,39 @@ async function startWatchLoop(options) {
3197
3542
  cliArgs
3198
3543
  });
3199
3544
  let child = !isSignal ? spawnWatchedChild(command, root, current.env) : void 0;
3200
- const watcherMap = /* @__PURE__ */ new Map();
3201
- let timer;
3202
3545
  let closed = false;
3203
- const attachWatcher = (targetPath, recursive = false) => {
3204
- if (watcherMap.has(targetPath)) {
3205
- return;
3206
- }
3207
- try {
3208
- const watcher = watch(
3209
- targetPath,
3210
- recursive ? {
3211
- recursive: true
3212
- } : void 0,
3213
- () => {
3214
- if (timer) {
3215
- clearTimeout(timer);
3216
- }
3217
- timer = setTimeout(() => {
3218
- void handleChange();
3219
- }, debounceMs);
3220
- }
3221
- );
3222
- watcherMap.set(targetPath, watcher);
3223
- } catch {
3224
- if (recursive) {
3225
- attachWatcher(targetPath, false);
3546
+ const watcher = await startGraphWatchLoop({
3547
+ ...options,
3548
+ cliArgs,
3549
+ debounceMs,
3550
+ async onChange(payload) {
3551
+ if (closed) {
3552
+ return;
3226
3553
  }
3227
- }
3228
- };
3229
- const refreshWatchers = async () => {
3230
- const nextTargets = await watchFiles(current.runtime, options.root);
3231
- attachWatcher(nextTargets.manifestPath, false);
3232
- for (const workspaceRoot of nextTargets.roots) {
3233
- attachWatcher(workspaceRoot, true);
3234
- }
3235
- for (const filePath of nextTargets.files) {
3236
- attachWatcher(filePath, false);
3237
- }
3238
- };
3239
- const handleChange = async () => {
3240
- if (closed) {
3241
- return;
3242
- }
3243
- const next = await buildRunEnvironment({
3244
- ...options,
3245
- cliArgs
3246
- });
3247
- const changedKeys = diffGraphs(current.runtime.graph, next.runtime.graph);
3248
- current = next;
3249
- await refreshWatchers();
3250
- if (changedKeys.length === 0) {
3251
- return;
3252
- }
3253
- if (isSignal) {
3254
- await options.onSignal?.({ changedKeys });
3255
- process.stdout.write(`${printJson({ changedKeys })}
3256
- `);
3257
- return;
3258
- }
3259
- if (child && !child.killed) {
3260
- await new Promise((resolve) => {
3261
- child?.once("close", () => resolve());
3262
- child?.kill();
3554
+ current = await buildRunEnvironment({
3555
+ ...options,
3556
+ cliArgs
3263
3557
  });
3558
+ if (isSignal) {
3559
+ await options.onSignal?.({ changedKeys: payload.changedKeys });
3560
+ process.stdout.write(`${printJson({ changedKeys: payload.changedKeys })}
3561
+ `);
3562
+ return;
3563
+ }
3564
+ if (child && !child.killed) {
3565
+ await new Promise((resolve) => {
3566
+ child?.once("close", () => resolve());
3567
+ child?.kill();
3568
+ });
3569
+ }
3570
+ child = spawnWatchedChild(command, root, current.env);
3571
+ await options.onRestart?.({ changedKeys: payload.changedKeys });
3264
3572
  }
3265
- child = spawnWatchedChild(command, root, current.env);
3266
- await options.onRestart?.({ changedKeys });
3267
- };
3268
- await refreshWatchers();
3573
+ });
3269
3574
  return {
3270
3575
  async close() {
3271
3576
  closed = true;
3272
- if (timer) {
3273
- clearTimeout(timer);
3274
- }
3275
- for (const watcher of watcherMap.values()) {
3276
- watcher.close();
3277
- }
3278
- watcherMap.clear();
3577
+ await watcher.close();
3279
3578
  if (child && !child.killed) {
3280
3579
  await new Promise((resolve) => {
3281
3580
  child?.once("close", () => resolve());
@@ -3314,6 +3613,12 @@ function resolveHelpTopic(command, args) {
3314
3613
  if (command === "export" && args[0] === "env") {
3315
3614
  return normalizeHelpTopic([command, args[0]]);
3316
3615
  }
3616
+ if (command === "build" && args[0] === "env") {
3617
+ return normalizeHelpTopic([command, args[0]]);
3618
+ }
3619
+ if (command === "dev" && args[0] === "env") {
3620
+ return normalizeHelpTopic([command, args[0]]);
3621
+ }
3317
3622
  if (command === "vault" && args[0] && ["create", "add", "list", "delete", "remove"].includes(args[0])) {
3318
3623
  return normalizeHelpTopic([command, args[0] === "delete" ? "remove" : args[0] === "add" ? "create" : args[0]]);
3319
3624
  }
@@ -3444,6 +3749,14 @@ async function main(argv) {
3444
3749
  return;
3445
3750
  case "export":
3446
3751
  process.stdout.write(`${await runExport(args[0], runtimeOptions)}
3752
+ `);
3753
+ return;
3754
+ case "build":
3755
+ process.stdout.write(`${await runBuild(args[0], runtimeOptions)}
3756
+ `);
3757
+ return;
3758
+ case "dev":
3759
+ process.stdout.write(`${await runDev(args[0], passthrough.length > 0 ? passthrough : args.slice(1), runtimeOptions)}
3447
3760
  `);
3448
3761
  return;
3449
3762
  case "dump":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitsy/cnos-cli",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "CLI entry point and developer tooling for CNOS.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,7 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@kitsy/cnos": "1.4.0"
39
+ "@kitsy/cnos": "1.5.1"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsup src/index.ts --format esm --dts",