@kitsy/cnos-cli 0.0.1 → 1.0.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 +980 -99
  2. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -7,8 +7,50 @@ var OPTION_KEYS = {
7
7
  "--profile": "profile",
8
8
  "--global-root": "globalRoot"
9
9
  };
10
- var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set(["--format", "--framework", "--prefix", "--target", "--to"]);
11
- var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set(["--flatten", "--public"]);
10
+ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
11
+ "--format",
12
+ "--framework",
13
+ "--prefix",
14
+ "--target",
15
+ "--to",
16
+ "--provider",
17
+ "--passphrase",
18
+ "--inherit"
19
+ ]);
20
+ var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set(["--flatten", "--public", "--local", "--remote", "--ref"]);
21
+ function normalizeCommand(argv) {
22
+ const [command = "doctor", ...rest] = argv;
23
+ const resource = rest[0];
24
+ const remaining = rest.slice(1);
25
+ if ((command === "create" || command === "add") && resource === "profile") {
26
+ return ["profile", "create", ...remaining];
27
+ }
28
+ if ((command === "delete" || command === "remove") && resource === "profile") {
29
+ return ["profile", "delete", ...remaining];
30
+ }
31
+ if (command === "list" && resource === "profile") {
32
+ return ["profile", "list", ...remaining];
33
+ }
34
+ if ((command === "create" || command === "add") && resource === "secret") {
35
+ return ["secret", "set", ...remaining];
36
+ }
37
+ if ((command === "delete" || command === "remove") && resource === "secret") {
38
+ return ["secret", "delete", ...remaining];
39
+ }
40
+ if (command === "list" && resource === "secret") {
41
+ return ["secret", "list", ...remaining];
42
+ }
43
+ if ((command === "create" || command === "add") && resource === "value") {
44
+ return ["value", "set", ...remaining];
45
+ }
46
+ if ((command === "delete" || command === "remove") && resource === "value") {
47
+ return ["value", "delete", ...remaining];
48
+ }
49
+ if (command === "list" && resource === "value") {
50
+ return ["value", "list", ...remaining];
51
+ }
52
+ return [command, ...rest];
53
+ }
12
54
  function setOption(options, key, value) {
13
55
  options[key] = value;
14
56
  }
@@ -23,7 +65,18 @@ function parseArgs(argv) {
23
65
  passthrough: []
24
66
  };
25
67
  }
26
- const [command = "doctor", ...rest] = argv;
68
+ if (argv[0] === "--version" || argv[0] === "-v") {
69
+ return {
70
+ command: "version",
71
+ args: [],
72
+ options: {
73
+ cliArgs: []
74
+ },
75
+ passthrough: []
76
+ };
77
+ }
78
+ const normalizedArgv = normalizeCommand(argv);
79
+ const [command = "doctor", ...rest] = normalizedArgv;
27
80
  const options = {};
28
81
  const args = [];
29
82
  const cliArgs = [];
@@ -128,15 +181,24 @@ function printJson(value) {
128
181
  }
129
182
 
130
183
  // src/services/writes.ts
131
- import { mkdir, readFile, writeFile } from "fs/promises";
184
+ import { randomUUID } from "crypto";
185
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
132
186
  import path from "path";
133
- import { parseYaml, resolveWorkspaceScopedPath, stringifyYaml } from "@kitsy/cnos-core";
187
+ import {
188
+ parseYaml,
189
+ resolveConfigDocumentPath,
190
+ resolveSecretStoreRoot,
191
+ stringifyYaml,
192
+ writeLocalSecret
193
+ } from "@kitsy/cnos/internal";
134
194
 
135
195
  // src/services/runtime.ts
136
196
  import { createCnos } from "@kitsy/cnos";
137
197
  async function createRuntimeService(options = {}) {
138
198
  const createOptions = {
139
- root: options.root ?? process.cwd(),
199
+ ...options.root ? {
200
+ root: options.root
201
+ } : {},
140
202
  ...options.workspace ? {
141
203
  workspace: options.workspace
142
204
  } : {},
@@ -181,8 +243,38 @@ function parseScalarValue(rawValue) {
181
243
  return rawValue;
182
244
  }
183
245
  }
184
- async function defineValue(namespace, configPath, rawValue, options = {}) {
185
- const runtime = await createRuntimeService(options);
246
+ function unsetNestedValue(target, pathSegments) {
247
+ const [head, ...tail] = pathSegments;
248
+ if (!head || !(head in target)) {
249
+ return false;
250
+ }
251
+ if (tail.length === 0) {
252
+ delete target[head];
253
+ return true;
254
+ }
255
+ const nextValue = target[head];
256
+ if (!nextValue || typeof nextValue !== "object" || Array.isArray(nextValue)) {
257
+ return false;
258
+ }
259
+ const deleted = unsetNestedValue(nextValue, tail);
260
+ if (deleted && Object.keys(nextValue).length === 0) {
261
+ delete target[head];
262
+ }
263
+ return deleted;
264
+ }
265
+ function isSecretReference(value) {
266
+ return Boolean(
267
+ value && typeof value === "object" && !Array.isArray(value) && typeof value.provider === "string" && typeof value.ref === "string"
268
+ );
269
+ }
270
+ async function readYamlDocument(filePath) {
271
+ try {
272
+ return parseYaml(await readFile(filePath, "utf8")) ?? {};
273
+ } catch {
274
+ return {};
275
+ }
276
+ }
277
+ function getSelectedWorkspaceRoot(options, runtime) {
186
278
  const target = options.target ?? "local";
187
279
  const workspaceRoot = runtime.graph.workspace.workspaceRoots.find(
188
280
  (entry) => entry.scope === target && entry.workspaceId === runtime.graph.workspace.workspaceId
@@ -193,18 +285,27 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
193
285
  if (target === "global" && !runtime.manifest.workspaces.global.allowWrite) {
194
286
  throw new Error("Global writes require workspaces.global.allowWrite: true");
195
287
  }
196
- const profile = options.profile ?? runtime.manifest.writePolicy.define.defaultProfile;
197
- const template = runtime.manifest.writePolicy.define.targets[namespace];
198
- const filePath = resolveWorkspaceScopedPath(workspaceRoot.path, template, {
199
- workspace: runtime.graph.workspace.workspaceId,
200
- profile
201
- });
202
- let document = {};
203
- try {
204
- document = parseYaml(await readFile(filePath, "utf8")) ?? {};
205
- } catch {
206
- document = {};
288
+ return workspaceRoot.path;
289
+ }
290
+ async function defineValue(namespace, configPath, rawValue, options = {}) {
291
+ if (namespace === "secret") {
292
+ const secret = await setSecret(configPath, rawValue, {
293
+ ...options,
294
+ mode: options.mode ?? "local"
295
+ });
296
+ return {
297
+ filePath: secret.filePath,
298
+ value: {
299
+ provider: secret.provider,
300
+ ref: secret.ref
301
+ }
302
+ };
207
303
  }
304
+ const runtime = await createRuntimeService(options);
305
+ const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
306
+ const profile = options.profile ?? runtime.graph.profile;
307
+ const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
308
+ const document = await readYamlDocument(filePath);
208
309
  const parsedValue = parseScalarValue(rawValue);
209
310
  setNestedValue(document, configPath.split("."), parsedValue);
210
311
  await mkdir(path.dirname(filePath), { recursive: true });
@@ -214,15 +315,118 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
214
315
  value: parsedValue
215
316
  };
216
317
  }
318
+ async function setSecret(configPath, rawValue, options = {}) {
319
+ const runtime = await createRuntimeService(options);
320
+ const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
321
+ const profile = options.profile ?? runtime.graph.profile;
322
+ const filePath = resolveConfigDocumentPath(workspaceRoot, "secret", configPath, profile);
323
+ const document = await readYamlDocument(filePath);
324
+ const mode = options.mode ?? "local";
325
+ let reference;
326
+ let storePath;
327
+ if (mode === "local") {
328
+ const passphrase = options.passphrase ?? options.processEnv?.CNOS_SECRET_PASSPHRASE ?? process.env.CNOS_SECRET_PASSPHRASE;
329
+ if (!passphrase) {
330
+ throw new Error("Local secret writes require --passphrase or CNOS_SECRET_PASSPHRASE");
331
+ }
332
+ const profileSegment = profile && profile !== "base" ? profile : "base";
333
+ const ref = [
334
+ runtime.manifest.project.name,
335
+ runtime.graph.workspace.workspaceId,
336
+ profileSegment,
337
+ ...configPath.split("."),
338
+ randomUUID()
339
+ ].map((segment) => segment.replace(/[^A-Za-z0-9._-]+/g, "-")).join("/");
340
+ storePath = await writeLocalSecret(resolveSecretStoreRoot(options.processEnv), ref, rawValue, passphrase);
341
+ reference = {
342
+ provider: "local",
343
+ ref
344
+ };
345
+ } else {
346
+ reference = {
347
+ provider: options.provider ?? (mode === "ref" ? "ref" : "remote"),
348
+ ref: rawValue
349
+ };
350
+ }
351
+ setNestedValue(document, configPath.split("."), reference);
352
+ await mkdir(path.dirname(filePath), { recursive: true });
353
+ await writeFile(filePath, stringifyYaml(document), "utf8");
354
+ return {
355
+ filePath,
356
+ provider: reference.provider,
357
+ ref: reference.ref,
358
+ ...storePath ? { storePath } : {}
359
+ };
360
+ }
361
+ async function deleteSecret(configPath, options = {}) {
362
+ const runtime = await createRuntimeService(options);
363
+ const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
364
+ const profile = options.profile ?? runtime.graph.profile;
365
+ const filePath = resolveConfigDocumentPath(workspaceRoot, "secret", configPath, profile);
366
+ const document = await readYamlDocument(filePath);
367
+ const metadata = runtime.graph.entries.get(`secret.${configPath}`)?.winner.metadata;
368
+ const deleted = unsetNestedValue(document, configPath.split("."));
369
+ if (!deleted) {
370
+ return {
371
+ filePath,
372
+ deleted: false
373
+ };
374
+ }
375
+ await writeFile(filePath, stringifyYaml(document), "utf8");
376
+ let removedStore;
377
+ const secretRef = metadata?.secretRef;
378
+ if (isSecretReference(secretRef) && secretRef.provider === "local") {
379
+ const storePath = path.join(resolveSecretStoreRoot(options.processEnv), "store", ...secretRef.ref.split("/")).concat(".json");
380
+ await rm(storePath, { force: true });
381
+ removedStore = storePath;
382
+ }
383
+ return {
384
+ filePath,
385
+ deleted: true,
386
+ ...removedStore ? { removedStore } : {}
387
+ };
388
+ }
389
+ async function deleteValue(namespace, configPath, options = {}) {
390
+ if (namespace === "secret") {
391
+ return deleteSecret(configPath, options);
392
+ }
393
+ const runtime = await createRuntimeService(options);
394
+ const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
395
+ const profile = options.profile ?? runtime.graph.profile;
396
+ const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
397
+ const document = await readYamlDocument(filePath);
398
+ const deleted = unsetNestedValue(document, configPath.split("."));
399
+ if (!deleted) {
400
+ return {
401
+ filePath,
402
+ deleted: false
403
+ };
404
+ }
405
+ await writeFile(filePath, stringifyYaml(document), "utf8");
406
+ return {
407
+ filePath,
408
+ deleted: true
409
+ };
410
+ }
217
411
 
218
412
  // src/commands/define.ts
219
413
  async function runDefine(namespace, configPath, rawValue, options = {}) {
220
414
  const cliArgs = [...options.cliArgs ?? []];
221
415
  const target = consumeOption(cliArgs, "--target") ?? "local";
416
+ const local = consumeFlag(cliArgs, "--local");
417
+ const remote = consumeFlag(cliArgs, "--remote");
418
+ const ref = consumeFlag(cliArgs, "--ref");
419
+ const provider = consumeOption(cliArgs, "--provider");
420
+ const passphrase = consumeOption(cliArgs, "--passphrase");
222
421
  const result = await defineValue(namespace, configPath, rawValue, {
223
422
  ...options,
224
423
  cliArgs,
225
- target
424
+ target,
425
+ ...namespace === "secret" ? {
426
+ mode: local ? "local" : remote ? "remote" : ref ? "ref" : "local",
427
+ ...provider ? { provider } : {},
428
+ ...passphrase ? { passphrase } : {}
429
+ } : {}
226
430
  });
227
431
  if (options.json) {
228
432
  return printJson({
@@ -237,7 +441,7 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
237
441
  }
238
442
 
239
443
  // src/commands/diff.ts
240
- import { flattenObject } from "@kitsy/cnos-core";
444
+ import { flattenObject } from "@kitsy/cnos/internal";
241
445
  function flattenRuntime(runtime) {
242
446
  return {
243
447
  ...Object.fromEntries(
@@ -278,7 +482,7 @@ import { readFile as readFile2 } from "fs/promises";
278
482
  import path2 from "path";
279
483
 
280
484
  // src/services/validation.ts
281
- import { validateRuntime } from "@kitsy/cnos-core";
485
+ import { validateRuntime } from "@kitsy/cnos/internal";
282
486
  async function createValidationSummary(options = {}) {
283
487
  const runtime = await createRuntimeService(options);
284
488
  const summary = await validateRuntime(runtime);
@@ -292,11 +496,14 @@ async function createValidationSummary(options = {}) {
292
496
  async function checkGitignore(root) {
293
497
  const gitignorePath = path2.join(root, ".gitignore");
294
498
  const expected = [
295
- "cnos/workspaces/*/secrets/",
296
- "cnos/workspaces/*/env/.env",
297
- "cnos/workspaces/*/env/.env.*",
298
- "!cnos/workspaces/*/env/.env.example",
299
- "!cnos/workspaces/*/env/.env.*.example"
499
+ ".cnos/env/.env",
500
+ ".cnos/env/.env.*",
501
+ "!.cnos/env/.env.example",
502
+ "!.cnos/env/.env.*.example",
503
+ ".cnos/workspaces/*/env/.env",
504
+ ".cnos/workspaces/*/env/.env.*",
505
+ "!.cnos/workspaces/*/env/.env.example",
506
+ "!.cnos/workspaces/*/env/.env.*.example"
300
507
  ];
301
508
  try {
302
509
  const content = await readFile2(gitignorePath, "utf8");
@@ -448,14 +655,14 @@ var COMMANDS = [
448
655
  id: "init",
449
656
  summary: "Scaffold a workspace-aware CNOS tree in the current project.",
450
657
  usage: "cnos init [--workspace <id>] [--root <path>] [--json]",
451
- description: "Creates cnos/cnos.yml, .cnos-workspace.yml, workspace folders, and .gitignore entries without overwriting existing files.",
658
+ description: "Creates .cnos/cnos.yml, optional .cnos-workspace.yml, config folders, and .gitignore entries without overwriting existing files.",
452
659
  examples: ["cnos init", "cnos init --workspace api", "cnos init --root ./apps/api --workspace api --json"]
453
660
  },
454
661
  {
455
662
  id: "onboard",
456
663
  summary: "Onboard an existing repo into CNOS and import root dotenv files.",
457
664
  usage: "cnos onboard [--workspace <id>] [--root <path>] [--move] [--json]",
458
- description: "Scaffolds the CNOS workspace tree and imports root-level .env, .env.<profile>, and .env.*.example files into cnos/workspaces/<workspace>/env.",
665
+ description: "Scaffolds the CNOS workspace tree and imports root-level .env, .env.<profile>, and .env.*.example files into .cnos/workspaces/<workspace>/env.",
459
666
  options: [
460
667
  {
461
668
  flag: "--move",
@@ -480,37 +687,98 @@ var COMMANDS = [
480
687
  },
481
688
  {
482
689
  id: "value",
483
- summary: "Read a value namespace key without the value. prefix.",
484
- usage: "cnos value <path> [global-options]",
485
- description: "Reads value.<path> from the selected workspace and profile.",
690
+ summary: "Read, write, list, and delete values.",
691
+ usage: "cnos value [get <path> | set <path> <value> | list | delete <path>] [options] [global-options]",
692
+ description: "Reads and manages value.<path> entries from the selected workspace and profile.",
486
693
  arguments: [
487
694
  {
488
695
  name: "path",
489
- description: "Value path without the value. namespace prefix.",
490
- required: true
696
+ description: "Value path without the value. namespace prefix."
697
+ }
698
+ ],
699
+ options: [
700
+ {
701
+ flag: "--target <local|global>",
702
+ description: "Choose whether writes land in the local project workspace or the configured global root."
703
+ },
704
+ {
705
+ flag: "--prefix <path>",
706
+ description: "Filter value list output to keys that begin with this logical path or key prefix."
491
707
  }
492
708
  ],
493
- examples: ["cnos value app.name", "cnos value server.port --profile stage --workspace api"]
709
+ examples: [
710
+ "cnos value app.name",
711
+ "cnos value set server.port 3000",
712
+ "cnos add value app.name demo",
713
+ "cnos value list --prefix app."
714
+ ]
715
+ },
716
+ {
717
+ id: "value set",
718
+ summary: "Write a value.",
719
+ usage: "cnos value set <path> <value> [--target <local|global>] [global-options]",
720
+ description: "Writes a deterministic value document into the selected workspace or explicit global target.",
721
+ examples: ["cnos value set app.name demo", "cnos add value server.port 3000 --target global"]
722
+ },
723
+ {
724
+ id: "value list",
725
+ summary: "List resolved values.",
726
+ usage: "cnos value list [--prefix <path>] [global-options]",
727
+ description: "Lists resolved value keys for the selected workspace and profile.",
728
+ examples: ["cnos value list", "cnos value list --prefix app."]
729
+ },
730
+ {
731
+ id: "value delete",
732
+ summary: "Delete a value entry.",
733
+ usage: "cnos value delete <path> [--target <local|global>] [global-options]",
734
+ description: "Deletes a value from the selected workspace or explicit global target.",
735
+ examples: ["cnos value delete app.name", "cnos remove value server.port"]
494
736
  },
495
737
  {
496
738
  id: "secret",
497
- summary: "Read a secret namespace key without the secret. prefix.",
498
- usage: "cnos secret <path> [global-options]",
499
- description: "Reads secret.<path> from the selected workspace and profile.",
739
+ summary: "Read, write, list, and delete secrets.",
740
+ usage: "cnos secret [get <path> | set <path> <value> | list | delete <path>] [options] [global-options]",
741
+ description: "Reads resolved secrets, writes secret refs plus external local-secret material, and manages secret entries for the selected workspace and profile.",
500
742
  arguments: [
501
743
  {
502
744
  name: "path",
503
- description: "Secret path without the secret. namespace prefix.",
504
- required: true
745
+ description: "Secret path without the secret. namespace prefix."
746
+ }
747
+ ],
748
+ options: [
749
+ {
750
+ flag: "--local",
751
+ description: "Store encrypted secret material under ~/.cnos/secrets and write a local ref into the repo."
752
+ },
753
+ {
754
+ flag: "--remote",
755
+ description: "Write a remote secret reference into the repo."
756
+ },
757
+ {
758
+ flag: "--ref",
759
+ description: "Write a generic secret reference into the repo."
760
+ },
761
+ {
762
+ flag: "--provider <name>",
763
+ description: "Provider name for --remote or --ref secret writes."
764
+ },
765
+ {
766
+ flag: "--passphrase <value>",
767
+ description: "Passphrase used to encrypt local secret material when --local is selected."
505
768
  }
506
769
  ],
507
- examples: ["cnos secret app.token", "cnos secret service.apiKey --workspace agents"]
770
+ examples: [
771
+ "cnos secret app.token",
772
+ "cnos secret set app.token super-secret --local --passphrase dev-pass",
773
+ "cnos add secret app.token APP_TOKEN --ref --provider env",
774
+ "cnos secret list --workspace agents"
775
+ ]
508
776
  },
509
777
  {
510
778
  id: "define",
511
779
  summary: "Write a value or secret into the selected workspace.",
512
780
  usage: "cnos define <value|secret> <path> <rawValue> [--target <local|global>] [global-options]",
513
- description: "Writes deterministic YAML into the selected workspace. Global writes require allowWrite and an explicit --target global flag.",
781
+ description: "Writes deterministic YAML into the selected workspace. Secret writes default to secure local-secret storage plus a repo ref. Global writes require allowWrite and an explicit --target global flag.",
514
782
  arguments: [
515
783
  {
516
784
  name: "namespace",
@@ -536,9 +804,99 @@ var COMMANDS = [
536
804
  ],
537
805
  examples: [
538
806
  "cnos define value server.port 3000 --workspace api",
539
- "cnos define secret app.token super-secret --workspace api --target global"
807
+ "cnos define secret app.token super-secret --workspace api"
540
808
  ]
541
809
  },
810
+ {
811
+ id: "use",
812
+ summary: "Persist repo-local CLI defaults such as workspace and profile.",
813
+ usage: "cnos use [--workspace <id>] [--profile <name>] [--global-root <path>] [--root <path>] [--json]",
814
+ description: "Writes .cnos-workspace.yml so future CLI invocations can omit repeated workspace/profile/global-root flags.",
815
+ examples: ["cnos use --workspace api --profile stage", "cnos use --global-root ~/.cnos"]
816
+ },
817
+ {
818
+ id: "list",
819
+ summary: "List resolved config entries.",
820
+ usage: "cnos list [value|secret|meta|all] [--prefix <path>] [global-options]",
821
+ description: "Lists resolved config entries across one namespace or the full effective graph, with optional key-prefix filtering.",
822
+ options: [
823
+ {
824
+ flag: "--namespace <value|secret|meta|all>",
825
+ description: "Explicit namespace selector when not using a positional namespace argument."
826
+ },
827
+ {
828
+ flag: "--prefix <path>",
829
+ description: "Filter list output to entries whose logical keys begin with this prefix."
830
+ }
831
+ ],
832
+ examples: ["cnos list", "cnos list value --prefix app.", "cnos list --namespace secret"]
833
+ },
834
+ {
835
+ id: "profile",
836
+ summary: "Manage CNOS profiles.",
837
+ usage: "cnos profile [create <name> | list | use <name> | delete <name>] [options] [global-options]",
838
+ description: "Creates and lists explicit profiles and stores the active repo-local profile selection for CLI usage.",
839
+ options: [
840
+ {
841
+ flag: "--inherit <name>",
842
+ description: "Parent profile to extend when creating a profile."
843
+ }
844
+ ],
845
+ examples: [
846
+ "cnos profile create stage --inherit base",
847
+ "cnos profile list",
848
+ "cnos profile use stage"
849
+ ]
850
+ },
851
+ {
852
+ id: "profile create",
853
+ summary: "Create a profile definition.",
854
+ usage: "cnos profile create <name> [--inherit <name>] [--root <path>] [--json]",
855
+ description: "Creates .cnos/profiles/<name>.yml for an explicit profile overlay.",
856
+ examples: ["cnos profile create stage --inherit base"]
857
+ },
858
+ {
859
+ id: "profile list",
860
+ summary: "List available profiles.",
861
+ usage: "cnos profile list [--root <path>] [--json]",
862
+ description: "Lists the base profile plus any explicit profile definition files in .cnos/profiles.",
863
+ examples: ["cnos profile list"]
864
+ },
865
+ {
866
+ id: "profile use",
867
+ summary: "Persist the active profile for this repo.",
868
+ usage: "cnos profile use <name> [--root <path>] [--json]",
869
+ description: "Writes the selected profile into .cnos-workspace.yml.",
870
+ examples: ["cnos profile use stage"]
871
+ },
872
+ {
873
+ id: "profile delete",
874
+ summary: "Delete a profile definition.",
875
+ usage: "cnos profile delete <name> [--root <path>] [--json]",
876
+ description: "Deletes .cnos/profiles/<name>.yml.",
877
+ examples: ["cnos profile delete stage"]
878
+ },
879
+ {
880
+ id: "secret set",
881
+ summary: "Write a secret securely.",
882
+ usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--provider <name>] [--passphrase <value>] [global-options]",
883
+ description: "Writes a secret reference into the repo and, when --local is used, stores encrypted secret material outside the repo under ~/.cnos/secrets.",
884
+ examples: ["cnos secret set app.token super-secret --local --passphrase dev-pass"]
885
+ },
886
+ {
887
+ id: "secret list",
888
+ summary: "List resolved secrets.",
889
+ usage: "cnos secret list [global-options]",
890
+ description: "Lists resolved secret keys for the selected workspace and profile.",
891
+ examples: ["cnos secret list --workspace api"]
892
+ },
893
+ {
894
+ id: "secret delete",
895
+ summary: "Delete a secret reference.",
896
+ usage: "cnos secret delete <path> [--target <local|global>] [global-options]",
897
+ description: "Deletes the secret reference from the repo and removes local encrypted material when the secret used the local provider.",
898
+ examples: ["cnos secret delete app.token"]
899
+ },
542
900
  {
543
901
  id: "inspect",
544
902
  summary: "Inspect the winning value and provenance for a key.",
@@ -572,7 +930,11 @@ var COMMANDS = [
572
930
  required: true
573
931
  }
574
932
  ],
575
- examples: ["cnos export env", "cnos export env --public --framework vite --workspace api"]
933
+ examples: [
934
+ "cnos export env",
935
+ "cnos export env --public --framework vite --workspace api",
936
+ "cnos export env --public --framework next --workspace webapp"
937
+ ]
576
938
  },
577
939
  {
578
940
  id: "export env",
@@ -586,7 +948,7 @@ var COMMANDS = [
586
948
  },
587
949
  {
588
950
  flag: "--framework <name>",
589
- description: "Apply framework-specific public env conventions such as vite or nextjs."
951
+ description: "Apply framework-specific public env conventions such as vite, next, or nuxt."
590
952
  },
591
953
  {
592
954
  flag: "--prefix <prefix>",
@@ -676,6 +1038,31 @@ var COMMANDS = [
676
1038
  }
677
1039
  ],
678
1040
  examples: ["cnos help-ai --format json", "cnos help-ai export env --format json"]
1041
+ },
1042
+ {
1043
+ id: "version",
1044
+ summary: "Print the installed CNOS CLI version.",
1045
+ usage: "cnos version",
1046
+ description: "Prints the installed @kitsy/cnos-cli version string.",
1047
+ examples: ["cnos version", "cnos --version"]
1048
+ }
1049
+ ];
1050
+ var INTEGRATIONS = [
1051
+ {
1052
+ id: "vite",
1053
+ packageName: "@kitsy/cnos-vite",
1054
+ entrypoint: "@kitsy/cnos-vite",
1055
+ summary: "Inject CNOS public env into Vite define replacements and import.meta.env.",
1056
+ usage: 'import { createCnosVitePlugin } from "@kitsy/cnos-vite"',
1057
+ examples: ["cnos export env --public --framework vite", "vite.config.ts -> plugins: [createCnosVitePlugin()]"]
1058
+ },
1059
+ {
1060
+ id: "next",
1061
+ packageName: "@kitsy/cnos-next",
1062
+ entrypoint: "@kitsy/cnos-next",
1063
+ summary: "Merge CNOS public env into next.config.* using the NEXT_PUBLIC_ convention.",
1064
+ usage: 'import { withCnosNext } from "@kitsy/cnos-next"',
1065
+ examples: ["cnos export env --public --framework next", "next.config.mjs -> export default withCnosNext({})"]
679
1066
  }
680
1067
  ];
681
1068
  var HELP_DOCUMENT = {
@@ -685,7 +1072,14 @@ var HELP_DOCUMENT = {
685
1072
  description: "CNOS resolves one active workspace per invocation, layers local and optional global config roots, and exposes read, write, export, dump, validation, and diagnostics commands.",
686
1073
  globalOptions: GLOBAL_OPTIONS,
687
1074
  commands: COMMANDS,
688
- examples: ["cnos doctor --workspace api", "cnos export env --public --framework vite", "cnos help-ai --format json"]
1075
+ integrations: INTEGRATIONS,
1076
+ examples: [
1077
+ "cnos use --profile stage",
1078
+ "cnos doctor --workspace api",
1079
+ "cnos export env --public --framework vite",
1080
+ "cnos export env --public --framework next",
1081
+ "cnos help-ai --format json"
1082
+ ]
689
1083
  };
690
1084
  function normalizeHelpTopic(parts) {
691
1085
  const cleaned = parts.filter((part) => part.length > 0);
@@ -739,6 +1133,11 @@ function formatRootHelp() {
739
1133
  "Commands",
740
1134
  ...HELP_DOCUMENT.commands.filter((command) => !command.id.includes(" ")).map((command) => ` ${command.id.padEnd(12, " ")} ${command.summary}`),
741
1135
  "",
1136
+ "Framework integrations",
1137
+ ...HELP_DOCUMENT.integrations.map(
1138
+ (integration) => ` ${integration.id.padEnd(12, " ")} ${integration.packageName} via ${integration.entrypoint}`
1139
+ ),
1140
+ "",
742
1141
  ...formatOptions("Global options", HELP_DOCUMENT.globalOptions),
743
1142
  "",
744
1143
  "Examples",
@@ -766,6 +1165,7 @@ function runHelpAi(topic, cliArgs = []) {
766
1165
  cli: HELP_DOCUMENT.name,
767
1166
  summary: HELP_DOCUMENT.summary,
768
1167
  globalOptions: HELP_DOCUMENT.globalOptions,
1168
+ integrations: HELP_DOCUMENT.integrations,
769
1169
  command
770
1170
  } : {
771
1171
  cli: HELP_DOCUMENT.name,
@@ -773,7 +1173,8 @@ function runHelpAi(topic, cliArgs = []) {
773
1173
  usage: HELP_DOCUMENT.usage,
774
1174
  description: HELP_DOCUMENT.description,
775
1175
  globalOptions: HELP_DOCUMENT.globalOptions,
776
- commands: HELP_DOCUMENT.commands
1176
+ commands: HELP_DOCUMENT.commands,
1177
+ integrations: HELP_DOCUMENT.integrations
777
1178
  };
778
1179
  if (topic && !command) {
779
1180
  throw new Error(`Unknown help topic: ${topic}`);
@@ -794,25 +1195,32 @@ import path4 from "path";
794
1195
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
795
1196
  import path3 from "path";
796
1197
  function scaffoldManifest(projectName, workspace) {
797
- return [
1198
+ const lines = [
798
1199
  "version: 1",
799
1200
  "project:",
800
1201
  ` name: ${projectName}`,
801
- "workspaces:",
802
- ` default: ${workspace}`,
803
- " global:",
804
- " enabled: false",
805
- " allowWrite: false",
806
- " items:",
807
- ` ${workspace}: {}`,
808
1202
  "profiles:",
809
- " default: local",
1203
+ " default: base",
810
1204
  "envMapping:",
811
1205
  " convention: SCREAMING_SNAKE",
812
1206
  "public:",
813
1207
  " promote: []",
814
1208
  ""
815
- ].join("\n");
1209
+ ];
1210
+ if (workspace) {
1211
+ lines.splice(
1212
+ 4,
1213
+ 0,
1214
+ "workspaces:",
1215
+ ` default: ${workspace}`,
1216
+ " global:",
1217
+ " enabled: false",
1218
+ " allowWrite: false",
1219
+ " items:",
1220
+ ` ${workspace}: {}`
1221
+ );
1222
+ }
1223
+ return lines.join("\n");
816
1224
  }
817
1225
  async function ensureFile(filePath, content) {
818
1226
  try {
@@ -826,11 +1234,14 @@ async function ensureFile(filePath, content) {
826
1234
  async function ensureGitignore(root) {
827
1235
  const gitignorePath = path3.join(root, ".gitignore");
828
1236
  const requiredEntries = [
829
- "cnos/workspaces/*/secrets/",
830
- "cnos/workspaces/*/env/.env",
831
- "cnos/workspaces/*/env/.env.*",
832
- "!cnos/workspaces/*/env/.env.example",
833
- "!cnos/workspaces/*/env/.env.*.example"
1237
+ ".cnos/env/.env",
1238
+ ".cnos/env/.env.*",
1239
+ "!.cnos/env/.env.example",
1240
+ "!.cnos/env/.env.*.example",
1241
+ ".cnos/workspaces/*/env/.env",
1242
+ ".cnos/workspaces/*/env/.env.*",
1243
+ "!.cnos/workspaces/*/env/.env.example",
1244
+ "!.cnos/workspaces/*/env/.env.*.example"
834
1245
  ];
835
1246
  let current = "";
836
1247
  try {
@@ -849,28 +1260,34 @@ async function ensureGitignore(root) {
849
1260
  return true;
850
1261
  }
851
1262
  async function scaffoldWorkspace(root, workspace) {
852
- const cnosRoot = path3.join(root, "cnos");
853
- const workspaceRoot = path3.join(cnosRoot, "workspaces", workspace);
1263
+ const cnosRoot = path3.join(root, ".cnos");
1264
+ const workspaceRoot = workspace ? path3.join(cnosRoot, "workspaces", workspace) : cnosRoot;
854
1265
  const createdPaths = [];
855
1266
  await mkdir2(path3.join(workspaceRoot, "profiles"), { recursive: true });
856
- await mkdir2(path3.join(workspaceRoot, "values", "local"), { recursive: true });
857
- await mkdir2(path3.join(workspaceRoot, "secrets", "local"), { recursive: true });
1267
+ await mkdir2(path3.join(workspaceRoot, "values"), { recursive: true });
1268
+ await mkdir2(path3.join(workspaceRoot, "secrets"), { recursive: true });
858
1269
  await mkdir2(path3.join(workspaceRoot, "env"), { recursive: true });
859
- for (const relativePath of [
1270
+ const relativePaths = workspace ? [
860
1271
  ["workspaces", workspace, "profiles", ".gitkeep"],
861
- ["workspaces", workspace, "values", "local", ".gitkeep"],
862
- ["workspaces", workspace, "secrets", "local", ".gitkeep"],
1272
+ ["workspaces", workspace, "values", ".gitkeep"],
1273
+ ["workspaces", workspace, "secrets", ".gitkeep"],
863
1274
  ["workspaces", workspace, "env", ".gitkeep"]
864
- ]) {
1275
+ ] : [
1276
+ ["profiles", ".gitkeep"],
1277
+ ["values", ".gitkeep"],
1278
+ ["secrets", ".gitkeep"],
1279
+ ["env", ".gitkeep"]
1280
+ ];
1281
+ for (const relativePath of relativePaths) {
865
1282
  const filePath = path3.join(cnosRoot, ...relativePath);
866
1283
  if (await ensureFile(filePath, "")) {
867
1284
  createdPaths.push(path3.relative(root, filePath).replace(/\\/g, "/"));
868
1285
  }
869
1286
  }
870
1287
  if (await ensureFile(path3.join(cnosRoot, "cnos.yml"), scaffoldManifest(path3.basename(root), workspace))) {
871
- createdPaths.push("cnos/cnos.yml");
1288
+ createdPaths.push(".cnos/cnos.yml");
872
1289
  }
873
- if (await ensureFile(path3.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1290
+ if (workspace && await ensureFile(path3.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
874
1291
  globalRoot: ~/.cnos
875
1292
  `)) {
876
1293
  createdPaths.push(".cnos-workspace.yml");
@@ -880,7 +1297,7 @@ globalRoot: ~/.cnos
880
1297
  }
881
1298
  return {
882
1299
  root,
883
- workspace,
1300
+ ...workspace ? { workspace } : {},
884
1301
  created: createdPaths
885
1302
  };
886
1303
  }
@@ -888,12 +1305,14 @@ globalRoot: ~/.cnos
888
1305
  // src/commands/init.ts
889
1306
  async function runInit(options = {}) {
890
1307
  const root = path4.resolve(options.root ?? process.cwd());
891
- const workspace = options.workspace ?? path4.basename(root);
892
- const result = await scaffoldWorkspace(root, workspace);
1308
+ const result = await scaffoldWorkspace(root, options.workspace);
893
1309
  if (options.json) {
894
1310
  return printJson(result);
895
1311
  }
896
- return `initialized CNOS workspace ${workspace} at ${root}`;
1312
+ if (result.workspace) {
1313
+ return `initialized CNOS workspace ${result.workspace} at ${root}`;
1314
+ }
1315
+ return `initialized CNOS project at ${root}`;
897
1316
  }
898
1317
 
899
1318
  // src/format/printInspect.ts
@@ -928,8 +1347,76 @@ async function runInspect(key, options = {}) {
928
1347
  return printInspect(inspectResult);
929
1348
  }
930
1349
 
1350
+ // src/format/printValue.ts
1351
+ function printValue(value, json = false) {
1352
+ if (json) {
1353
+ return printJson(value);
1354
+ }
1355
+ if (typeof value === "string") {
1356
+ return value;
1357
+ }
1358
+ if (value === void 0 || value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1359
+ return String(value);
1360
+ }
1361
+ return printJson(value);
1362
+ }
1363
+
1364
+ // src/services/listing.ts
1365
+ import { flattenObject as flattenObject2 } from "@kitsy/cnos/internal";
1366
+ async function listConfigEntries(namespace, options = {}) {
1367
+ const runtime = await createRuntimeService(options);
1368
+ const namespaces = namespace === "all" ? ["value", "secret", "meta"] : [namespace];
1369
+ const entries = [];
1370
+ const prefix = options.prefix?.trim();
1371
+ for (const currentNamespace of namespaces) {
1372
+ const projected = flattenObject2(runtime.toNamespace(currentNamespace));
1373
+ for (const [path10, value] of Object.entries(projected)) {
1374
+ const key = `${currentNamespace}.${path10}`;
1375
+ if (prefix && !key.startsWith(prefix) && !path10.startsWith(prefix)) {
1376
+ continue;
1377
+ }
1378
+ entries.push({
1379
+ key,
1380
+ value
1381
+ });
1382
+ }
1383
+ }
1384
+ return entries.sort((left, right) => left.key.localeCompare(right.key));
1385
+ }
1386
+
1387
+ // src/commands/list.ts
1388
+ function normalizeNamespace(value) {
1389
+ if (!value || value === "all") {
1390
+ return "all";
1391
+ }
1392
+ if (value === "values" || value === "value") {
1393
+ return "value";
1394
+ }
1395
+ if (value === "secrets" || value === "secret") {
1396
+ return "secret";
1397
+ }
1398
+ if (value === "meta") {
1399
+ return "meta";
1400
+ }
1401
+ throw new Error(`Unsupported list namespace: ${value}`);
1402
+ }
1403
+ async function runList(args = [], options = {}) {
1404
+ const cliArgs = [...options.cliArgs ?? []];
1405
+ const namespace = normalizeNamespace(args[0] ?? consumeOption(cliArgs, "--namespace"));
1406
+ const prefix = consumeOption(cliArgs, "--prefix");
1407
+ const entries = await listConfigEntries(namespace, {
1408
+ ...options,
1409
+ cliArgs,
1410
+ ...prefix ? { prefix } : {}
1411
+ });
1412
+ if (options.json) {
1413
+ return printJson(entries);
1414
+ }
1415
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
1416
+ }
1417
+
931
1418
  // src/commands/onboard.ts
932
- import { copyFile, readdir, rm } from "fs/promises";
1419
+ import { copyFile, readdir, rm as rm2 } from "fs/promises";
933
1420
  import path5 from "path";
934
1421
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
935
1422
  async function listRootEnvFiles(root) {
@@ -945,7 +1432,7 @@ async function runOnboard(options = {}) {
945
1432
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
946
1433
  }
947
1434
  const scaffold = await scaffoldWorkspace(root, workspace);
948
- const envRoot = path5.join(root, "cnos", "workspaces", workspace, "env");
1435
+ const envRoot = path5.join(root, ".cnos", "workspaces", workspace, "env");
949
1436
  const rootFiles = await listRootEnvFiles(root);
950
1437
  const imported = [];
951
1438
  const skipped = [];
@@ -956,7 +1443,7 @@ async function runOnboard(options = {}) {
956
1443
  await copyFile(sourcePath, targetPath);
957
1444
  imported.push(path5.relative(root, targetPath).replace(/\\/g, "/"));
958
1445
  if (move) {
959
- await rm(sourcePath);
1446
+ await rm2(sourcePath);
960
1447
  }
961
1448
  } catch {
962
1449
  skipped.push(fileName);
@@ -975,21 +1462,169 @@ async function runOnboard(options = {}) {
975
1462
  }
976
1463
  const importedCount = imported.length;
977
1464
  const skippedSuffix = skipped.length > 0 ? ` (${skipped.length} skipped)` : "";
978
- return `onboarded ${workspace} at ${root}; imported ${importedCount} root env files into cnos/workspaces/${workspace}/env using ${result.mode}${skippedSuffix}`;
1465
+ return `onboarded ${workspace} at ${root}; imported ${importedCount} root env files into .cnos/workspaces/${workspace}/env using ${result.mode}${skippedSuffix}`;
979
1466
  }
980
1467
 
981
- // src/format/printValue.ts
982
- function printValue(value, json = false) {
983
- if (json) {
984
- return printJson(value);
1468
+ // src/commands/profile.ts
1469
+ import path8 from "path";
1470
+
1471
+ // src/services/context.ts
1472
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1473
+ import path6 from "path";
1474
+ import { parseYaml as parseYaml2, stringifyYaml as stringifyYaml2 } from "@kitsy/cnos/internal";
1475
+ async function loadCliContext(root = process.cwd()) {
1476
+ const filePath = path6.join(path6.resolve(root), ".cnos-workspace.yml");
1477
+ try {
1478
+ const source = await readFile4(filePath, "utf8");
1479
+ const parsed = parseYaml2(source);
1480
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1481
+ return {};
1482
+ }
1483
+ return parsed;
1484
+ } catch {
1485
+ return {};
985
1486
  }
986
- if (typeof value === "string") {
987
- return value;
1487
+ }
1488
+ async function saveCliContext(options = {}) {
1489
+ const root = path6.resolve(options.root ?? process.cwd());
1490
+ const filePath = path6.join(root, ".cnos-workspace.yml");
1491
+ const current = await loadCliContext(root);
1492
+ const next = {
1493
+ ...current.workspace ? { workspace: current.workspace } : {},
1494
+ ...current.profile ? { profile: current.profile } : {},
1495
+ ...current.globalRoot ? { globalRoot: current.globalRoot } : {},
1496
+ ...options.workspace ? { workspace: options.workspace } : {},
1497
+ ...options.profile ? { profile: options.profile } : {},
1498
+ ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
1499
+ };
1500
+ await writeFile3(filePath, stringifyYaml2(next), "utf8");
1501
+ return {
1502
+ filePath,
1503
+ context: next
1504
+ };
1505
+ }
1506
+
1507
+ // src/services/profiles.ts
1508
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "fs/promises";
1509
+ import path7 from "path";
1510
+ import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
1511
+ async function createProfileDefinition(root = process.cwd(), profile, inherit) {
1512
+ const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1513
+ await mkdir3(path7.dirname(filePath), { recursive: true });
1514
+ const document = inherit && inherit !== "base" ? {
1515
+ name: profile,
1516
+ extends: [inherit]
1517
+ } : {
1518
+ name: profile
1519
+ };
1520
+ await writeFile4(filePath, stringifyYaml3(document), "utf8");
1521
+ return {
1522
+ filePath,
1523
+ profile,
1524
+ ...inherit ? { inherit } : {}
1525
+ };
1526
+ }
1527
+ async function listProfiles(root = process.cwd()) {
1528
+ const profilesRoot = path7.join(path7.resolve(root), ".cnos", "profiles");
1529
+ try {
1530
+ const entries = await readdir2(profilesRoot, { withFileTypes: true });
1531
+ const discovered = /* @__PURE__ */ new Set(["base"]);
1532
+ for (const entry of entries) {
1533
+ if (entry.isFile() && entry.name.endsWith(".yml")) {
1534
+ discovered.add(entry.name.replace(/\.yml$/, ""));
1535
+ }
1536
+ }
1537
+ return [...discovered].sort((left, right) => left.localeCompare(right));
1538
+ } catch {
1539
+ return ["base"];
988
1540
  }
989
- if (value === void 0 || value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
990
- return String(value);
1541
+ }
1542
+ async function deleteProfileDefinition(root = process.cwd(), profile) {
1543
+ const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1544
+ try {
1545
+ await rm3(filePath);
1546
+ return {
1547
+ filePath,
1548
+ deleted: true
1549
+ };
1550
+ } catch {
1551
+ return {
1552
+ filePath,
1553
+ deleted: false
1554
+ };
1555
+ }
1556
+ }
1557
+ async function readProfileDefinition(root = process.cwd(), profile = "base") {
1558
+ if (profile === "base") {
1559
+ return {
1560
+ name: "base"
1561
+ };
1562
+ }
1563
+ const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1564
+ try {
1565
+ return parseYaml3(await readFile5(filePath, "utf8")) ?? void 0;
1566
+ } catch {
1567
+ return void 0;
991
1568
  }
992
- return printJson(value);
1569
+ }
1570
+
1571
+ // src/commands/profile.ts
1572
+ function normalizeProfileAction(args) {
1573
+ const [action = "list", ...tail] = args;
1574
+ if (["create", "list", "use", "delete", "remove"].includes(action)) {
1575
+ return {
1576
+ action: action === "remove" ? "delete" : action,
1577
+ tail
1578
+ };
1579
+ }
1580
+ return {
1581
+ action: "list",
1582
+ tail: args
1583
+ };
1584
+ }
1585
+ async function runProfile(args, options = {}) {
1586
+ const { action, tail } = normalizeProfileAction(args);
1587
+ const root = path8.resolve(options.root ?? process.cwd());
1588
+ const cliArgs = [...options.cliArgs ?? []];
1589
+ if (action === "create") {
1590
+ const profile = tail[0] ?? "stage";
1591
+ const inherit = consumeOption(cliArgs, "--inherit");
1592
+ const result = await createProfileDefinition(root, profile, inherit);
1593
+ if (options.json) {
1594
+ return printJson(result);
1595
+ }
1596
+ return `created profile ${profile} at ${result.filePath}`;
1597
+ }
1598
+ if (action === "use") {
1599
+ const profile = tail[0] ?? "base";
1600
+ const result = await saveCliContext({
1601
+ root,
1602
+ profile
1603
+ });
1604
+ if (options.json) {
1605
+ return printJson(result);
1606
+ }
1607
+ return `active profile set to ${profile} in ${result.filePath}`;
1608
+ }
1609
+ if (action === "delete") {
1610
+ const profile = tail[0] ?? "base";
1611
+ const result = await deleteProfileDefinition(root, profile);
1612
+ if (options.json) {
1613
+ return printJson(result);
1614
+ }
1615
+ return result.deleted ? `deleted profile ${profile}` : `profile ${profile} was not found`;
1616
+ }
1617
+ const profiles = await listProfiles(root);
1618
+ if (options.json) {
1619
+ const details = await Promise.all(
1620
+ profiles.map(async (profile) => ({
1621
+ profile,
1622
+ definition: await readProfileDefinition(root, profile)
1623
+ }))
1624
+ );
1625
+ return printJson(details);
1626
+ }
1627
+ return profiles.join("\n");
993
1628
  }
994
1629
 
995
1630
  // src/commands/read.ts
@@ -1050,21 +1685,113 @@ async function runCommand(command, options = {}) {
1050
1685
  }
1051
1686
 
1052
1687
  // src/commands/secret.ts
1053
- async function runSecret(path6, options = {}) {
1688
+ function isSecretRef(value) {
1689
+ return Boolean(
1690
+ value && typeof value === "object" && !Array.isArray(value) && typeof value.provider === "string" && typeof value.ref === "string"
1691
+ );
1692
+ }
1693
+ function normalizeSecretCommand(args) {
1694
+ const [actionOrPath, ...tail] = args;
1695
+ if (!actionOrPath) {
1696
+ return {
1697
+ action: "list",
1698
+ tail: []
1699
+ };
1700
+ }
1701
+ if (["get", "set", "create", "add", "list", "delete", "remove"].includes(actionOrPath)) {
1702
+ return {
1703
+ action: actionOrPath === "remove" ? "delete" : actionOrPath === "create" || actionOrPath === "add" ? "set" : actionOrPath,
1704
+ tail
1705
+ };
1706
+ }
1707
+ return {
1708
+ action: "get",
1709
+ tail: args
1710
+ };
1711
+ }
1712
+ async function runSecret(argsOrPath, options = {}) {
1713
+ const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
1714
+ const { action, tail } = normalizeSecretCommand(args);
1715
+ const cliArgs = [...options.cliArgs ?? []];
1716
+ if (action === "list") {
1717
+ const runtime2 = await createRuntimeService(options);
1718
+ const secrets = runtime2.toNamespace("secret");
1719
+ if (options.json) {
1720
+ return printJson(secrets);
1721
+ }
1722
+ return Object.entries(secrets).sort(([left], [right]) => left.localeCompare(right)).map(([key, value2]) => `${key}=${printValue(value2)}`).join("\n");
1723
+ }
1724
+ if (action === "set") {
1725
+ const secretPath = tail[0];
1726
+ const rawValue = tail[1] ?? "";
1727
+ const local = consumeFlag(cliArgs, "--local");
1728
+ const remote = consumeFlag(cliArgs, "--remote");
1729
+ const ref = consumeFlag(cliArgs, "--ref");
1730
+ const target = consumeOption(cliArgs, "--target") ?? "local";
1731
+ const provider = consumeOption(cliArgs, "--provider");
1732
+ const passphrase = consumeOption(cliArgs, "--passphrase");
1733
+ const mode = local ? "local" : remote ? "remote" : ref ? "ref" : "local";
1734
+ const result = await setSecret(secretPath ?? "app.token", rawValue, {
1735
+ ...options,
1736
+ cliArgs,
1737
+ target,
1738
+ mode,
1739
+ ...provider ? { provider } : {},
1740
+ ...passphrase ? { passphrase } : {}
1741
+ });
1742
+ if (options.json) {
1743
+ return printJson(result);
1744
+ }
1745
+ return `set secret.${secretPath} via ${result.provider} in ${result.filePath}`;
1746
+ }
1747
+ if (action === "delete") {
1748
+ const secretPath = tail[0];
1749
+ const target = consumeOption(cliArgs, "--target") ?? "local";
1750
+ const result = await deleteSecret(secretPath ?? "app.token", {
1751
+ ...options,
1752
+ cliArgs,
1753
+ target
1754
+ });
1755
+ if (options.json) {
1756
+ return printJson(result);
1757
+ }
1758
+ return result.deleted ? `deleted secret.${secretPath} from ${result.filePath}` : `no secret.${secretPath} found in ${result.filePath}`;
1759
+ }
1054
1760
  const runtime = await createRuntimeService(options);
1055
- const value = runtime.secret(path6);
1761
+ const value = runtime.secret(tail[0] ?? "app.token");
1056
1762
  if (value === void 0) {
1057
- throw new Error(`Missing CNOS secret path: ${path6}`);
1763
+ throw new Error(`Missing CNOS secret path: ${tail[0] ?? "app.token"}`);
1764
+ }
1765
+ if (isSecretRef(value)) {
1766
+ throw new Error(
1767
+ `Secret ${tail[0] ?? "app.token"} is stored as an unresolved ${value.provider} reference. Provide the required provider context to resolve it.`
1768
+ );
1058
1769
  }
1059
1770
  if (options.json) {
1060
1771
  return printJson({
1061
- key: `secret.${path6}`,
1772
+ key: `secret.${tail[0] ?? "app.token"}`,
1062
1773
  value
1063
1774
  });
1064
1775
  }
1065
1776
  return printValue(value);
1066
1777
  }
1067
1778
 
1779
+ // src/commands/use.ts
1780
+ import path9 from "path";
1781
+ async function runUse(options = {}) {
1782
+ const root = path9.resolve(options.root ?? process.cwd());
1783
+ const result = await saveCliContext({
1784
+ root,
1785
+ ...options.workspace ? { workspace: options.workspace } : {},
1786
+ ...options.profile ? { profile: options.profile } : {},
1787
+ ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
1788
+ });
1789
+ if (options.json) {
1790
+ return printJson(result);
1791
+ }
1792
+ return `updated CLI context in ${result.filePath}`;
1793
+ }
1794
+
1068
1795
  // src/commands/validate.ts
1069
1796
  async function runValidate(options = {}) {
1070
1797
  const { summary } = await createValidationSummary(options);
@@ -1077,16 +1804,139 @@ async function runValidate(options = {}) {
1077
1804
  return summary.valid ? "validation passed" : summary.issues.map((issue) => `${issue.code}: ${issue.message}`).join("\n");
1078
1805
  }
1079
1806
 
1807
+ // package.json
1808
+ var package_default = {
1809
+ name: "@kitsy/cnos-cli",
1810
+ version: "1.0.1",
1811
+ description: "CLI entry point and developer tooling for CNOS.",
1812
+ type: "module",
1813
+ main: "./dist/index.js",
1814
+ types: "./dist/index.d.ts",
1815
+ bin: {
1816
+ cnos: "./dist/index.js"
1817
+ },
1818
+ exports: {
1819
+ ".": {
1820
+ types: "./dist/index.d.ts",
1821
+ import: "./dist/index.js"
1822
+ }
1823
+ },
1824
+ files: [
1825
+ "dist"
1826
+ ],
1827
+ license: "MIT",
1828
+ repository: {
1829
+ type: "git",
1830
+ url: "https://github.com/kitsyai/cnos.git",
1831
+ directory: "packages/cli"
1832
+ },
1833
+ homepage: "https://github.com/kitsyai/cnos/tree/main/packages/cli",
1834
+ bugs: {
1835
+ url: "https://github.com/kitsyai/cnos/issues"
1836
+ },
1837
+ keywords: [
1838
+ "cnos",
1839
+ "cli",
1840
+ "config"
1841
+ ],
1842
+ publishConfig: {
1843
+ access: "public"
1844
+ },
1845
+ dependencies: {
1846
+ "@kitsy/cnos": "workspace:*"
1847
+ },
1848
+ scripts: {
1849
+ build: "tsup src/index.ts --format esm --dts",
1850
+ clean: "rimraf dist",
1851
+ dev: "tsup src/index.ts --watch --format esm --dts",
1852
+ lint: "eslint src test",
1853
+ test: "vitest run",
1854
+ typecheck: "tsc -p tsconfig.json --noEmit"
1855
+ }
1856
+ };
1857
+
1858
+ // src/commands/version.ts
1859
+ function runVersion() {
1860
+ return package_default.version;
1861
+ }
1862
+
1080
1863
  // src/commands/value.ts
1081
- async function runValue(path6, options = {}) {
1864
+ function normalizeValueCommand(args) {
1865
+ const [actionOrPath, ...tail] = args;
1866
+ if (!actionOrPath) {
1867
+ return {
1868
+ action: "list",
1869
+ tail: []
1870
+ };
1871
+ }
1872
+ if (["get", "set", "create", "add", "list", "delete", "remove"].includes(actionOrPath)) {
1873
+ return {
1874
+ action: actionOrPath === "remove" ? "delete" : actionOrPath === "create" || actionOrPath === "add" ? "set" : actionOrPath,
1875
+ tail
1876
+ };
1877
+ }
1878
+ return {
1879
+ action: "get",
1880
+ tail: args
1881
+ };
1882
+ }
1883
+ async function runValue(argsOrPath, options = {}) {
1884
+ const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
1885
+ const { action, tail } = normalizeValueCommand(args);
1886
+ const cliArgs = [...options.cliArgs ?? []];
1887
+ if (action === "list") {
1888
+ const prefix = consumeOption(cliArgs, "--prefix");
1889
+ const entries = await listConfigEntries("value", {
1890
+ ...options,
1891
+ cliArgs,
1892
+ ...prefix ? { prefix } : {}
1893
+ });
1894
+ if (options.json) {
1895
+ return printJson(entries);
1896
+ }
1897
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
1898
+ }
1899
+ if (action === "set") {
1900
+ const valuePath = tail[0] ?? "app.name";
1901
+ const rawValue = tail[1] ?? "";
1902
+ const target = consumeOption(cliArgs, "--target") ?? "local";
1903
+ const result = await defineValue("value", valuePath, rawValue, {
1904
+ ...options,
1905
+ cliArgs,
1906
+ target
1907
+ });
1908
+ if (options.json) {
1909
+ return printJson({
1910
+ namespace: "value",
1911
+ path: valuePath,
1912
+ target,
1913
+ filePath: result.filePath,
1914
+ value: result.value
1915
+ });
1916
+ }
1917
+ return `set value.${valuePath} in ${result.filePath}`;
1918
+ }
1919
+ if (action === "delete") {
1920
+ const valuePath = tail[0] ?? "app.name";
1921
+ const target = consumeOption(cliArgs, "--target") ?? "local";
1922
+ const result = await deleteValue("value", valuePath, {
1923
+ ...options,
1924
+ cliArgs,
1925
+ target
1926
+ });
1927
+ if (options.json) {
1928
+ return printJson(result);
1929
+ }
1930
+ return result.deleted ? `deleted value.${valuePath} from ${result.filePath}` : `no value.${valuePath} found in ${result.filePath}`;
1931
+ }
1082
1932
  const runtime = await createRuntimeService(options);
1083
- const value = runtime.value(path6);
1933
+ const value = runtime.value(tail[0] ?? "app.name");
1084
1934
  if (value === void 0) {
1085
- throw new Error(`Missing CNOS value path: ${path6}`);
1935
+ throw new Error(`Missing CNOS value path: ${tail[0] ?? "app.name"}`);
1086
1936
  }
1087
1937
  if (options.json) {
1088
1938
  return printJson({
1089
- key: `value.${path6}`,
1939
+ key: `value.${tail[0] ?? "app.name"}`,
1090
1940
  value
1091
1941
  });
1092
1942
  }
@@ -1101,6 +1951,21 @@ function resolveHelpTopic(command, args) {
1101
1951
  if (command === "export" && args[0] === "env") {
1102
1952
  return normalizeHelpTopic([command, args[0]]);
1103
1953
  }
1954
+ if (command === "secret" && args[0] && ["set", "create", "add", "list", "delete", "remove"].includes(args[0])) {
1955
+ return normalizeHelpTopic([
1956
+ command,
1957
+ args[0] === "remove" ? "delete" : args[0] === "create" || args[0] === "add" ? "set" : args[0]
1958
+ ]);
1959
+ }
1960
+ if (command === "value" && args[0] && ["set", "create", "add", "list", "delete", "remove"].includes(args[0])) {
1961
+ return normalizeHelpTopic([
1962
+ command,
1963
+ args[0] === "remove" ? "delete" : args[0] === "create" || args[0] === "add" ? "set" : args[0]
1964
+ ]);
1965
+ }
1966
+ if (command === "profile" && args[0] && ["create", "list", "use", "delete", "remove"].includes(args[0])) {
1967
+ return normalizeHelpTopic([command, args[0] === "remove" ? "delete" : args[0]]);
1968
+ }
1104
1969
  return normalizeHelpTopic([command]);
1105
1970
  }
1106
1971
  async function main(argv) {
@@ -1137,6 +2002,10 @@ async function main(argv) {
1137
2002
  return;
1138
2003
  case "help-ai":
1139
2004
  process.stdout.write(`${runHelpAi(resolveHelpTopic(command, args), options.cliArgs)}
2005
+ `);
2006
+ return;
2007
+ case "version":
2008
+ process.stdout.write(`${runVersion()}
1140
2009
  `);
1141
2010
  return;
1142
2011
  case "init":
@@ -1152,11 +2021,23 @@ async function main(argv) {
1152
2021
  `);
1153
2022
  return;
1154
2023
  case "value":
1155
- process.stdout.write(`${await runValue(args[0] ?? "app.name", runtimeOptions)}
2024
+ process.stdout.write(`${await runValue(args.length > 0 ? args : ["app.name"], runtimeOptions)}
1156
2025
  `);
1157
2026
  return;
1158
2027
  case "secret":
1159
- process.stdout.write(`${await runSecret(args[0] ?? "app.token", runtimeOptions)}
2028
+ process.stdout.write(`${await runSecret(args.length > 0 ? args : ["app.token"], runtimeOptions)}
2029
+ `);
2030
+ return;
2031
+ case "use":
2032
+ process.stdout.write(`${await runUse(runtimeOptions)}
2033
+ `);
2034
+ return;
2035
+ case "profile":
2036
+ process.stdout.write(`${await runProfile(args, runtimeOptions)}
2037
+ `);
2038
+ return;
2039
+ case "list":
2040
+ process.stdout.write(`${await runList(args, runtimeOptions)}
1160
2041
  `);
1161
2042
  return;
1162
2043
  case "define":