@kitsy/cnos-cli 0.0.1 → 1.0.0

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 +903 -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,8 @@ function parseArgs(argv) {
23
65
  passthrough: []
24
66
  };
25
67
  }
26
- const [command = "doctor", ...rest] = argv;
68
+ const normalizedArgv = normalizeCommand(argv);
69
+ const [command = "doctor", ...rest] = normalizedArgv;
27
70
  const options = {};
28
71
  const args = [];
29
72
  const cliArgs = [];
@@ -128,15 +171,24 @@ function printJson(value) {
128
171
  }
129
172
 
130
173
  // src/services/writes.ts
131
- import { mkdir, readFile, writeFile } from "fs/promises";
174
+ import { randomUUID } from "crypto";
175
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
132
176
  import path from "path";
133
- import { parseYaml, resolveWorkspaceScopedPath, stringifyYaml } from "@kitsy/cnos-core";
177
+ import {
178
+ parseYaml,
179
+ resolveConfigDocumentPath,
180
+ resolveSecretStoreRoot,
181
+ stringifyYaml,
182
+ writeLocalSecret
183
+ } from "@kitsy/cnos/internal";
134
184
 
135
185
  // src/services/runtime.ts
136
186
  import { createCnos } from "@kitsy/cnos";
137
187
  async function createRuntimeService(options = {}) {
138
188
  const createOptions = {
139
- root: options.root ?? process.cwd(),
189
+ ...options.root ? {
190
+ root: options.root
191
+ } : {},
140
192
  ...options.workspace ? {
141
193
  workspace: options.workspace
142
194
  } : {},
@@ -181,8 +233,38 @@ function parseScalarValue(rawValue) {
181
233
  return rawValue;
182
234
  }
183
235
  }
184
- async function defineValue(namespace, configPath, rawValue, options = {}) {
185
- const runtime = await createRuntimeService(options);
236
+ function unsetNestedValue(target, pathSegments) {
237
+ const [head, ...tail] = pathSegments;
238
+ if (!head || !(head in target)) {
239
+ return false;
240
+ }
241
+ if (tail.length === 0) {
242
+ delete target[head];
243
+ return true;
244
+ }
245
+ const nextValue = target[head];
246
+ if (!nextValue || typeof nextValue !== "object" || Array.isArray(nextValue)) {
247
+ return false;
248
+ }
249
+ const deleted = unsetNestedValue(nextValue, tail);
250
+ if (deleted && Object.keys(nextValue).length === 0) {
251
+ delete target[head];
252
+ }
253
+ return deleted;
254
+ }
255
+ function isSecretReference(value) {
256
+ return Boolean(
257
+ value && typeof value === "object" && !Array.isArray(value) && typeof value.provider === "string" && typeof value.ref === "string"
258
+ );
259
+ }
260
+ async function readYamlDocument(filePath) {
261
+ try {
262
+ return parseYaml(await readFile(filePath, "utf8")) ?? {};
263
+ } catch {
264
+ return {};
265
+ }
266
+ }
267
+ function getSelectedWorkspaceRoot(options, runtime) {
186
268
  const target = options.target ?? "local";
187
269
  const workspaceRoot = runtime.graph.workspace.workspaceRoots.find(
188
270
  (entry) => entry.scope === target && entry.workspaceId === runtime.graph.workspace.workspaceId
@@ -193,18 +275,27 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
193
275
  if (target === "global" && !runtime.manifest.workspaces.global.allowWrite) {
194
276
  throw new Error("Global writes require workspaces.global.allowWrite: true");
195
277
  }
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 = {};
278
+ return workspaceRoot.path;
279
+ }
280
+ async function defineValue(namespace, configPath, rawValue, options = {}) {
281
+ if (namespace === "secret") {
282
+ const secret = await setSecret(configPath, rawValue, {
283
+ ...options,
284
+ mode: options.mode ?? "local"
285
+ });
286
+ return {
287
+ filePath: secret.filePath,
288
+ value: {
289
+ provider: secret.provider,
290
+ ref: secret.ref
291
+ }
292
+ };
207
293
  }
294
+ const runtime = await createRuntimeService(options);
295
+ const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
296
+ const profile = options.profile ?? runtime.graph.profile;
297
+ const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
298
+ const document = await readYamlDocument(filePath);
208
299
  const parsedValue = parseScalarValue(rawValue);
209
300
  setNestedValue(document, configPath.split("."), parsedValue);
210
301
  await mkdir(path.dirname(filePath), { recursive: true });
@@ -214,15 +305,118 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
214
305
  value: parsedValue
215
306
  };
216
307
  }
308
+ async function setSecret(configPath, rawValue, options = {}) {
309
+ const runtime = await createRuntimeService(options);
310
+ const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
311
+ const profile = options.profile ?? runtime.graph.profile;
312
+ const filePath = resolveConfigDocumentPath(workspaceRoot, "secret", configPath, profile);
313
+ const document = await readYamlDocument(filePath);
314
+ const mode = options.mode ?? "local";
315
+ let reference;
316
+ let storePath;
317
+ if (mode === "local") {
318
+ const passphrase = options.passphrase ?? options.processEnv?.CNOS_SECRET_PASSPHRASE ?? process.env.CNOS_SECRET_PASSPHRASE;
319
+ if (!passphrase) {
320
+ throw new Error("Local secret writes require --passphrase or CNOS_SECRET_PASSPHRASE");
321
+ }
322
+ const profileSegment = profile && profile !== "base" ? profile : "base";
323
+ const ref = [
324
+ runtime.manifest.project.name,
325
+ runtime.graph.workspace.workspaceId,
326
+ profileSegment,
327
+ ...configPath.split("."),
328
+ randomUUID()
329
+ ].map((segment) => segment.replace(/[^A-Za-z0-9._-]+/g, "-")).join("/");
330
+ storePath = await writeLocalSecret(resolveSecretStoreRoot(options.processEnv), ref, rawValue, passphrase);
331
+ reference = {
332
+ provider: "local",
333
+ ref
334
+ };
335
+ } else {
336
+ reference = {
337
+ provider: options.provider ?? (mode === "ref" ? "ref" : "remote"),
338
+ ref: rawValue
339
+ };
340
+ }
341
+ setNestedValue(document, configPath.split("."), reference);
342
+ await mkdir(path.dirname(filePath), { recursive: true });
343
+ await writeFile(filePath, stringifyYaml(document), "utf8");
344
+ return {
345
+ filePath,
346
+ provider: reference.provider,
347
+ ref: reference.ref,
348
+ ...storePath ? { storePath } : {}
349
+ };
350
+ }
351
+ async function deleteSecret(configPath, options = {}) {
352
+ const runtime = await createRuntimeService(options);
353
+ const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
354
+ const profile = options.profile ?? runtime.graph.profile;
355
+ const filePath = resolveConfigDocumentPath(workspaceRoot, "secret", configPath, profile);
356
+ const document = await readYamlDocument(filePath);
357
+ const metadata = runtime.graph.entries.get(`secret.${configPath}`)?.winner.metadata;
358
+ const deleted = unsetNestedValue(document, configPath.split("."));
359
+ if (!deleted) {
360
+ return {
361
+ filePath,
362
+ deleted: false
363
+ };
364
+ }
365
+ await writeFile(filePath, stringifyYaml(document), "utf8");
366
+ let removedStore;
367
+ const secretRef = metadata?.secretRef;
368
+ if (isSecretReference(secretRef) && secretRef.provider === "local") {
369
+ const storePath = path.join(resolveSecretStoreRoot(options.processEnv), "store", ...secretRef.ref.split("/")).concat(".json");
370
+ await rm(storePath, { force: true });
371
+ removedStore = storePath;
372
+ }
373
+ return {
374
+ filePath,
375
+ deleted: true,
376
+ ...removedStore ? { removedStore } : {}
377
+ };
378
+ }
379
+ async function deleteValue(namespace, configPath, options = {}) {
380
+ if (namespace === "secret") {
381
+ return deleteSecret(configPath, options);
382
+ }
383
+ const runtime = await createRuntimeService(options);
384
+ const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
385
+ const profile = options.profile ?? runtime.graph.profile;
386
+ const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
387
+ const document = await readYamlDocument(filePath);
388
+ const deleted = unsetNestedValue(document, configPath.split("."));
389
+ if (!deleted) {
390
+ return {
391
+ filePath,
392
+ deleted: false
393
+ };
394
+ }
395
+ await writeFile(filePath, stringifyYaml(document), "utf8");
396
+ return {
397
+ filePath,
398
+ deleted: true
399
+ };
400
+ }
217
401
 
218
402
  // src/commands/define.ts
219
403
  async function runDefine(namespace, configPath, rawValue, options = {}) {
220
404
  const cliArgs = [...options.cliArgs ?? []];
221
405
  const target = consumeOption(cliArgs, "--target") ?? "local";
406
+ const local = consumeFlag(cliArgs, "--local");
407
+ const remote = consumeFlag(cliArgs, "--remote");
408
+ const ref = consumeFlag(cliArgs, "--ref");
409
+ const provider = consumeOption(cliArgs, "--provider");
410
+ const passphrase = consumeOption(cliArgs, "--passphrase");
222
411
  const result = await defineValue(namespace, configPath, rawValue, {
223
412
  ...options,
224
413
  cliArgs,
225
- target
414
+ target,
415
+ ...namespace === "secret" ? {
416
+ mode: local ? "local" : remote ? "remote" : ref ? "ref" : "local",
417
+ ...provider ? { provider } : {},
418
+ ...passphrase ? { passphrase } : {}
419
+ } : {}
226
420
  });
227
421
  if (options.json) {
228
422
  return printJson({
@@ -237,7 +431,7 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
237
431
  }
238
432
 
239
433
  // src/commands/diff.ts
240
- import { flattenObject } from "@kitsy/cnos-core";
434
+ import { flattenObject } from "@kitsy/cnos/internal";
241
435
  function flattenRuntime(runtime) {
242
436
  return {
243
437
  ...Object.fromEntries(
@@ -278,7 +472,7 @@ import { readFile as readFile2 } from "fs/promises";
278
472
  import path2 from "path";
279
473
 
280
474
  // src/services/validation.ts
281
- import { validateRuntime } from "@kitsy/cnos-core";
475
+ import { validateRuntime } from "@kitsy/cnos/internal";
282
476
  async function createValidationSummary(options = {}) {
283
477
  const runtime = await createRuntimeService(options);
284
478
  const summary = await validateRuntime(runtime);
@@ -292,11 +486,14 @@ async function createValidationSummary(options = {}) {
292
486
  async function checkGitignore(root) {
293
487
  const gitignorePath = path2.join(root, ".gitignore");
294
488
  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"
489
+ ".cnos/env/.env",
490
+ ".cnos/env/.env.*",
491
+ "!.cnos/env/.env.example",
492
+ "!.cnos/env/.env.*.example",
493
+ ".cnos/workspaces/*/env/.env",
494
+ ".cnos/workspaces/*/env/.env.*",
495
+ "!.cnos/workspaces/*/env/.env.example",
496
+ "!.cnos/workspaces/*/env/.env.*.example"
300
497
  ];
301
498
  try {
302
499
  const content = await readFile2(gitignorePath, "utf8");
@@ -448,14 +645,14 @@ var COMMANDS = [
448
645
  id: "init",
449
646
  summary: "Scaffold a workspace-aware CNOS tree in the current project.",
450
647
  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.",
648
+ description: "Creates .cnos/cnos.yml, optional .cnos-workspace.yml, config folders, and .gitignore entries without overwriting existing files.",
452
649
  examples: ["cnos init", "cnos init --workspace api", "cnos init --root ./apps/api --workspace api --json"]
453
650
  },
454
651
  {
455
652
  id: "onboard",
456
653
  summary: "Onboard an existing repo into CNOS and import root dotenv files.",
457
654
  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.",
655
+ description: "Scaffolds the CNOS workspace tree and imports root-level .env, .env.<profile>, and .env.*.example files into .cnos/workspaces/<workspace>/env.",
459
656
  options: [
460
657
  {
461
658
  flag: "--move",
@@ -480,37 +677,98 @@ var COMMANDS = [
480
677
  },
481
678
  {
482
679
  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.",
680
+ summary: "Read, write, list, and delete values.",
681
+ usage: "cnos value [get <path> | set <path> <value> | list | delete <path>] [options] [global-options]",
682
+ description: "Reads and manages value.<path> entries from the selected workspace and profile.",
486
683
  arguments: [
487
684
  {
488
685
  name: "path",
489
- description: "Value path without the value. namespace prefix.",
490
- required: true
686
+ description: "Value path without the value. namespace prefix."
687
+ }
688
+ ],
689
+ options: [
690
+ {
691
+ flag: "--target <local|global>",
692
+ description: "Choose whether writes land in the local project workspace or the configured global root."
693
+ },
694
+ {
695
+ flag: "--prefix <path>",
696
+ description: "Filter value list output to keys that begin with this logical path or key prefix."
491
697
  }
492
698
  ],
493
- examples: ["cnos value app.name", "cnos value server.port --profile stage --workspace api"]
699
+ examples: [
700
+ "cnos value app.name",
701
+ "cnos value set server.port 3000",
702
+ "cnos add value app.name demo",
703
+ "cnos value list --prefix app."
704
+ ]
705
+ },
706
+ {
707
+ id: "value set",
708
+ summary: "Write a value.",
709
+ usage: "cnos value set <path> <value> [--target <local|global>] [global-options]",
710
+ description: "Writes a deterministic value document into the selected workspace or explicit global target.",
711
+ examples: ["cnos value set app.name demo", "cnos add value server.port 3000 --target global"]
712
+ },
713
+ {
714
+ id: "value list",
715
+ summary: "List resolved values.",
716
+ usage: "cnos value list [--prefix <path>] [global-options]",
717
+ description: "Lists resolved value keys for the selected workspace and profile.",
718
+ examples: ["cnos value list", "cnos value list --prefix app."]
719
+ },
720
+ {
721
+ id: "value delete",
722
+ summary: "Delete a value entry.",
723
+ usage: "cnos value delete <path> [--target <local|global>] [global-options]",
724
+ description: "Deletes a value from the selected workspace or explicit global target.",
725
+ examples: ["cnos value delete app.name", "cnos remove value server.port"]
494
726
  },
495
727
  {
496
728
  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.",
729
+ summary: "Read, write, list, and delete secrets.",
730
+ usage: "cnos secret [get <path> | set <path> <value> | list | delete <path>] [options] [global-options]",
731
+ description: "Reads resolved secrets, writes secret refs plus external local-secret material, and manages secret entries for the selected workspace and profile.",
500
732
  arguments: [
501
733
  {
502
734
  name: "path",
503
- description: "Secret path without the secret. namespace prefix.",
504
- required: true
735
+ description: "Secret path without the secret. namespace prefix."
505
736
  }
506
737
  ],
507
- examples: ["cnos secret app.token", "cnos secret service.apiKey --workspace agents"]
738
+ options: [
739
+ {
740
+ flag: "--local",
741
+ description: "Store encrypted secret material under ~/.cnos/secrets and write a local ref into the repo."
742
+ },
743
+ {
744
+ flag: "--remote",
745
+ description: "Write a remote secret reference into the repo."
746
+ },
747
+ {
748
+ flag: "--ref",
749
+ description: "Write a generic secret reference into the repo."
750
+ },
751
+ {
752
+ flag: "--provider <name>",
753
+ description: "Provider name for --remote or --ref secret writes."
754
+ },
755
+ {
756
+ flag: "--passphrase <value>",
757
+ description: "Passphrase used to encrypt local secret material when --local is selected."
758
+ }
759
+ ],
760
+ examples: [
761
+ "cnos secret app.token",
762
+ "cnos secret set app.token super-secret --local --passphrase dev-pass",
763
+ "cnos add secret app.token APP_TOKEN --ref --provider env",
764
+ "cnos secret list --workspace agents"
765
+ ]
508
766
  },
509
767
  {
510
768
  id: "define",
511
769
  summary: "Write a value or secret into the selected workspace.",
512
770
  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.",
771
+ 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
772
  arguments: [
515
773
  {
516
774
  name: "namespace",
@@ -536,9 +794,99 @@ var COMMANDS = [
536
794
  ],
537
795
  examples: [
538
796
  "cnos define value server.port 3000 --workspace api",
539
- "cnos define secret app.token super-secret --workspace api --target global"
797
+ "cnos define secret app.token super-secret --workspace api"
540
798
  ]
541
799
  },
800
+ {
801
+ id: "use",
802
+ summary: "Persist repo-local CLI defaults such as workspace and profile.",
803
+ usage: "cnos use [--workspace <id>] [--profile <name>] [--global-root <path>] [--root <path>] [--json]",
804
+ description: "Writes .cnos-workspace.yml so future CLI invocations can omit repeated workspace/profile/global-root flags.",
805
+ examples: ["cnos use --workspace api --profile stage", "cnos use --global-root ~/.cnos"]
806
+ },
807
+ {
808
+ id: "list",
809
+ summary: "List resolved config entries.",
810
+ usage: "cnos list [value|secret|meta|all] [--prefix <path>] [global-options]",
811
+ description: "Lists resolved config entries across one namespace or the full effective graph, with optional key-prefix filtering.",
812
+ options: [
813
+ {
814
+ flag: "--namespace <value|secret|meta|all>",
815
+ description: "Explicit namespace selector when not using a positional namespace argument."
816
+ },
817
+ {
818
+ flag: "--prefix <path>",
819
+ description: "Filter list output to entries whose logical keys begin with this prefix."
820
+ }
821
+ ],
822
+ examples: ["cnos list", "cnos list value --prefix app.", "cnos list --namespace secret"]
823
+ },
824
+ {
825
+ id: "profile",
826
+ summary: "Manage CNOS profiles.",
827
+ usage: "cnos profile [create <name> | list | use <name> | delete <name>] [options] [global-options]",
828
+ description: "Creates and lists explicit profiles and stores the active repo-local profile selection for CLI usage.",
829
+ options: [
830
+ {
831
+ flag: "--inherit <name>",
832
+ description: "Parent profile to extend when creating a profile."
833
+ }
834
+ ],
835
+ examples: [
836
+ "cnos profile create stage --inherit base",
837
+ "cnos profile list",
838
+ "cnos profile use stage"
839
+ ]
840
+ },
841
+ {
842
+ id: "profile create",
843
+ summary: "Create a profile definition.",
844
+ usage: "cnos profile create <name> [--inherit <name>] [--root <path>] [--json]",
845
+ description: "Creates .cnos/profiles/<name>.yml for an explicit profile overlay.",
846
+ examples: ["cnos profile create stage --inherit base"]
847
+ },
848
+ {
849
+ id: "profile list",
850
+ summary: "List available profiles.",
851
+ usage: "cnos profile list [--root <path>] [--json]",
852
+ description: "Lists the base profile plus any explicit profile definition files in .cnos/profiles.",
853
+ examples: ["cnos profile list"]
854
+ },
855
+ {
856
+ id: "profile use",
857
+ summary: "Persist the active profile for this repo.",
858
+ usage: "cnos profile use <name> [--root <path>] [--json]",
859
+ description: "Writes the selected profile into .cnos-workspace.yml.",
860
+ examples: ["cnos profile use stage"]
861
+ },
862
+ {
863
+ id: "profile delete",
864
+ summary: "Delete a profile definition.",
865
+ usage: "cnos profile delete <name> [--root <path>] [--json]",
866
+ description: "Deletes .cnos/profiles/<name>.yml.",
867
+ examples: ["cnos profile delete stage"]
868
+ },
869
+ {
870
+ id: "secret set",
871
+ summary: "Write a secret securely.",
872
+ usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--provider <name>] [--passphrase <value>] [global-options]",
873
+ description: "Writes a secret reference into the repo and, when --local is used, stores encrypted secret material outside the repo under ~/.cnos/secrets.",
874
+ examples: ["cnos secret set app.token super-secret --local --passphrase dev-pass"]
875
+ },
876
+ {
877
+ id: "secret list",
878
+ summary: "List resolved secrets.",
879
+ usage: "cnos secret list [global-options]",
880
+ description: "Lists resolved secret keys for the selected workspace and profile.",
881
+ examples: ["cnos secret list --workspace api"]
882
+ },
883
+ {
884
+ id: "secret delete",
885
+ summary: "Delete a secret reference.",
886
+ usage: "cnos secret delete <path> [--target <local|global>] [global-options]",
887
+ description: "Deletes the secret reference from the repo and removes local encrypted material when the secret used the local provider.",
888
+ examples: ["cnos secret delete app.token"]
889
+ },
542
890
  {
543
891
  id: "inspect",
544
892
  summary: "Inspect the winning value and provenance for a key.",
@@ -572,7 +920,11 @@ var COMMANDS = [
572
920
  required: true
573
921
  }
574
922
  ],
575
- examples: ["cnos export env", "cnos export env --public --framework vite --workspace api"]
923
+ examples: [
924
+ "cnos export env",
925
+ "cnos export env --public --framework vite --workspace api",
926
+ "cnos export env --public --framework next --workspace webapp"
927
+ ]
576
928
  },
577
929
  {
578
930
  id: "export env",
@@ -586,7 +938,7 @@ var COMMANDS = [
586
938
  },
587
939
  {
588
940
  flag: "--framework <name>",
589
- description: "Apply framework-specific public env conventions such as vite or nextjs."
941
+ description: "Apply framework-specific public env conventions such as vite, next, or nuxt."
590
942
  },
591
943
  {
592
944
  flag: "--prefix <prefix>",
@@ -678,6 +1030,24 @@ var COMMANDS = [
678
1030
  examples: ["cnos help-ai --format json", "cnos help-ai export env --format json"]
679
1031
  }
680
1032
  ];
1033
+ var INTEGRATIONS = [
1034
+ {
1035
+ id: "vite",
1036
+ packageName: "@kitsy/cnos-vite",
1037
+ entrypoint: "@kitsy/cnos-vite",
1038
+ summary: "Inject CNOS public env into Vite define replacements and import.meta.env.",
1039
+ usage: 'import { createCnosVitePlugin } from "@kitsy/cnos-vite"',
1040
+ examples: ["cnos export env --public --framework vite", "vite.config.ts -> plugins: [createCnosVitePlugin()]"]
1041
+ },
1042
+ {
1043
+ id: "next",
1044
+ packageName: "@kitsy/cnos-next",
1045
+ entrypoint: "@kitsy/cnos-next",
1046
+ summary: "Merge CNOS public env into next.config.* using the NEXT_PUBLIC_ convention.",
1047
+ usage: 'import { withCnosNext } from "@kitsy/cnos-next"',
1048
+ examples: ["cnos export env --public --framework next", "next.config.mjs -> export default withCnosNext({})"]
1049
+ }
1050
+ ];
681
1051
  var HELP_DOCUMENT = {
682
1052
  name: "cnos",
683
1053
  summary: "Workspace-aware configuration runtime and CLI for local, global, and promoted environment data.",
@@ -685,7 +1055,14 @@ var HELP_DOCUMENT = {
685
1055
  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
1056
  globalOptions: GLOBAL_OPTIONS,
687
1057
  commands: COMMANDS,
688
- examples: ["cnos doctor --workspace api", "cnos export env --public --framework vite", "cnos help-ai --format json"]
1058
+ integrations: INTEGRATIONS,
1059
+ examples: [
1060
+ "cnos use --profile stage",
1061
+ "cnos doctor --workspace api",
1062
+ "cnos export env --public --framework vite",
1063
+ "cnos export env --public --framework next",
1064
+ "cnos help-ai --format json"
1065
+ ]
689
1066
  };
690
1067
  function normalizeHelpTopic(parts) {
691
1068
  const cleaned = parts.filter((part) => part.length > 0);
@@ -739,6 +1116,11 @@ function formatRootHelp() {
739
1116
  "Commands",
740
1117
  ...HELP_DOCUMENT.commands.filter((command) => !command.id.includes(" ")).map((command) => ` ${command.id.padEnd(12, " ")} ${command.summary}`),
741
1118
  "",
1119
+ "Framework integrations",
1120
+ ...HELP_DOCUMENT.integrations.map(
1121
+ (integration) => ` ${integration.id.padEnd(12, " ")} ${integration.packageName} via ${integration.entrypoint}`
1122
+ ),
1123
+ "",
742
1124
  ...formatOptions("Global options", HELP_DOCUMENT.globalOptions),
743
1125
  "",
744
1126
  "Examples",
@@ -766,6 +1148,7 @@ function runHelpAi(topic, cliArgs = []) {
766
1148
  cli: HELP_DOCUMENT.name,
767
1149
  summary: HELP_DOCUMENT.summary,
768
1150
  globalOptions: HELP_DOCUMENT.globalOptions,
1151
+ integrations: HELP_DOCUMENT.integrations,
769
1152
  command
770
1153
  } : {
771
1154
  cli: HELP_DOCUMENT.name,
@@ -773,7 +1156,8 @@ function runHelpAi(topic, cliArgs = []) {
773
1156
  usage: HELP_DOCUMENT.usage,
774
1157
  description: HELP_DOCUMENT.description,
775
1158
  globalOptions: HELP_DOCUMENT.globalOptions,
776
- commands: HELP_DOCUMENT.commands
1159
+ commands: HELP_DOCUMENT.commands,
1160
+ integrations: HELP_DOCUMENT.integrations
777
1161
  };
778
1162
  if (topic && !command) {
779
1163
  throw new Error(`Unknown help topic: ${topic}`);
@@ -794,25 +1178,32 @@ import path4 from "path";
794
1178
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
795
1179
  import path3 from "path";
796
1180
  function scaffoldManifest(projectName, workspace) {
797
- return [
1181
+ const lines = [
798
1182
  "version: 1",
799
1183
  "project:",
800
1184
  ` name: ${projectName}`,
801
- "workspaces:",
802
- ` default: ${workspace}`,
803
- " global:",
804
- " enabled: false",
805
- " allowWrite: false",
806
- " items:",
807
- ` ${workspace}: {}`,
808
1185
  "profiles:",
809
- " default: local",
1186
+ " default: base",
810
1187
  "envMapping:",
811
1188
  " convention: SCREAMING_SNAKE",
812
1189
  "public:",
813
1190
  " promote: []",
814
1191
  ""
815
- ].join("\n");
1192
+ ];
1193
+ if (workspace) {
1194
+ lines.splice(
1195
+ 4,
1196
+ 0,
1197
+ "workspaces:",
1198
+ ` default: ${workspace}`,
1199
+ " global:",
1200
+ " enabled: false",
1201
+ " allowWrite: false",
1202
+ " items:",
1203
+ ` ${workspace}: {}`
1204
+ );
1205
+ }
1206
+ return lines.join("\n");
816
1207
  }
817
1208
  async function ensureFile(filePath, content) {
818
1209
  try {
@@ -826,11 +1217,14 @@ async function ensureFile(filePath, content) {
826
1217
  async function ensureGitignore(root) {
827
1218
  const gitignorePath = path3.join(root, ".gitignore");
828
1219
  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"
1220
+ ".cnos/env/.env",
1221
+ ".cnos/env/.env.*",
1222
+ "!.cnos/env/.env.example",
1223
+ "!.cnos/env/.env.*.example",
1224
+ ".cnos/workspaces/*/env/.env",
1225
+ ".cnos/workspaces/*/env/.env.*",
1226
+ "!.cnos/workspaces/*/env/.env.example",
1227
+ "!.cnos/workspaces/*/env/.env.*.example"
834
1228
  ];
835
1229
  let current = "";
836
1230
  try {
@@ -849,28 +1243,34 @@ async function ensureGitignore(root) {
849
1243
  return true;
850
1244
  }
851
1245
  async function scaffoldWorkspace(root, workspace) {
852
- const cnosRoot = path3.join(root, "cnos");
853
- const workspaceRoot = path3.join(cnosRoot, "workspaces", workspace);
1246
+ const cnosRoot = path3.join(root, ".cnos");
1247
+ const workspaceRoot = workspace ? path3.join(cnosRoot, "workspaces", workspace) : cnosRoot;
854
1248
  const createdPaths = [];
855
1249
  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 });
1250
+ await mkdir2(path3.join(workspaceRoot, "values"), { recursive: true });
1251
+ await mkdir2(path3.join(workspaceRoot, "secrets"), { recursive: true });
858
1252
  await mkdir2(path3.join(workspaceRoot, "env"), { recursive: true });
859
- for (const relativePath of [
1253
+ const relativePaths = workspace ? [
860
1254
  ["workspaces", workspace, "profiles", ".gitkeep"],
861
- ["workspaces", workspace, "values", "local", ".gitkeep"],
862
- ["workspaces", workspace, "secrets", "local", ".gitkeep"],
1255
+ ["workspaces", workspace, "values", ".gitkeep"],
1256
+ ["workspaces", workspace, "secrets", ".gitkeep"],
863
1257
  ["workspaces", workspace, "env", ".gitkeep"]
864
- ]) {
1258
+ ] : [
1259
+ ["profiles", ".gitkeep"],
1260
+ ["values", ".gitkeep"],
1261
+ ["secrets", ".gitkeep"],
1262
+ ["env", ".gitkeep"]
1263
+ ];
1264
+ for (const relativePath of relativePaths) {
865
1265
  const filePath = path3.join(cnosRoot, ...relativePath);
866
1266
  if (await ensureFile(filePath, "")) {
867
1267
  createdPaths.push(path3.relative(root, filePath).replace(/\\/g, "/"));
868
1268
  }
869
1269
  }
870
1270
  if (await ensureFile(path3.join(cnosRoot, "cnos.yml"), scaffoldManifest(path3.basename(root), workspace))) {
871
- createdPaths.push("cnos/cnos.yml");
1271
+ createdPaths.push(".cnos/cnos.yml");
872
1272
  }
873
- if (await ensureFile(path3.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
1273
+ if (workspace && await ensureFile(path3.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
874
1274
  globalRoot: ~/.cnos
875
1275
  `)) {
876
1276
  createdPaths.push(".cnos-workspace.yml");
@@ -880,7 +1280,7 @@ globalRoot: ~/.cnos
880
1280
  }
881
1281
  return {
882
1282
  root,
883
- workspace,
1283
+ ...workspace ? { workspace } : {},
884
1284
  created: createdPaths
885
1285
  };
886
1286
  }
@@ -888,12 +1288,14 @@ globalRoot: ~/.cnos
888
1288
  // src/commands/init.ts
889
1289
  async function runInit(options = {}) {
890
1290
  const root = path4.resolve(options.root ?? process.cwd());
891
- const workspace = options.workspace ?? path4.basename(root);
892
- const result = await scaffoldWorkspace(root, workspace);
1291
+ const result = await scaffoldWorkspace(root, options.workspace);
893
1292
  if (options.json) {
894
1293
  return printJson(result);
895
1294
  }
896
- return `initialized CNOS workspace ${workspace} at ${root}`;
1295
+ if (result.workspace) {
1296
+ return `initialized CNOS workspace ${result.workspace} at ${root}`;
1297
+ }
1298
+ return `initialized CNOS project at ${root}`;
897
1299
  }
898
1300
 
899
1301
  // src/format/printInspect.ts
@@ -928,8 +1330,76 @@ async function runInspect(key, options = {}) {
928
1330
  return printInspect(inspectResult);
929
1331
  }
930
1332
 
1333
+ // src/format/printValue.ts
1334
+ function printValue(value, json = false) {
1335
+ if (json) {
1336
+ return printJson(value);
1337
+ }
1338
+ if (typeof value === "string") {
1339
+ return value;
1340
+ }
1341
+ if (value === void 0 || value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1342
+ return String(value);
1343
+ }
1344
+ return printJson(value);
1345
+ }
1346
+
1347
+ // src/services/listing.ts
1348
+ import { flattenObject as flattenObject2 } from "@kitsy/cnos/internal";
1349
+ async function listConfigEntries(namespace, options = {}) {
1350
+ const runtime = await createRuntimeService(options);
1351
+ const namespaces = namespace === "all" ? ["value", "secret", "meta"] : [namespace];
1352
+ const entries = [];
1353
+ const prefix = options.prefix?.trim();
1354
+ for (const currentNamespace of namespaces) {
1355
+ const projected = flattenObject2(runtime.toNamespace(currentNamespace));
1356
+ for (const [path10, value] of Object.entries(projected)) {
1357
+ const key = `${currentNamespace}.${path10}`;
1358
+ if (prefix && !key.startsWith(prefix) && !path10.startsWith(prefix)) {
1359
+ continue;
1360
+ }
1361
+ entries.push({
1362
+ key,
1363
+ value
1364
+ });
1365
+ }
1366
+ }
1367
+ return entries.sort((left, right) => left.key.localeCompare(right.key));
1368
+ }
1369
+
1370
+ // src/commands/list.ts
1371
+ function normalizeNamespace(value) {
1372
+ if (!value || value === "all") {
1373
+ return "all";
1374
+ }
1375
+ if (value === "values" || value === "value") {
1376
+ return "value";
1377
+ }
1378
+ if (value === "secrets" || value === "secret") {
1379
+ return "secret";
1380
+ }
1381
+ if (value === "meta") {
1382
+ return "meta";
1383
+ }
1384
+ throw new Error(`Unsupported list namespace: ${value}`);
1385
+ }
1386
+ async function runList(args = [], options = {}) {
1387
+ const cliArgs = [...options.cliArgs ?? []];
1388
+ const namespace = normalizeNamespace(args[0] ?? consumeOption(cliArgs, "--namespace"));
1389
+ const prefix = consumeOption(cliArgs, "--prefix");
1390
+ const entries = await listConfigEntries(namespace, {
1391
+ ...options,
1392
+ cliArgs,
1393
+ ...prefix ? { prefix } : {}
1394
+ });
1395
+ if (options.json) {
1396
+ return printJson(entries);
1397
+ }
1398
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
1399
+ }
1400
+
931
1401
  // src/commands/onboard.ts
932
- import { copyFile, readdir, rm } from "fs/promises";
1402
+ import { copyFile, readdir, rm as rm2 } from "fs/promises";
933
1403
  import path5 from "path";
934
1404
  var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
935
1405
  async function listRootEnvFiles(root) {
@@ -945,7 +1415,7 @@ async function runOnboard(options = {}) {
945
1415
  throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
946
1416
  }
947
1417
  const scaffold = await scaffoldWorkspace(root, workspace);
948
- const envRoot = path5.join(root, "cnos", "workspaces", workspace, "env");
1418
+ const envRoot = path5.join(root, ".cnos", "workspaces", workspace, "env");
949
1419
  const rootFiles = await listRootEnvFiles(root);
950
1420
  const imported = [];
951
1421
  const skipped = [];
@@ -956,7 +1426,7 @@ async function runOnboard(options = {}) {
956
1426
  await copyFile(sourcePath, targetPath);
957
1427
  imported.push(path5.relative(root, targetPath).replace(/\\/g, "/"));
958
1428
  if (move) {
959
- await rm(sourcePath);
1429
+ await rm2(sourcePath);
960
1430
  }
961
1431
  } catch {
962
1432
  skipped.push(fileName);
@@ -975,21 +1445,169 @@ async function runOnboard(options = {}) {
975
1445
  }
976
1446
  const importedCount = imported.length;
977
1447
  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}`;
1448
+ return `onboarded ${workspace} at ${root}; imported ${importedCount} root env files into .cnos/workspaces/${workspace}/env using ${result.mode}${skippedSuffix}`;
979
1449
  }
980
1450
 
981
- // src/format/printValue.ts
982
- function printValue(value, json = false) {
983
- if (json) {
984
- return printJson(value);
1451
+ // src/commands/profile.ts
1452
+ import path8 from "path";
1453
+
1454
+ // src/services/context.ts
1455
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1456
+ import path6 from "path";
1457
+ import { parseYaml as parseYaml2, stringifyYaml as stringifyYaml2 } from "@kitsy/cnos/internal";
1458
+ async function loadCliContext(root = process.cwd()) {
1459
+ const filePath = path6.join(path6.resolve(root), ".cnos-workspace.yml");
1460
+ try {
1461
+ const source = await readFile4(filePath, "utf8");
1462
+ const parsed = parseYaml2(source);
1463
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1464
+ return {};
1465
+ }
1466
+ return parsed;
1467
+ } catch {
1468
+ return {};
985
1469
  }
986
- if (typeof value === "string") {
987
- return value;
1470
+ }
1471
+ async function saveCliContext(options = {}) {
1472
+ const root = path6.resolve(options.root ?? process.cwd());
1473
+ const filePath = path6.join(root, ".cnos-workspace.yml");
1474
+ const current = await loadCliContext(root);
1475
+ const next = {
1476
+ ...current.workspace ? { workspace: current.workspace } : {},
1477
+ ...current.profile ? { profile: current.profile } : {},
1478
+ ...current.globalRoot ? { globalRoot: current.globalRoot } : {},
1479
+ ...options.workspace ? { workspace: options.workspace } : {},
1480
+ ...options.profile ? { profile: options.profile } : {},
1481
+ ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
1482
+ };
1483
+ await writeFile3(filePath, stringifyYaml2(next), "utf8");
1484
+ return {
1485
+ filePath,
1486
+ context: next
1487
+ };
1488
+ }
1489
+
1490
+ // src/services/profiles.ts
1491
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "fs/promises";
1492
+ import path7 from "path";
1493
+ import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
1494
+ async function createProfileDefinition(root = process.cwd(), profile, inherit) {
1495
+ const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1496
+ await mkdir3(path7.dirname(filePath), { recursive: true });
1497
+ const document = inherit && inherit !== "base" ? {
1498
+ name: profile,
1499
+ extends: [inherit]
1500
+ } : {
1501
+ name: profile
1502
+ };
1503
+ await writeFile4(filePath, stringifyYaml3(document), "utf8");
1504
+ return {
1505
+ filePath,
1506
+ profile,
1507
+ ...inherit ? { inherit } : {}
1508
+ };
1509
+ }
1510
+ async function listProfiles(root = process.cwd()) {
1511
+ const profilesRoot = path7.join(path7.resolve(root), ".cnos", "profiles");
1512
+ try {
1513
+ const entries = await readdir2(profilesRoot, { withFileTypes: true });
1514
+ const discovered = /* @__PURE__ */ new Set(["base"]);
1515
+ for (const entry of entries) {
1516
+ if (entry.isFile() && entry.name.endsWith(".yml")) {
1517
+ discovered.add(entry.name.replace(/\.yml$/, ""));
1518
+ }
1519
+ }
1520
+ return [...discovered].sort((left, right) => left.localeCompare(right));
1521
+ } catch {
1522
+ return ["base"];
988
1523
  }
989
- if (value === void 0 || value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
990
- return String(value);
1524
+ }
1525
+ async function deleteProfileDefinition(root = process.cwd(), profile) {
1526
+ const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1527
+ try {
1528
+ await rm3(filePath);
1529
+ return {
1530
+ filePath,
1531
+ deleted: true
1532
+ };
1533
+ } catch {
1534
+ return {
1535
+ filePath,
1536
+ deleted: false
1537
+ };
1538
+ }
1539
+ }
1540
+ async function readProfileDefinition(root = process.cwd(), profile = "base") {
1541
+ if (profile === "base") {
1542
+ return {
1543
+ name: "base"
1544
+ };
1545
+ }
1546
+ const filePath = path7.join(path7.resolve(root), ".cnos", "profiles", `${profile}.yml`);
1547
+ try {
1548
+ return parseYaml3(await readFile5(filePath, "utf8")) ?? void 0;
1549
+ } catch {
1550
+ return void 0;
991
1551
  }
992
- return printJson(value);
1552
+ }
1553
+
1554
+ // src/commands/profile.ts
1555
+ function normalizeProfileAction(args) {
1556
+ const [action = "list", ...tail] = args;
1557
+ if (["create", "list", "use", "delete", "remove"].includes(action)) {
1558
+ return {
1559
+ action: action === "remove" ? "delete" : action,
1560
+ tail
1561
+ };
1562
+ }
1563
+ return {
1564
+ action: "list",
1565
+ tail: args
1566
+ };
1567
+ }
1568
+ async function runProfile(args, options = {}) {
1569
+ const { action, tail } = normalizeProfileAction(args);
1570
+ const root = path8.resolve(options.root ?? process.cwd());
1571
+ const cliArgs = [...options.cliArgs ?? []];
1572
+ if (action === "create") {
1573
+ const profile = tail[0] ?? "stage";
1574
+ const inherit = consumeOption(cliArgs, "--inherit");
1575
+ const result = await createProfileDefinition(root, profile, inherit);
1576
+ if (options.json) {
1577
+ return printJson(result);
1578
+ }
1579
+ return `created profile ${profile} at ${result.filePath}`;
1580
+ }
1581
+ if (action === "use") {
1582
+ const profile = tail[0] ?? "base";
1583
+ const result = await saveCliContext({
1584
+ root,
1585
+ profile
1586
+ });
1587
+ if (options.json) {
1588
+ return printJson(result);
1589
+ }
1590
+ return `active profile set to ${profile} in ${result.filePath}`;
1591
+ }
1592
+ if (action === "delete") {
1593
+ const profile = tail[0] ?? "base";
1594
+ const result = await deleteProfileDefinition(root, profile);
1595
+ if (options.json) {
1596
+ return printJson(result);
1597
+ }
1598
+ return result.deleted ? `deleted profile ${profile}` : `profile ${profile} was not found`;
1599
+ }
1600
+ const profiles = await listProfiles(root);
1601
+ if (options.json) {
1602
+ const details = await Promise.all(
1603
+ profiles.map(async (profile) => ({
1604
+ profile,
1605
+ definition: await readProfileDefinition(root, profile)
1606
+ }))
1607
+ );
1608
+ return printJson(details);
1609
+ }
1610
+ return profiles.join("\n");
993
1611
  }
994
1612
 
995
1613
  // src/commands/read.ts
@@ -1050,21 +1668,113 @@ async function runCommand(command, options = {}) {
1050
1668
  }
1051
1669
 
1052
1670
  // src/commands/secret.ts
1053
- async function runSecret(path6, options = {}) {
1671
+ function isSecretRef(value) {
1672
+ return Boolean(
1673
+ value && typeof value === "object" && !Array.isArray(value) && typeof value.provider === "string" && typeof value.ref === "string"
1674
+ );
1675
+ }
1676
+ function normalizeSecretCommand(args) {
1677
+ const [actionOrPath, ...tail] = args;
1678
+ if (!actionOrPath) {
1679
+ return {
1680
+ action: "list",
1681
+ tail: []
1682
+ };
1683
+ }
1684
+ if (["get", "set", "create", "add", "list", "delete", "remove"].includes(actionOrPath)) {
1685
+ return {
1686
+ action: actionOrPath === "remove" ? "delete" : actionOrPath === "create" || actionOrPath === "add" ? "set" : actionOrPath,
1687
+ tail
1688
+ };
1689
+ }
1690
+ return {
1691
+ action: "get",
1692
+ tail: args
1693
+ };
1694
+ }
1695
+ async function runSecret(argsOrPath, options = {}) {
1696
+ const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
1697
+ const { action, tail } = normalizeSecretCommand(args);
1698
+ const cliArgs = [...options.cliArgs ?? []];
1699
+ if (action === "list") {
1700
+ const runtime2 = await createRuntimeService(options);
1701
+ const secrets = runtime2.toNamespace("secret");
1702
+ if (options.json) {
1703
+ return printJson(secrets);
1704
+ }
1705
+ return Object.entries(secrets).sort(([left], [right]) => left.localeCompare(right)).map(([key, value2]) => `${key}=${printValue(value2)}`).join("\n");
1706
+ }
1707
+ if (action === "set") {
1708
+ const secretPath = tail[0];
1709
+ const rawValue = tail[1] ?? "";
1710
+ const local = consumeFlag(cliArgs, "--local");
1711
+ const remote = consumeFlag(cliArgs, "--remote");
1712
+ const ref = consumeFlag(cliArgs, "--ref");
1713
+ const target = consumeOption(cliArgs, "--target") ?? "local";
1714
+ const provider = consumeOption(cliArgs, "--provider");
1715
+ const passphrase = consumeOption(cliArgs, "--passphrase");
1716
+ const mode = local ? "local" : remote ? "remote" : ref ? "ref" : "local";
1717
+ const result = await setSecret(secretPath ?? "app.token", rawValue, {
1718
+ ...options,
1719
+ cliArgs,
1720
+ target,
1721
+ mode,
1722
+ ...provider ? { provider } : {},
1723
+ ...passphrase ? { passphrase } : {}
1724
+ });
1725
+ if (options.json) {
1726
+ return printJson(result);
1727
+ }
1728
+ return `set secret.${secretPath} via ${result.provider} in ${result.filePath}`;
1729
+ }
1730
+ if (action === "delete") {
1731
+ const secretPath = tail[0];
1732
+ const target = consumeOption(cliArgs, "--target") ?? "local";
1733
+ const result = await deleteSecret(secretPath ?? "app.token", {
1734
+ ...options,
1735
+ cliArgs,
1736
+ target
1737
+ });
1738
+ if (options.json) {
1739
+ return printJson(result);
1740
+ }
1741
+ return result.deleted ? `deleted secret.${secretPath} from ${result.filePath}` : `no secret.${secretPath} found in ${result.filePath}`;
1742
+ }
1054
1743
  const runtime = await createRuntimeService(options);
1055
- const value = runtime.secret(path6);
1744
+ const value = runtime.secret(tail[0] ?? "app.token");
1056
1745
  if (value === void 0) {
1057
- throw new Error(`Missing CNOS secret path: ${path6}`);
1746
+ throw new Error(`Missing CNOS secret path: ${tail[0] ?? "app.token"}`);
1747
+ }
1748
+ if (isSecretRef(value)) {
1749
+ throw new Error(
1750
+ `Secret ${tail[0] ?? "app.token"} is stored as an unresolved ${value.provider} reference. Provide the required provider context to resolve it.`
1751
+ );
1058
1752
  }
1059
1753
  if (options.json) {
1060
1754
  return printJson({
1061
- key: `secret.${path6}`,
1755
+ key: `secret.${tail[0] ?? "app.token"}`,
1062
1756
  value
1063
1757
  });
1064
1758
  }
1065
1759
  return printValue(value);
1066
1760
  }
1067
1761
 
1762
+ // src/commands/use.ts
1763
+ import path9 from "path";
1764
+ async function runUse(options = {}) {
1765
+ const root = path9.resolve(options.root ?? process.cwd());
1766
+ const result = await saveCliContext({
1767
+ root,
1768
+ ...options.workspace ? { workspace: options.workspace } : {},
1769
+ ...options.profile ? { profile: options.profile } : {},
1770
+ ...options.globalRoot ? { globalRoot: options.globalRoot } : {}
1771
+ });
1772
+ if (options.json) {
1773
+ return printJson(result);
1774
+ }
1775
+ return `updated CLI context in ${result.filePath}`;
1776
+ }
1777
+
1068
1778
  // src/commands/validate.ts
1069
1779
  async function runValidate(options = {}) {
1070
1780
  const { summary } = await createValidationSummary(options);
@@ -1078,15 +1788,82 @@ async function runValidate(options = {}) {
1078
1788
  }
1079
1789
 
1080
1790
  // src/commands/value.ts
1081
- async function runValue(path6, options = {}) {
1791
+ function normalizeValueCommand(args) {
1792
+ const [actionOrPath, ...tail] = args;
1793
+ if (!actionOrPath) {
1794
+ return {
1795
+ action: "list",
1796
+ tail: []
1797
+ };
1798
+ }
1799
+ if (["get", "set", "create", "add", "list", "delete", "remove"].includes(actionOrPath)) {
1800
+ return {
1801
+ action: actionOrPath === "remove" ? "delete" : actionOrPath === "create" || actionOrPath === "add" ? "set" : actionOrPath,
1802
+ tail
1803
+ };
1804
+ }
1805
+ return {
1806
+ action: "get",
1807
+ tail: args
1808
+ };
1809
+ }
1810
+ async function runValue(argsOrPath, options = {}) {
1811
+ const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
1812
+ const { action, tail } = normalizeValueCommand(args);
1813
+ const cliArgs = [...options.cliArgs ?? []];
1814
+ if (action === "list") {
1815
+ const prefix = consumeOption(cliArgs, "--prefix");
1816
+ const entries = await listConfigEntries("value", {
1817
+ ...options,
1818
+ cliArgs,
1819
+ ...prefix ? { prefix } : {}
1820
+ });
1821
+ if (options.json) {
1822
+ return printJson(entries);
1823
+ }
1824
+ return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
1825
+ }
1826
+ if (action === "set") {
1827
+ const valuePath = tail[0] ?? "app.name";
1828
+ const rawValue = tail[1] ?? "";
1829
+ const target = consumeOption(cliArgs, "--target") ?? "local";
1830
+ const result = await defineValue("value", valuePath, rawValue, {
1831
+ ...options,
1832
+ cliArgs,
1833
+ target
1834
+ });
1835
+ if (options.json) {
1836
+ return printJson({
1837
+ namespace: "value",
1838
+ path: valuePath,
1839
+ target,
1840
+ filePath: result.filePath,
1841
+ value: result.value
1842
+ });
1843
+ }
1844
+ return `set value.${valuePath} in ${result.filePath}`;
1845
+ }
1846
+ if (action === "delete") {
1847
+ const valuePath = tail[0] ?? "app.name";
1848
+ const target = consumeOption(cliArgs, "--target") ?? "local";
1849
+ const result = await deleteValue("value", valuePath, {
1850
+ ...options,
1851
+ cliArgs,
1852
+ target
1853
+ });
1854
+ if (options.json) {
1855
+ return printJson(result);
1856
+ }
1857
+ return result.deleted ? `deleted value.${valuePath} from ${result.filePath}` : `no value.${valuePath} found in ${result.filePath}`;
1858
+ }
1082
1859
  const runtime = await createRuntimeService(options);
1083
- const value = runtime.value(path6);
1860
+ const value = runtime.value(tail[0] ?? "app.name");
1084
1861
  if (value === void 0) {
1085
- throw new Error(`Missing CNOS value path: ${path6}`);
1862
+ throw new Error(`Missing CNOS value path: ${tail[0] ?? "app.name"}`);
1086
1863
  }
1087
1864
  if (options.json) {
1088
1865
  return printJson({
1089
- key: `value.${path6}`,
1866
+ key: `value.${tail[0] ?? "app.name"}`,
1090
1867
  value
1091
1868
  });
1092
1869
  }
@@ -1101,6 +1878,21 @@ function resolveHelpTopic(command, args) {
1101
1878
  if (command === "export" && args[0] === "env") {
1102
1879
  return normalizeHelpTopic([command, args[0]]);
1103
1880
  }
1881
+ if (command === "secret" && args[0] && ["set", "create", "add", "list", "delete", "remove"].includes(args[0])) {
1882
+ return normalizeHelpTopic([
1883
+ command,
1884
+ args[0] === "remove" ? "delete" : args[0] === "create" || args[0] === "add" ? "set" : args[0]
1885
+ ]);
1886
+ }
1887
+ if (command === "value" && args[0] && ["set", "create", "add", "list", "delete", "remove"].includes(args[0])) {
1888
+ return normalizeHelpTopic([
1889
+ command,
1890
+ args[0] === "remove" ? "delete" : args[0] === "create" || args[0] === "add" ? "set" : args[0]
1891
+ ]);
1892
+ }
1893
+ if (command === "profile" && args[0] && ["create", "list", "use", "delete", "remove"].includes(args[0])) {
1894
+ return normalizeHelpTopic([command, args[0] === "remove" ? "delete" : args[0]]);
1895
+ }
1104
1896
  return normalizeHelpTopic([command]);
1105
1897
  }
1106
1898
  async function main(argv) {
@@ -1152,11 +1944,23 @@ async function main(argv) {
1152
1944
  `);
1153
1945
  return;
1154
1946
  case "value":
1155
- process.stdout.write(`${await runValue(args[0] ?? "app.name", runtimeOptions)}
1947
+ process.stdout.write(`${await runValue(args.length > 0 ? args : ["app.name"], runtimeOptions)}
1156
1948
  `);
1157
1949
  return;
1158
1950
  case "secret":
1159
- process.stdout.write(`${await runSecret(args[0] ?? "app.token", runtimeOptions)}
1951
+ process.stdout.write(`${await runSecret(args.length > 0 ? args : ["app.token"], runtimeOptions)}
1952
+ `);
1953
+ return;
1954
+ case "use":
1955
+ process.stdout.write(`${await runUse(runtimeOptions)}
1956
+ `);
1957
+ return;
1958
+ case "profile":
1959
+ process.stdout.write(`${await runProfile(args, runtimeOptions)}
1960
+ `);
1961
+ return;
1962
+ case "list":
1963
+ process.stdout.write(`${await runList(args, runtimeOptions)}
1160
1964
  `);
1161
1965
  return;
1162
1966
  case "define":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitsy/cnos-cli",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "description": "CLI entry point and developer tooling for CNOS.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,8 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@kitsy/cnos": "0.0.1",
40
- "@kitsy/cnos-core": "0.0.1"
39
+ "@kitsy/cnos": "1.0.0"
41
40
  },
42
41
  "scripts": {
43
42
  "build": "tsup src/index.ts --format esm --dts",