@kitsy/cnos-cli 1.5.1 → 1.6.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 +484 -159
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
17
17
  "--provider",
18
18
  "--passphrase",
19
19
  "--vault",
20
+ "--package-root",
20
21
  "--inherit",
21
22
  "--as",
22
23
  "--set",
@@ -250,94 +251,169 @@ function printJson(value) {
250
251
  return JSON.stringify(value, null, 2);
251
252
  }
252
253
 
253
- // src/services/envMaterialization.ts
254
+ // src/services/projections.ts
254
255
  import { mkdir, writeFile } from "fs/promises";
255
256
  import path2 from "path";
257
+ import { resolveBrowserData, resolveFrameworkEnv, resolveServerProjection } from "@kitsy/cnos/build";
258
+ import { stringifyYaml } from "@kitsy/cnos/internal";
256
259
 
257
260
  // src/services/runtime.ts
258
261
  import { createCnos } from "@kitsy/cnos/configure";
259
262
  async function createRuntimeService(options = {}) {
260
263
  return createCnos({
264
+ ...options.cwd ? { cwd: options.cwd } : {},
261
265
  ...options.root ? { root: options.root } : {},
262
266
  ...options.workspace ? { workspace: options.workspace } : {},
263
267
  ...options.profile ? { profile: options.profile } : {},
264
268
  ...options.globalRoot ? { globalRoot: options.globalRoot } : {},
265
269
  ...options.cliArgs && options.cliArgs.length > 0 ? { cliArgs: options.cliArgs } : {},
266
270
  ...options.secretResolution ? { secretResolution: options.secretResolution } : {},
271
+ ...typeof options.secretRefreshTtl === "number" ? { secretRefreshTtl: options.secretRefreshTtl } : {},
267
272
  processEnv: options.processEnv ?? process.env
268
273
  });
269
274
  }
270
275
 
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();
276
+ // src/services/projections.ts
277
+ function stringifyScalar(value) {
278
+ if (value === void 0 || value === null) {
279
+ return "";
280
+ }
281
+ if (typeof value === "string") {
282
+ return value;
283
+ }
284
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
285
+ return String(value);
286
+ }
287
+ return JSON.stringify(value);
281
288
  }
282
- function formatEnvOutput(env) {
283
- return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
289
+ function escapeShell(value) {
290
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
284
291
  }
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
- };
292
+ function quoteToml(value) {
293
+ return `"${escapeShell(value)}"`;
296
294
  }
297
- function resolveMaterializedEnvTarget(to, root = process.cwd()) {
298
- return path2.resolve(root, to);
295
+ function formatKeyValueMap(values, format) {
296
+ const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right));
297
+ switch (format) {
298
+ case "json":
299
+ return `${JSON.stringify(Object.fromEntries(entries), null, 2)}
300
+ `;
301
+ case "yaml":
302
+ return stringifyYaml(Object.fromEntries(entries));
303
+ case "shell":
304
+ return entries.map(([key, value]) => `export ${key}="${escapeShell(stringifyScalar(value))}"`).join("\n");
305
+ case "toml":
306
+ return entries.map(([key, value]) => `${key} = ${quoteToml(stringifyScalar(value))}`).join("\n");
307
+ case "docker-env":
308
+ case "dotenv":
309
+ default:
310
+ return entries.map(([key, value]) => `${key}=${stringifyScalar(value)}`).join("\n");
311
+ }
299
312
  }
300
- async function writeMaterializedEnvFile(to, output, root = process.cwd()) {
301
- const targetPath = resolveMaterializedEnvTarget(to, root);
313
+ async function writeProjectionFile(to, output, root = process.cwd()) {
314
+ const targetPath = path2.resolve(root, to);
302
315
  await mkdir(path2.dirname(targetPath), { recursive: true });
303
316
  await writeFile(targetPath, output, "utf8");
304
317
  return targetPath;
305
318
  }
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
- };
319
+ async function buildServerProjectionArtifact(to, options = {}, format = "json") {
320
+ const projection = await resolveServerProjection(options);
321
+ const output = format === "yaml" ? stringifyYaml(projection) : `${JSON.stringify(projection, null, 2)}
322
+ `;
323
+ const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
324
+ return { targetPath, output };
325
+ }
326
+ async function buildBrowserProjectionArtifact(to, options = {}, format = "json") {
327
+ const projection = await resolveBrowserData(options);
328
+ const output = format === "yaml" ? stringifyYaml(projection) : `${JSON.stringify(projection, null, 2)}
329
+ `;
330
+ const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
331
+ return { targetPath, output };
332
+ }
333
+ async function buildPublicProjectionArtifact(to, options = {}, format = "dotenv") {
334
+ const cliArgs = [...options.cliArgs ?? []];
335
+ const frameworkIndex = cliArgs.indexOf("--framework");
336
+ const framework = frameworkIndex >= 0 && cliArgs[frameworkIndex + 1] ? cliArgs[frameworkIndex + 1] : "generic";
337
+ const env = await resolveFrameworkEnv(options, framework);
338
+ const output = formatKeyValueMap(env, format);
339
+ const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
340
+ return { targetPath, output, env };
341
+ }
342
+ async function buildEnvProjectionArtifact(to, options = {}, format = "dotenv") {
343
+ const runtime = await createRuntimeService({
344
+ ...options,
345
+ cliArgs: [...options.cliArgs ?? []]
346
+ });
347
+ const env = runtime.toEnv();
348
+ for (const [envVar, logicalKey] of Object.entries(runtime.manifest.envMapping.explicit)) {
349
+ const entry = runtime.graph.entries.get(logicalKey);
350
+ if (entry?.namespace === "secret" && !(envVar in env)) {
351
+ env[envVar] = "****";
352
+ }
353
+ }
354
+ const output = formatKeyValueMap(env, format);
355
+ const targetPath = await writeProjectionFile(to, output, options.root ?? process.cwd());
356
+ return { targetPath, output, env };
313
357
  }
314
358
 
315
359
  // src/commands/build.ts
316
360
  async function runBuild(subcommand, options = {}) {
317
- if (subcommand !== "env") {
318
- throw new Error(`Unsupported build target: ${subcommand ?? "(missing)"}`);
319
- }
320
361
  const infoArgs = [...options.cliArgs ?? []];
362
+ const format = consumeOption(infoArgs, "--format") ?? void 0;
321
363
  const isPublic = consumeFlag(infoArgs, "--public");
322
364
  const framework = consumeOption(infoArgs, "--framework");
323
365
  consumeOption(infoArgs, "--prefix");
324
366
  const to = consumeOption(infoArgs, "--to");
367
+ const provenanceTarget = consumeOption(infoArgs, "--with-provenance");
325
368
  if (!to) {
326
- throw new Error("build env requires --to <path>");
369
+ throw new Error(`build ${subcommand ?? "(missing)"} requires --to <path>`);
370
+ }
371
+ let targetPath;
372
+ let count = 0;
373
+ switch (subcommand) {
374
+ case "env": {
375
+ const result = await buildEnvProjectionArtifact(to, {
376
+ ...options,
377
+ cliArgs: [...options.cliArgs ?? []]
378
+ }, format ?? "dotenv");
379
+ targetPath = result.targetPath;
380
+ count = Object.keys(result.env).length;
381
+ break;
382
+ }
383
+ case "public": {
384
+ const result = await buildPublicProjectionArtifact(to, {
385
+ ...options,
386
+ cliArgs: [...options.cliArgs ?? []]
387
+ }, format ?? "dotenv");
388
+ targetPath = result.targetPath;
389
+ count = Object.keys(result.env).length;
390
+ break;
391
+ }
392
+ case "server": {
393
+ const result = await buildServerProjectionArtifact(to, options, format ?? "json");
394
+ targetPath = result.targetPath;
395
+ count = 1;
396
+ break;
397
+ }
398
+ case "browser": {
399
+ const result = await buildBrowserProjectionArtifact(to, options, format ?? "json");
400
+ targetPath = result.targetPath;
401
+ count = 1;
402
+ break;
403
+ }
404
+ default:
405
+ throw new Error(`Unsupported build target: ${subcommand ?? "(missing)"}`);
327
406
  }
328
- const result = await materializeEnvToFile(to, {
329
- ...options,
330
- cliArgs: [...options.cliArgs ?? []]
331
- });
332
407
  if (options.json) {
333
408
  return printJson({
334
- to: result.targetPath,
335
- count: Object.keys(result.env).length,
409
+ to: targetPath,
410
+ count,
336
411
  public: isPublic,
337
- ...framework ? { framework } : {}
412
+ ...framework ? { framework } : {},
413
+ ...provenanceTarget ? { provenance: provenanceTarget } : {}
338
414
  });
339
415
  }
340
- return `built env artifact at ${displayPath(result.targetPath, options.root ?? process.cwd())}`;
416
+ return `built ${subcommand} artifact at ${displayPath(targetPath, options.root ?? process.cwd())}`;
341
417
  }
342
418
 
343
419
  // src/commands/define.ts
@@ -352,7 +428,7 @@ import {
352
428
  parseYaml,
353
429
  resolveConfigDocumentPath,
354
430
  resolveVaultAuth,
355
- stringifyYaml
431
+ stringifyYaml as stringifyYaml2
356
432
  } from "@kitsy/cnos/internal";
357
433
  function setNestedValue(target, pathSegments, value) {
358
434
  const [head, ...tail] = pathSegments;
@@ -454,7 +530,7 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
454
530
  const parsedValue = parseScalarValue(rawValue);
455
531
  setNestedValue(document, configPath.split("."), parsedValue);
456
532
  await mkdir2(path3.dirname(filePath), { recursive: true });
457
- await writeFile2(filePath, stringifyYaml(document), "utf8");
533
+ await writeFile2(filePath, stringifyYaml2(document), "utf8");
458
534
  return {
459
535
  filePath,
460
536
  value: parsedValue
@@ -492,7 +568,7 @@ async function setSecret(configPath, rawValue, options = {}) {
492
568
  }
493
569
  setNestedValue(document, configPath.split("."), reference);
494
570
  await mkdir2(path3.dirname(filePath), { recursive: true });
495
- await writeFile2(filePath, stringifyYaml(document), "utf8");
571
+ await writeFile2(filePath, stringifyYaml2(document), "utf8");
496
572
  return {
497
573
  filePath,
498
574
  provider: reference.provider,
@@ -514,7 +590,7 @@ async function deleteSecret(configPath, options = {}) {
514
590
  deleted: false
515
591
  };
516
592
  }
517
- await writeFile2(filePath, stringifyYaml(document), "utf8");
593
+ await writeFile2(filePath, stringifyYaml2(document), "utf8");
518
594
  const secretRef = metadata?.secretRef;
519
595
  if (isSecretReference(secretRef) && secretRef.provider === "local") {
520
596
  const definition = runtime.manifest.vaults[secretRef.vault ?? "default"];
@@ -559,7 +635,7 @@ async function deleteValue(namespace, configPath, options = {}) {
559
635
  deleted: false
560
636
  };
561
637
  }
562
- await writeFile2(filePath, stringifyYaml(document), "utf8");
638
+ await writeFile2(filePath, stringifyYaml2(document), "utf8");
563
639
  return {
564
640
  filePath,
565
641
  deleted: true
@@ -598,6 +674,52 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
598
674
  return `defined ${namespace}.${configPath} in ${displayPath(result.filePath, root)}`;
599
675
  }
600
676
 
677
+ // src/services/envMaterialization.ts
678
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
679
+ import path5 from "path";
680
+ function resolveEnvFromRuntime(runtime, cliArgs = []) {
681
+ const args = [...cliArgs];
682
+ const isPublic = consumeFlag(args, "--public");
683
+ const framework = consumeOption(args, "--framework");
684
+ const prefix = consumeOption(args, "--prefix");
685
+ return isPublic ? runtime.toPublicEnv({
686
+ ...framework ? { framework } : {},
687
+ ...prefix ? { prefix } : {}
688
+ }) : runtime.toEnv();
689
+ }
690
+ function formatEnvOutput(env) {
691
+ return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
692
+ }
693
+ async function resolveMaterializedEnv(options = {}) {
694
+ const runtime = await createRuntimeService({
695
+ ...options,
696
+ cliArgs: [...options.cliArgs ?? []]
697
+ });
698
+ const env = resolveEnvFromRuntime(runtime, options.cliArgs ?? []);
699
+ return {
700
+ runtime,
701
+ env,
702
+ output: formatEnvOutput(env)
703
+ };
704
+ }
705
+ function resolveMaterializedEnvTarget(to, root = process.cwd()) {
706
+ return path5.resolve(root, to);
707
+ }
708
+ async function writeMaterializedEnvFile(to, output, root = process.cwd()) {
709
+ const targetPath = resolveMaterializedEnvTarget(to, root);
710
+ await mkdir3(path5.dirname(targetPath), { recursive: true });
711
+ await writeFile3(targetPath, output, "utf8");
712
+ return targetPath;
713
+ }
714
+ async function materializeEnvToFile(to, options = {}) {
715
+ const result = await resolveMaterializedEnv(options);
716
+ const targetPath = await writeMaterializedEnvFile(to, result.output, options.root ?? process.cwd());
717
+ return {
718
+ ...result,
719
+ targetPath
720
+ };
721
+ }
722
+
601
723
  // src/services/spawn.ts
602
724
  import { spawn } from "child_process";
603
725
  function shouldUseShellForCommand(command) {
@@ -835,7 +957,7 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
835
957
 
836
958
  // src/services/doctor.ts
837
959
  import { readdir, readFile as readFile2 } from "fs/promises";
838
- import path5 from "path";
960
+ import path6 from "path";
839
961
  import {
840
962
  detectLegacyVaultFormat,
841
963
  isSecretReference as isSecretReference2,
@@ -857,7 +979,7 @@ async function createValidationSummary(options = {}) {
857
979
 
858
980
  // src/services/doctor.ts
859
981
  async function checkGitignore(root) {
860
- const gitignorePath = path5.join(root, ".gitignore");
982
+ const gitignorePath = path6.join(root, ".gitignore");
861
983
  const expected = [
862
984
  ".cnos/env/.env",
863
985
  ".cnos/env/.env.*",
@@ -892,12 +1014,12 @@ async function collectYamlFiles(root) {
892
1014
  const entries = await readdir(root, { withFileTypes: true });
893
1015
  const results = [];
894
1016
  for (const entry of entries) {
895
- const target = path5.join(root, entry.name);
1017
+ const target = path6.join(root, entry.name);
896
1018
  if (entry.isDirectory()) {
897
1019
  results.push(...await collectYamlFiles(target));
898
1020
  continue;
899
1021
  }
900
- if (entry.isFile() && [".yml", ".yaml"].includes(path5.extname(entry.name).toLowerCase())) {
1022
+ if (entry.isFile() && [".yml", ".yaml"].includes(path6.extname(entry.name).toLowerCase())) {
901
1023
  results.push(target);
902
1024
  }
903
1025
  }
@@ -922,7 +1044,7 @@ async function checkSecretSecurity(options, runtime) {
922
1044
  );
923
1045
  const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
924
1046
  const secretFiles = await Promise.all(
925
- runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path5.join(root.path, "secrets")))
1047
+ runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path6.join(root.path, "secrets")))
926
1048
  );
927
1049
  const plaintextFiles = [];
928
1050
  for (const file of secretFiles.flat()) {
@@ -952,7 +1074,7 @@ async function checkSecretSecurity(options, runtime) {
952
1074
  };
953
1075
  }
954
1076
  async function evaluateDoctor(options = {}) {
955
- const root = path5.resolve(options.root ?? process.cwd());
1077
+ const root = path6.resolve(options.root ?? process.cwd());
956
1078
  const { runtime, summary } = await createValidationSummary(options);
957
1079
  const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
958
1080
  const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
@@ -1145,7 +1267,7 @@ var COMMANDS = [
1145
1267
  id: "init",
1146
1268
  summary: "Scaffold a workspace-aware CNOS tree in the current project.",
1147
1269
  usage: "cnos init [--workspace <id>] [--root <path>] [--json]",
1148
- description: "Creates .cnos/cnos.yml, optional .cnos-workspace.yml, config folders, and .gitignore entries without overwriting existing files.",
1270
+ description: "Creates .cnos/cnos.yml, .cnosrc.yml, optional .cnos-workspace.yml, config folders, and .gitignore entries without overwriting existing files.",
1149
1271
  examples: ["cnos init", "cnos init --workspace api", "cnos init --root ./apps/api --workspace api --json"]
1150
1272
  },
1151
1273
  {
@@ -1563,23 +1685,39 @@ var COMMANDS = [
1563
1685
  id: "build",
1564
1686
  summary: "Build derived configuration artifacts from CNOS.",
1565
1687
  usage: "cnos build <subcommand> [options] [global-options]",
1566
- description: "Builds deterministic derived outputs from the selected workspace. Currently supports env artifact generation.",
1688
+ description: "Builds deterministic derived outputs from the selected workspace, including server projections, browser projections, env files, and framework-prefixed public env.",
1567
1689
  arguments: [
1568
1690
  {
1569
1691
  name: "subcommand",
1570
- description: "Supported value: env.",
1692
+ description: "Supported values: server, browser, env, public.",
1571
1693
  required: true
1572
1694
  }
1573
1695
  ],
1574
1696
  examples: [
1697
+ "cnos build server --to .cnos-server.json",
1698
+ "cnos build browser --to .cnos-browser.json",
1575
1699
  "cnos build env --profile local --to .env.local",
1576
- "cnos build env --public --framework vite --profile prod --to .env.production"
1700
+ "cnos build public --framework vite --profile prod --to .env.production"
1577
1701
  ]
1578
1702
  },
1703
+ {
1704
+ id: "build server",
1705
+ summary: "Build a server runtime projection artifact.",
1706
+ usage: "cnos build server --to <path> [--format <json|yaml>] [global-options]",
1707
+ description: "Builds a flat server projection for runtime auto-loading. Non-secret values are embedded, while secret refs remain refs and hydrate at runtime.",
1708
+ examples: ["cnos build server --to .cnos-server.json", "cnos build server --profile prod --to dist/.cnos-server.json"]
1709
+ },
1710
+ {
1711
+ id: "build browser",
1712
+ summary: "Build a browser projection artifact.",
1713
+ usage: "cnos build browser --to <path> [--format <json|yaml>] [global-options]",
1714
+ description: "Builds a public-only browser projection for tooling and offline packaging flows. secret.* keys are excluded entirely.",
1715
+ examples: ["cnos build browser --to .cnos-browser.json"]
1716
+ },
1579
1717
  {
1580
1718
  id: "build env",
1581
1719
  summary: "Build a flat env-file artifact from CNOS.",
1582
- usage: "cnos build env --to <path> [--public] [--framework <name>] [--prefix <prefix>] [global-options]",
1720
+ usage: "cnos build env --to <path> [--format <dotenv|docker-env|json|shell|toml|yaml>] [global-options]",
1583
1721
  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
1722
  options: [
1585
1723
  {
@@ -1587,24 +1725,23 @@ var COMMANDS = [
1587
1725
  description: "Write the rendered KEY=VALUE output to a file. Required."
1588
1726
  },
1589
1727
  {
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."
1728
+ flag: "--format <dotenv|docker-env|json|shell|toml|yaml>",
1729
+ description: "Select the output format. Defaults to dotenv."
1600
1730
  }
1601
1731
  ],
1602
1732
  examples: [
1603
1733
  "cnos build env --profile local --to .env.local",
1604
1734
  "cnos build env --profile stage --to .env.stage",
1605
- "cnos build env --public --framework vite --to .env.local"
1735
+ "cnos build env --profile prod --format yaml --to env.yaml"
1606
1736
  ]
1607
1737
  },
1738
+ {
1739
+ id: "build public",
1740
+ summary: "Build a public env artifact with optional framework prefixing.",
1741
+ usage: "cnos build public --to <path> [--framework <name>] [--format <dotenv|docker-env|json|shell|toml|yaml>] [global-options]",
1742
+ description: "Builds env-style public artifacts from promoted keys only, with framework-specific prefixes such as vite or next when requested.",
1743
+ examples: ["cnos build public --framework vite --to .env.vite", "cnos build public --framework next --format json --to public.json"]
1744
+ },
1608
1745
  {
1609
1746
  id: "export env",
1610
1747
  summary: "Render environment variables for the selected workspace.",
@@ -1735,6 +1872,27 @@ var COMMANDS = [
1735
1872
  "cnos run --public --framework vite -- pnpm build"
1736
1873
  ]
1737
1874
  },
1875
+ {
1876
+ id: "workspace",
1877
+ summary: "Attach or detach package-local workspace config from a parent CNOS root.",
1878
+ usage: "cnos workspace <attach|detach> [options] [global-options]",
1879
+ description: "Detaches a child package into a standalone .cnos root or reattaches a detached package back into a parent workspace.",
1880
+ examples: ["cnos workspace detach", "cnos workspace attach --package-root apps/travel"]
1881
+ },
1882
+ {
1883
+ id: "workspace detach",
1884
+ summary: "Detach a package workspace into a standalone .cnos root.",
1885
+ usage: "cnos workspace detach [--package-root <path>] [--force] [global-options]",
1886
+ description: "Materializes the effective local workspace chain into a package-local .cnos directory, rewrites .cnosrc.yml to root: ./.cnos, and records the original parent binding in .cnos/.detached.",
1887
+ examples: ["cnos workspace detach", "cnos workspace detach --package-root apps/travel --force"]
1888
+ },
1889
+ {
1890
+ id: "workspace attach",
1891
+ summary: "Attach a detached package back to its original parent CNOS root.",
1892
+ usage: "cnos workspace attach [--package-root <path>] [--force] [global-options]",
1893
+ description: "Imports a detached package-local .cnos directory back into the original parent workspace, archives the detached snapshot, and restores .cnosrc.yml to the parent root/workspace binding.",
1894
+ examples: ["cnos workspace attach", "cnos workspace attach --package-root apps/travel --force"]
1895
+ },
1738
1896
  {
1739
1897
  id: "diff",
1740
1898
  summary: "Diff two profiles for the same workspace.",
@@ -2007,11 +2165,11 @@ function runHelpAi(topic, cliArgs = []) {
2007
2165
  }
2008
2166
 
2009
2167
  // src/commands/init.ts
2010
- import path7 from "path";
2168
+ import path8 from "path";
2011
2169
 
2012
2170
  // src/services/scaffold.ts
2013
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2014
- import path6 from "path";
2171
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2172
+ import path7 from "path";
2015
2173
  function scaffoldManifest(projectName, workspace) {
2016
2174
  const lines = [
2017
2175
  "version: 1",
@@ -2045,12 +2203,12 @@ async function ensureFile(filePath, content) {
2045
2203
  await readFile3(filePath, "utf8");
2046
2204
  return false;
2047
2205
  } catch {
2048
- await writeFile3(filePath, content, "utf8");
2206
+ await writeFile4(filePath, content, "utf8");
2049
2207
  return true;
2050
2208
  }
2051
2209
  }
2052
2210
  async function ensureGitignore(root) {
2053
- const gitignorePath = path6.join(root, ".gitignore");
2211
+ const gitignorePath = path7.join(root, ".gitignore");
2054
2212
  const requiredEntries = [
2055
2213
  ".cnos/env/.env",
2056
2214
  ".cnos/env/.env.*",
@@ -2073,18 +2231,18 @@ async function ensureGitignore(root) {
2073
2231
  }
2074
2232
  const prefix = current.trim().length > 0 ? `${current.trimEnd()}
2075
2233
  ` : "";
2076
- await writeFile3(gitignorePath, `${prefix}${missingEntries.join("\n")}
2234
+ await writeFile4(gitignorePath, `${prefix}${missingEntries.join("\n")}
2077
2235
  `, "utf8");
2078
2236
  return true;
2079
2237
  }
2080
2238
  async function scaffoldWorkspace(root, workspace) {
2081
- const cnosRoot = path6.join(root, ".cnos");
2082
- const workspaceRoot = workspace ? path6.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2239
+ const cnosRoot = path7.join(root, ".cnos");
2240
+ const workspaceRoot = workspace ? path7.join(cnosRoot, "workspaces", workspace) : cnosRoot;
2083
2241
  const createdPaths = [];
2084
- await mkdir3(path6.join(workspaceRoot, "profiles"), { recursive: true });
2085
- await mkdir3(path6.join(workspaceRoot, "values"), { recursive: true });
2086
- await mkdir3(path6.join(workspaceRoot, "secrets"), { recursive: true });
2087
- await mkdir3(path6.join(workspaceRoot, "env"), { recursive: true });
2242
+ await mkdir4(path7.join(workspaceRoot, "profiles"), { recursive: true });
2243
+ await mkdir4(path7.join(workspaceRoot, "values"), { recursive: true });
2244
+ await mkdir4(path7.join(workspaceRoot, "secrets"), { recursive: true });
2245
+ await mkdir4(path7.join(workspaceRoot, "env"), { recursive: true });
2088
2246
  const relativePaths = workspace ? [
2089
2247
  ["workspaces", workspace, "profiles", ".gitkeep"],
2090
2248
  ["workspaces", workspace, "values", ".gitkeep"],
@@ -2097,15 +2255,23 @@ async function scaffoldWorkspace(root, workspace) {
2097
2255
  ["env", ".gitkeep"]
2098
2256
  ];
2099
2257
  for (const relativePath of relativePaths) {
2100
- const filePath = path6.join(cnosRoot, ...relativePath);
2258
+ const filePath = path7.join(cnosRoot, ...relativePath);
2101
2259
  if (await ensureFile(filePath, "")) {
2102
- createdPaths.push(path6.relative(root, filePath).replace(/\\/g, "/"));
2260
+ createdPaths.push(path7.relative(root, filePath).replace(/\\/g, "/"));
2103
2261
  }
2104
2262
  }
2105
- if (await ensureFile(path6.join(cnosRoot, "cnos.yml"), scaffoldManifest(path6.basename(root), workspace))) {
2263
+ if (await ensureFile(path7.join(cnosRoot, "cnos.yml"), scaffoldManifest(path7.basename(root), workspace))) {
2106
2264
  createdPaths.push(".cnos/cnos.yml");
2107
2265
  }
2108
- if (workspace && await ensureFile(path6.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
2266
+ if (await ensureFile(
2267
+ path7.join(root, ".cnosrc.yml"),
2268
+ workspace ? `root: ./.cnos
2269
+ workspace: ${workspace}
2270
+ ` : "root: ./.cnos\n"
2271
+ )) {
2272
+ createdPaths.push(".cnosrc.yml");
2273
+ }
2274
+ if (workspace && await ensureFile(path7.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
2109
2275
  globalRoot: ~/.cnos
2110
2276
  `)) {
2111
2277
  createdPaths.push(".cnos-workspace.yml");
@@ -2122,7 +2288,7 @@ globalRoot: ~/.cnos
2122
2288
 
2123
2289
  // src/commands/init.ts
2124
2290
  async function runInit(options = {}) {
2125
- const root = path7.resolve(options.root ?? process.cwd());
2291
+ const root = path8.resolve(options.root ?? process.cwd());
2126
2292
  const result = await scaffoldWorkspace(root, options.workspace);
2127
2293
  if (options.json) {
2128
2294
  return printJson(result);
@@ -2317,7 +2483,7 @@ async function runList(args = [], options = {}) {
2317
2483
  }
2318
2484
 
2319
2485
  // src/commands/migrate.ts
2320
- import path8 from "path";
2486
+ import path9 from "path";
2321
2487
  import {
2322
2488
  applyManifestMappings,
2323
2489
  loadManifest,
@@ -2335,7 +2501,7 @@ async function runMigrate(options = {}) {
2335
2501
  throw new Error(`Unknown migrate options: ${cliArgs.join(" ")}`);
2336
2502
  }
2337
2503
  const manifest = await loadManifest(options.root ? { root: options.root } : {});
2338
- const scanRoot = path8.resolve(manifest.repoRoot, scan ?? "src");
2504
+ const scanRoot = path9.resolve(manifest.repoRoot, scan ?? "src");
2339
2505
  const usages = await scanEnvUsage(scanRoot);
2340
2506
  const uniqueProposals = new Map(usages.map((usage) => [usage.envVar, proposeMapping(usage.envVar)]));
2341
2507
  const proposals = Array.from(uniqueProposals.values()).sort((left, right) => left.envVar.localeCompare(right.envVar));
@@ -2397,7 +2563,7 @@ async function runMigrate(options = {}) {
2397
2563
  }
2398
2564
 
2399
2565
  // src/commands/namespace.ts
2400
- import path9 from "path";
2566
+ import path10 from "path";
2401
2567
  function normalizeCommand2(args) {
2402
2568
  const [actionOrPath, ...tail] = args;
2403
2569
  if (!actionOrPath) {
@@ -2420,7 +2586,7 @@ function normalizeCommand2(args) {
2420
2586
  async function runNamespace(namespace, args = [], options = {}) {
2421
2587
  const { action, tail } = normalizeCommand2(args);
2422
2588
  const cliArgs = [...options.cliArgs ?? []];
2423
- const root = path9.resolve(options.root ?? process.cwd());
2589
+ const root = path10.resolve(options.root ?? process.cwd());
2424
2590
  if (action === "list") {
2425
2591
  const prefix = consumeOption(cliArgs, "--prefix");
2426
2592
  const entries = await listConfigEntries(namespace, {
@@ -2483,31 +2649,31 @@ async function runNamespace(namespace, args = [], options = {}) {
2483
2649
 
2484
2650
  // src/commands/onboard.ts
2485
2651
  import { copyFile, readdir as readdir2, rm } from "fs/promises";
2486
- import path10 from "path";
2652
+ import path11 from "path";
2487
2653
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
2488
2654
  async function listRootEnvFiles(root) {
2489
2655
  const entries = await readdir2(root, { withFileTypes: true });
2490
2656
  return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
2491
2657
  }
2492
2658
  async function runOnboard(options = {}) {
2493
- const root = path10.resolve(options.root ?? process.cwd());
2494
- const workspace = options.workspace ?? path10.basename(root);
2659
+ const root = path11.resolve(options.root ?? process.cwd());
2660
+ const workspace = options.workspace ?? path11.basename(root);
2495
2661
  const cliArgs = [...options.cliArgs ?? []];
2496
2662
  const move = consumeFlag(cliArgs, "--move");
2497
2663
  if (cliArgs.length > 0) {
2498
2664
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
2499
2665
  }
2500
2666
  const scaffold = await scaffoldWorkspace(root, workspace);
2501
- const envRoot = path10.join(root, ".cnos", "workspaces", workspace, "env");
2667
+ const envRoot = path11.join(root, ".cnos", "workspaces", workspace, "env");
2502
2668
  const rootFiles = await listRootEnvFiles(root);
2503
2669
  const imported = [];
2504
2670
  const skipped = [];
2505
2671
  for (const fileName of rootFiles) {
2506
- const sourcePath = path10.join(root, fileName);
2507
- const targetPath = path10.join(envRoot, fileName);
2672
+ const sourcePath = path11.join(root, fileName);
2673
+ const targetPath = path11.join(envRoot, fileName);
2508
2674
  try {
2509
2675
  await copyFile(sourcePath, targetPath);
2510
- imported.push(path10.relative(root, targetPath).replace(/\\/g, "/"));
2676
+ imported.push(path11.relative(root, targetPath).replace(/\\/g, "/"));
2511
2677
  if (move) {
2512
2678
  await rm(sourcePath);
2513
2679
  }
@@ -2532,14 +2698,14 @@ async function runOnboard(options = {}) {
2532
2698
  }
2533
2699
 
2534
2700
  // src/commands/profile.ts
2535
- import path13 from "path";
2701
+ import path14 from "path";
2536
2702
 
2537
2703
  // src/services/context.ts
2538
- import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2539
- import path11 from "path";
2540
- import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml2 } from "@kitsy/cnos/internal";
2704
+ import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
2705
+ import path12 from "path";
2706
+ import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
2541
2707
  async function loadCliContext(root = process.cwd()) {
2542
- const filePath = path11.join(path11.resolve(root), ".cnos-workspace.yml");
2708
+ const filePath = path12.join(path12.resolve(root), ".cnos-workspace.yml");
2543
2709
  try {
2544
2710
  const source = await readFile4(filePath, "utf8");
2545
2711
  const parsed = parseYaml3(source);
@@ -2552,8 +2718,8 @@ async function loadCliContext(root = process.cwd()) {
2552
2718
  }
2553
2719
  }
2554
2720
  async function saveCliContext(options = {}) {
2555
- const root = path11.resolve(options.root ?? process.cwd());
2556
- const filePath = path11.join(root, ".cnos-workspace.yml");
2721
+ const root = path12.resolve(options.root ?? process.cwd());
2722
+ const filePath = path12.join(root, ".cnos-workspace.yml");
2557
2723
  const current = await loadCliContext(root);
2558
2724
  const next = {
2559
2725
  ...current.workspace ? { workspace: current.workspace } : {},
@@ -2563,7 +2729,7 @@ async function saveCliContext(options = {}) {
2563
2729
  ...options.profile ? { profile: options.profile } : {},
2564
2730
  ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
2565
2731
  };
2566
- await writeFile4(filePath, stringifyYaml2(next), "utf8");
2732
+ await writeFile5(filePath, stringifyYaml3(next), "utf8");
2567
2733
  return {
2568
2734
  filePath,
2569
2735
  context: next
@@ -2571,12 +2737,12 @@ async function saveCliContext(options = {}) {
2571
2737
  }
2572
2738
 
2573
2739
  // src/services/profiles.ts
2574
- import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
2575
- import path12 from "path";
2576
- import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
2740
+ import { mkdir as mkdir5, readdir as readdir3, readFile as readFile5, rm as rm2, writeFile as writeFile6 } from "fs/promises";
2741
+ import path13 from "path";
2742
+ import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
2577
2743
  async function createProfileDefinition(root = process.cwd(), profile, inherit, options = {}) {
2578
- const filePath = path12.join(path12.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2579
- await mkdir4(path12.dirname(filePath), { recursive: true });
2744
+ const filePath = path13.join(path13.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2745
+ await mkdir5(path13.dirname(filePath), { recursive: true });
2580
2746
  const document = options.noInherit ? {
2581
2747
  name: profile,
2582
2748
  activate: {
@@ -2590,7 +2756,7 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit, o
2590
2756
  } : {
2591
2757
  name: profile
2592
2758
  };
2593
- await writeFile5(filePath, stringifyYaml3(document), "utf8");
2759
+ await writeFile6(filePath, stringifyYaml4(document), "utf8");
2594
2760
  return {
2595
2761
  filePath,
2596
2762
  profile,
@@ -2599,7 +2765,7 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit, o
2599
2765
  };
2600
2766
  }
2601
2767
  async function listProfiles(root = process.cwd()) {
2602
- const profilesRoot = path12.join(path12.resolve(root), ".cnos", "profiles");
2768
+ const profilesRoot = path13.join(path13.resolve(root), ".cnos", "profiles");
2603
2769
  try {
2604
2770
  const entries = await readdir3(profilesRoot, { withFileTypes: true });
2605
2771
  const discovered = /* @__PURE__ */ new Set(["base"]);
@@ -2614,7 +2780,7 @@ async function listProfiles(root = process.cwd()) {
2614
2780
  }
2615
2781
  }
2616
2782
  async function deleteProfileDefinition(root = process.cwd(), profile) {
2617
- const filePath = path12.join(path12.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2783
+ const filePath = path13.join(path13.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2618
2784
  try {
2619
2785
  await rm2(filePath);
2620
2786
  return {
@@ -2634,7 +2800,7 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
2634
2800
  name: "base"
2635
2801
  };
2636
2802
  }
2637
- const filePath = path12.join(path12.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2803
+ const filePath = path13.join(path13.resolve(root), ".cnos", "profiles", `${profile}.yml`);
2638
2804
  try {
2639
2805
  return parseYaml4(await readFile5(filePath, "utf8")) ?? void 0;
2640
2806
  } catch {
@@ -2658,7 +2824,7 @@ function normalizeProfileAction(args) {
2658
2824
  }
2659
2825
  async function runProfile(args, options = {}) {
2660
2826
  const { action, tail } = normalizeProfileAction(args);
2661
- const root = path13.resolve(options.root ?? process.cwd());
2827
+ const root = path14.resolve(options.root ?? process.cwd());
2662
2828
  const cliArgs = [...options.cliArgs ?? []];
2663
2829
  if (action === "create") {
2664
2830
  const profile = tail[0] ?? "stage";
@@ -2709,12 +2875,12 @@ async function runProfile(args, options = {}) {
2709
2875
  }
2710
2876
 
2711
2877
  // src/commands/promote.ts
2712
- import path14 from "path";
2713
- import { writeFile as writeFile6 } from "fs/promises";
2878
+ import path15 from "path";
2879
+ import { writeFile as writeFile7 } from "fs/promises";
2714
2880
  import {
2715
2881
  ensureProjectionAllowed,
2716
2882
  loadManifest as loadManifest2,
2717
- stringifyYaml as stringifyYaml4
2883
+ stringifyYaml as stringifyYaml5
2718
2884
  } from "@kitsy/cnos/internal";
2719
2885
  function normalizeTarget(value) {
2720
2886
  if (value === "public" || value === "env") {
@@ -2726,7 +2892,7 @@ function sortRecord(record) {
2726
2892
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
2727
2893
  }
2728
2894
  async function runPromote(args = [], options = {}) {
2729
- const root = path14.resolve(options.root ?? process.cwd());
2895
+ const root = path15.resolve(options.root ?? process.cwd());
2730
2896
  const cliArgs = [...options.cliArgs ?? []];
2731
2897
  const target = normalizeTarget(consumeOption(cliArgs, "--to"));
2732
2898
  const alias = consumeOption(cliArgs, "--as");
@@ -2765,7 +2931,7 @@ async function runPromote(args = [], options = {}) {
2765
2931
  })
2766
2932
  };
2767
2933
  }
2768
- await writeFile6(loadedManifest.manifestPath, stringifyYaml4(rawManifest), "utf8");
2934
+ await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
2769
2935
  if (options.json) {
2770
2936
  return printJson({
2771
2937
  target,
@@ -2797,9 +2963,8 @@ async function runRead(key, options = {}) {
2797
2963
  // src/commands/run.ts
2798
2964
  import {
2799
2965
  CNOS_GRAPH_ENV_VAR,
2800
- CNOS_SECRET_PAYLOAD_ENV_VAR,
2801
- CNOS_SESSION_KEY_ENV_VAR,
2802
- serializeSecretPayload,
2966
+ CNOS_PROJECTION_ENV_VAR,
2967
+ serializeServerProjection,
2803
2968
  serializeRuntimeGraph
2804
2969
  } from "@kitsy/cnos/internal";
2805
2970
  function consumeOptions(args, flag) {
@@ -2838,7 +3003,7 @@ async function runCommand(command, options = {}) {
2838
3003
  }
2839
3004
  const cliArgs = [...options.cliArgs ?? []];
2840
3005
  const isPublic = consumeFlag(cliArgs, "--public");
2841
- const isAuthenticated = consumeFlag(cliArgs, "--auth");
3006
+ consumeFlag(cliArgs, "--auth");
2842
3007
  const framework = consumeOption(cliArgs, "--framework");
2843
3008
  const prefix = consumeOption(cliArgs, "--prefix");
2844
3009
  const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
@@ -2846,21 +3011,14 @@ async function runCommand(command, options = {}) {
2846
3011
  ...options,
2847
3012
  cliArgs: [...cliArgs, ...setOverrides]
2848
3013
  });
2849
- const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
2850
- Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
2851
- ) : void 0;
2852
- const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
2853
3014
  const env = {
2854
3015
  ...process.env,
2855
3016
  ...isPublic ? runtime.toPublicEnv({
2856
3017
  ...framework ? { framework } : {},
2857
3018
  ...prefix ? { prefix } : {}
2858
3019
  }) : runtime.toEnv(),
2859
- [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph),
2860
- ...secretPayload ? {
2861
- [CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
2862
- [CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
2863
- } : {}
3020
+ [CNOS_PROJECTION_ENV_VAR]: serializeServerProjection(runtime.toServerProjection()),
3021
+ [CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph)
2864
3022
  };
2865
3023
  return new Promise((resolve, reject) => {
2866
3024
  const executable = command[0];
@@ -2895,14 +3053,14 @@ async function runCommand(command, options = {}) {
2895
3053
  }
2896
3054
 
2897
3055
  // src/commands/secret.ts
2898
- import path17 from "path";
3056
+ import path18 from "path";
2899
3057
 
2900
3058
  // src/commands/vault.ts
2901
- import path16 from "path";
3059
+ import path17 from "path";
2902
3060
 
2903
3061
  // src/services/vaults.ts
2904
- import { rm as rm3, writeFile as writeFile7 } from "fs/promises";
2905
- import path15 from "path";
3062
+ import { rm as rm3, writeFile as writeFile8 } from "fs/promises";
3063
+ import path16 from "path";
2906
3064
  import {
2907
3065
  clearAllVaultSessionKeys,
2908
3066
  clearVaultSessionKey,
@@ -2915,7 +3073,7 @@ import {
2915
3073
  resolveSecretStoreRoot as resolveSecretStoreRoot2,
2916
3074
  resolveVaultAuth as resolveVaultAuth2,
2917
3075
  resolveVaultDefinition,
2918
- stringifyYaml as stringifyYaml5,
3076
+ stringifyYaml as stringifyYaml6,
2919
3077
  writeKeychain,
2920
3078
  writeVaultSessionKey
2921
3079
  } from "@kitsy/cnos/internal";
@@ -2956,7 +3114,7 @@ async function createVaultDefinition(name, options = {}) {
2956
3114
  [vault]: vaultDefinition
2957
3115
  }
2958
3116
  };
2959
- await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
3117
+ await writeFile8(loadedManifest.manifestPath, stringifyYaml6(rawManifest), "utf8");
2960
3118
  const definition = resolveVaultDefinition({ [vault]: vaultDefinition }, vault);
2961
3119
  if (provider === "local") {
2962
3120
  const auth = await resolveVaultAuth2(vault, vaultDefinition, options.processEnv ?? process.env);
@@ -3007,8 +3165,8 @@ async function removeVaultDefinition(name, options = {}) {
3007
3165
  if (Object.keys(nextVaults).length === 0) {
3008
3166
  delete rawManifest.vaults;
3009
3167
  }
3010
- await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
3011
- const vaultRoot = path15.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
3168
+ await writeFile8(loadedManifest.manifestPath, stringifyYaml6(rawManifest), "utf8");
3169
+ const vaultRoot = path16.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
3012
3170
  let removedStore;
3013
3171
  try {
3014
3172
  await rm3(vaultRoot, { recursive: true, force: true });
@@ -3102,7 +3260,7 @@ function normalizeVaultAction(args) {
3102
3260
  async function runVault(args = [], options = {}) {
3103
3261
  const { action, tail } = normalizeVaultAction(args);
3104
3262
  const cliArgs = [...options.cliArgs ?? []];
3105
- const root = path16.resolve(options.root ?? process.cwd());
3263
+ const root = path17.resolve(options.root ?? process.cwd());
3106
3264
  if (consumeOption(cliArgs, "--passphrase")) {
3107
3265
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
3108
3266
  }
@@ -3208,7 +3366,7 @@ async function runSecret(argsOrPath, options = {}) {
3208
3366
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
3209
3367
  const { action, tail } = normalizeSecretCommand(args);
3210
3368
  const cliArgs = [...options.cliArgs ?? []];
3211
- const root = path17.resolve(options.root ?? process.cwd());
3369
+ const root = path18.resolve(options.root ?? process.cwd());
3212
3370
  if (consumeOption(cliArgs, "--passphrase")) {
3213
3371
  throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
3214
3372
  }
@@ -3304,9 +3462,9 @@ async function runSecret(argsOrPath, options = {}) {
3304
3462
  }
3305
3463
 
3306
3464
  // src/commands/use.ts
3307
- import path18 from "path";
3465
+ import path19 from "path";
3308
3466
  async function runUse(args = [], options = {}) {
3309
- const root = path18.resolve(options.root ?? process.cwd());
3467
+ const root = path19.resolve(options.root ?? process.cwd());
3310
3468
  const action = args[0];
3311
3469
  const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
3312
3470
  if (action === "show" || !action && !hasUpdates) {
@@ -3343,7 +3501,7 @@ async function runValidate(options = {}) {
3343
3501
  // package.json
3344
3502
  var package_default = {
3345
3503
  name: "@kitsy/cnos-cli",
3346
- version: "1.5.1",
3504
+ version: "1.6.1",
3347
3505
  description: "CLI entry point and developer tooling for CNOS.",
3348
3506
  type: "module",
3349
3507
  main: "./dist/index.js",
@@ -3398,7 +3556,7 @@ function runVersion() {
3398
3556
  }
3399
3557
 
3400
3558
  // src/commands/value.ts
3401
- import path19 from "path";
3559
+ import path20 from "path";
3402
3560
  function normalizeValueCommand(args) {
3403
3561
  const [actionOrPath, ...tail] = args;
3404
3562
  if (!actionOrPath) {
@@ -3422,7 +3580,7 @@ async function runValue(argsOrPath, options = {}) {
3422
3580
  const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
3423
3581
  const { action, tail } = normalizeValueCommand(args);
3424
3582
  const cliArgs = [...options.cliArgs ?? []];
3425
- const root = path19.resolve(options.root ?? process.cwd());
3583
+ const root = path20.resolve(options.root ?? process.cwd());
3426
3584
  if (action === "list") {
3427
3585
  const prefix = consumeOption(cliArgs, "--prefix");
3428
3586
  const entries = await listConfigEntries("value", {
@@ -3485,10 +3643,10 @@ async function runValue(argsOrPath, options = {}) {
3485
3643
  // src/commands/watch.ts
3486
3644
  import {
3487
3645
  CNOS_GRAPH_ENV_VAR as CNOS_GRAPH_ENV_VAR2,
3488
- CNOS_SECRET_PAYLOAD_ENV_VAR as CNOS_SECRET_PAYLOAD_ENV_VAR2,
3489
- CNOS_SESSION_KEY_ENV_VAR as CNOS_SESSION_KEY_ENV_VAR2,
3646
+ CNOS_SECRET_PAYLOAD_ENV_VAR,
3647
+ CNOS_SESSION_KEY_ENV_VAR,
3490
3648
  serializeRuntimeGraph as serializeRuntimeGraph2,
3491
- serializeSecretPayload as serializeSecretPayload2
3649
+ serializeSecretPayload
3492
3650
  } from "@kitsy/cnos/internal";
3493
3651
  async function buildRunEnvironment(options) {
3494
3652
  const cliArgs = [...options.cliArgs ?? []];
@@ -3503,7 +3661,7 @@ async function buildRunEnvironment(options) {
3503
3661
  const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
3504
3662
  Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
3505
3663
  ) : void 0;
3506
- const secretPayload = authenticatedSecrets ? serializeSecretPayload2(authenticatedSecrets) : void 0;
3664
+ const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
3507
3665
  return {
3508
3666
  runtime,
3509
3667
  env: {
@@ -3514,8 +3672,8 @@ async function buildRunEnvironment(options) {
3514
3672
  }) : runtime.toEnv(),
3515
3673
  [CNOS_GRAPH_ENV_VAR2]: serializeRuntimeGraph2(runtime.graph),
3516
3674
  ...secretPayload ? {
3517
- [CNOS_SECRET_PAYLOAD_ENV_VAR2]: secretPayload.payload,
3518
- [CNOS_SESSION_KEY_ENV_VAR2]: secretPayload.sessionKey
3675
+ [CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
3676
+ [CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
3519
3677
  } : {}
3520
3678
  }
3521
3679
  };
@@ -3605,6 +3763,163 @@ async function runWatch(command, options = {}) {
3605
3763
  return isSignal ? "watching config changes in signal mode" : "watching config changes in restart mode";
3606
3764
  }
3607
3765
 
3766
+ // src/commands/workspace.ts
3767
+ import { cp, mkdir as mkdir6, rename, rm as rm4, stat, writeFile as writeFile9, readFile as readFile6 } from "fs/promises";
3768
+ import path21 from "path";
3769
+ import { loadManifest as loadManifest4, parseYaml as parseYaml5, stringifyYaml as stringifyYaml7 } from "@kitsy/cnos/internal";
3770
+ async function exists(targetPath) {
3771
+ try {
3772
+ await stat(targetPath);
3773
+ return true;
3774
+ } catch {
3775
+ return false;
3776
+ }
3777
+ }
3778
+ async function copyIfExists(source, target) {
3779
+ if (!await exists(source)) {
3780
+ return;
3781
+ }
3782
+ await mkdir6(path21.dirname(target), { recursive: true });
3783
+ await cp(source, target, { recursive: true, force: true });
3784
+ }
3785
+ async function mergeWorkspaceRootsIntoStandalone(targetCnosRoot, sourceRoots) {
3786
+ for (const sourceRoot of sourceRoots) {
3787
+ for (const folderName of ["values", "secrets", "env", "profiles"]) {
3788
+ await copyIfExists(
3789
+ path21.join(sourceRoot, folderName),
3790
+ path21.join(targetCnosRoot, folderName)
3791
+ );
3792
+ }
3793
+ }
3794
+ }
3795
+ async function writeCnosrc(packageRoot, config) {
3796
+ await writeFile9(
3797
+ path21.join(packageRoot, ".cnosrc.yml"),
3798
+ stringifyYaml7({
3799
+ root: config.root,
3800
+ ...config.workspace ? { workspace: config.workspace } : {}
3801
+ }),
3802
+ "utf8"
3803
+ );
3804
+ }
3805
+ function createDetachedManifest(rawManifest) {
3806
+ const next = structuredClone(rawManifest);
3807
+ if ("workspaces" in next) {
3808
+ delete next.workspaces;
3809
+ }
3810
+ return next;
3811
+ }
3812
+ async function runDetach(packageRoot, options = {}) {
3813
+ const loaded = await loadManifest4({ cwd: packageRoot });
3814
+ if (!loaded.anchorPath || !loaded.anchoredWorkspace) {
3815
+ throw new Error("workspace detach requires a package-local .cnosrc.yml with a workspace binding");
3816
+ }
3817
+ const targetCnosRoot = path21.join(packageRoot, ".cnos");
3818
+ const force = consumeFlag([...options.cliArgs ?? []], "--force");
3819
+ if (await exists(targetCnosRoot) && !force) {
3820
+ throw new Error(`Refusing to detach because ${displayPath(targetCnosRoot, packageRoot)} already exists. Use --force to overwrite.`);
3821
+ }
3822
+ if (force) {
3823
+ await rm4(targetCnosRoot, { recursive: true, force: true });
3824
+ }
3825
+ const runtime = await createRuntimeService({
3826
+ ...options,
3827
+ root: loaded.manifestRoot,
3828
+ workspace: loaded.anchoredWorkspace
3829
+ });
3830
+ const localRoots = runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => root.path);
3831
+ await mkdir6(targetCnosRoot, { recursive: true });
3832
+ await mergeWorkspaceRootsIntoStandalone(targetCnosRoot, localRoots);
3833
+ await writeFile9(
3834
+ path21.join(targetCnosRoot, "cnos.yml"),
3835
+ stringifyYaml7(createDetachedManifest(loaded.rawManifest)),
3836
+ "utf8"
3837
+ );
3838
+ const relativeRoot = path21.relative(packageRoot, loaded.manifestRoot).replace(/\\/g, "/");
3839
+ const marker = {
3840
+ detachedFrom: relativeRoot || ".",
3841
+ detachedWorkspace: loaded.anchoredWorkspace,
3842
+ detachedAt: (/* @__PURE__ */ new Date()).toISOString(),
3843
+ originalCnosrc: {
3844
+ root: relativeRoot || ".",
3845
+ workspace: loaded.anchoredWorkspace
3846
+ }
3847
+ };
3848
+ await writeFile9(path21.join(targetCnosRoot, ".detached"), stringifyYaml7(marker), "utf8");
3849
+ await writeCnosrc(packageRoot, { root: "./.cnos" });
3850
+ if (options.json) {
3851
+ return printJson({
3852
+ packageRoot,
3853
+ detachedWorkspace: loaded.anchoredWorkspace,
3854
+ cnosRoot: targetCnosRoot
3855
+ });
3856
+ }
3857
+ return `detached workspace ${loaded.anchoredWorkspace} into ${displayPath(targetCnosRoot, packageRoot)}`;
3858
+ }
3859
+ async function runAttach(packageRoot, options = {}) {
3860
+ const cliArgs = [...options.cliArgs ?? []];
3861
+ const force = consumeFlag(cliArgs, "--force");
3862
+ const childCnosRoot = path21.join(packageRoot, ".cnos");
3863
+ const markerPath = path21.join(childCnosRoot, ".detached");
3864
+ if (!await exists(markerPath)) {
3865
+ throw new Error("workspace attach requires a detached package with .cnos/.detached");
3866
+ }
3867
+ const marker = parseYaml5(await readFile6(markerPath, "utf8"));
3868
+ if (!marker?.originalCnosrc?.root || !marker.detachedWorkspace) {
3869
+ throw new Error("Invalid .detached marker");
3870
+ }
3871
+ const parentManifestRoot = path21.resolve(packageRoot, marker.originalCnosrc.root);
3872
+ const parentLoaded = await loadManifest4({ root: parentManifestRoot });
3873
+ const workspaceId = marker.originalCnosrc.workspace ?? marker.detachedWorkspace;
3874
+ const parentWorkspaceRoot = path21.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
3875
+ if (await exists(parentWorkspaceRoot) && !force) {
3876
+ throw new Error(`workspace "${workspaceId}" already exists in parent root. Use --force to overwrite.`);
3877
+ }
3878
+ if (force) {
3879
+ await rm4(parentWorkspaceRoot, { recursive: true, force: true });
3880
+ }
3881
+ await mkdir6(parentWorkspaceRoot, { recursive: true });
3882
+ for (const folderName of ["values", "secrets", "env", "profiles"]) {
3883
+ await copyIfExists(path21.join(childCnosRoot, folderName), path21.join(parentWorkspaceRoot, folderName));
3884
+ }
3885
+ const rawManifest = parentLoaded.rawManifest;
3886
+ const workspaces = rawManifest.workspaces ?? {};
3887
+ const items = workspaces.items ?? {};
3888
+ items[workspaceId] = items[workspaceId] ?? {};
3889
+ workspaces.items = items;
3890
+ rawManifest.workspaces = workspaces;
3891
+ await writeFile9(path21.join(parentLoaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
3892
+ const archivePath = path21.join(packageRoot, ".cnos.detached.bak");
3893
+ await rm4(archivePath, { recursive: true, force: true });
3894
+ await rename(childCnosRoot, archivePath);
3895
+ await writeCnosrc(packageRoot, {
3896
+ root: marker.originalCnosrc.root,
3897
+ ...workspaceId ? { workspace: workspaceId } : {}
3898
+ });
3899
+ if (options.json) {
3900
+ return printJson({
3901
+ packageRoot,
3902
+ workspace: workspaceId,
3903
+ parentRoot: parentLoaded.manifestRoot,
3904
+ archivedTo: archivePath
3905
+ });
3906
+ }
3907
+ return `attached workspace ${workspaceId} to ${displayPath(parentLoaded.manifestRoot, packageRoot)}`;
3908
+ }
3909
+ async function runWorkspace(args = [], options = {}) {
3910
+ const [action] = args;
3911
+ const cliArgs = [...options.cliArgs ?? []];
3912
+ const packageRoot = path21.resolve(consumeOption(cliArgs, "--package-root") ?? options.root ?? process.cwd());
3913
+ switch (action) {
3914
+ case "detach":
3915
+ return runDetach(packageRoot, { ...options, cliArgs });
3916
+ case "attach":
3917
+ return runAttach(packageRoot, { ...options, cliArgs });
3918
+ default:
3919
+ throw new Error(`Unsupported workspace action: ${action ?? "(missing)"}`);
3920
+ }
3921
+ }
3922
+
3608
3923
  // src/index.ts
3609
3924
  function resolveHelpTopic(command, args) {
3610
3925
  if (command === "help" || command === "help-ai") {
@@ -3616,9 +3931,15 @@ function resolveHelpTopic(command, args) {
3616
3931
  if (command === "build" && args[0] === "env") {
3617
3932
  return normalizeHelpTopic([command, args[0]]);
3618
3933
  }
3934
+ if (command === "build" && args[0] && ["env", "server", "browser", "public"].includes(args[0])) {
3935
+ return normalizeHelpTopic([command, args[0]]);
3936
+ }
3619
3937
  if (command === "dev" && args[0] === "env") {
3620
3938
  return normalizeHelpTopic([command, args[0]]);
3621
3939
  }
3940
+ if (command === "workspace" && args[0] && ["attach", "detach"].includes(args[0])) {
3941
+ return normalizeHelpTopic([command, args[0]]);
3942
+ }
3622
3943
  if (command === "vault" && args[0] && ["create", "add", "list", "delete", "remove"].includes(args[0])) {
3623
3944
  return normalizeHelpTopic([command, args[0] === "delete" ? "remove" : args[0] === "add" ? "create" : args[0]]);
3624
3945
  }
@@ -3773,6 +4094,10 @@ async function main(argv) {
3773
4094
  }
3774
4095
  case "watch":
3775
4096
  process.stdout.write(`${await runWatch(passthrough.length > 0 ? passthrough : args, runtimeOptions)}
4097
+ `);
4098
+ return;
4099
+ case "workspace":
4100
+ process.stdout.write(`${await runWorkspace(args, runtimeOptions)}
3776
4101
  `);
3777
4102
  return;
3778
4103
  case "diff":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitsy/cnos-cli",
3
- "version": "1.5.1",
3
+ "version": "1.6.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.5.1"
39
+ "@kitsy/cnos": "1.6.1"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsup src/index.ts --format esm --dts",