@kitsy/cnos-cli 1.2.0 → 1.4.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.
- package/dist/index.js +1161 -366
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
|
11
11
|
"--format",
|
|
12
12
|
"--framework",
|
|
13
13
|
"--prefix",
|
|
14
|
+
"--scan",
|
|
14
15
|
"--target",
|
|
15
16
|
"--to",
|
|
16
17
|
"--provider",
|
|
@@ -18,13 +19,42 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
|
18
19
|
"--vault",
|
|
19
20
|
"--inherit",
|
|
20
21
|
"--as",
|
|
21
|
-
"--set"
|
|
22
|
+
"--set",
|
|
23
|
+
"--debounce"
|
|
24
|
+
]);
|
|
25
|
+
var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
|
|
26
|
+
"--flatten",
|
|
27
|
+
"--public",
|
|
28
|
+
"--local",
|
|
29
|
+
"--remote",
|
|
30
|
+
"--ref",
|
|
31
|
+
"--no-passphrase",
|
|
32
|
+
"--stdin",
|
|
33
|
+
"--reveal",
|
|
34
|
+
"--auth",
|
|
35
|
+
"--all",
|
|
36
|
+
"--store-keychain",
|
|
37
|
+
"--watch",
|
|
38
|
+
"--dry-run",
|
|
39
|
+
"--apply",
|
|
40
|
+
"--rewrite",
|
|
41
|
+
"--signal"
|
|
22
42
|
]);
|
|
23
|
-
var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set(["--flatten", "--public", "--local", "--remote", "--ref", "--no-passphrase"]);
|
|
24
43
|
function normalizeCommand(argv) {
|
|
25
44
|
const [command = "doctor", ...rest] = argv;
|
|
26
45
|
const resource = rest[0];
|
|
27
46
|
const remaining = rest.slice(1);
|
|
47
|
+
const dottedNamespace = resource?.includes(".") ? {
|
|
48
|
+
namespace: resource.slice(0, resource.indexOf(".")),
|
|
49
|
+
path: resource.slice(resource.indexOf(".") + 1)
|
|
50
|
+
} : void 0;
|
|
51
|
+
const normalizedVerb = command === "remove" || command === "delete" ? "delete" : command === "create" || command === "add" ? "set" : command === "set" || command === "get" ? command : void 0;
|
|
52
|
+
if ((command === "set" || command === "get") && (resource === "value" || resource === "secret")) {
|
|
53
|
+
return [resource, command, ...remaining];
|
|
54
|
+
}
|
|
55
|
+
if (normalizedVerb && dottedNamespace && dottedNamespace.namespace && dottedNamespace.path) {
|
|
56
|
+
return [dottedNamespace.namespace, normalizedVerb, dottedNamespace.path, ...remaining];
|
|
57
|
+
}
|
|
28
58
|
if ((command === "create" || command === "add") && resource === "profile") {
|
|
29
59
|
return ["profile", "create", ...remaining];
|
|
30
60
|
}
|
|
@@ -37,6 +67,12 @@ function normalizeCommand(argv) {
|
|
|
37
67
|
if ((command === "delete" || command === "remove") && resource === "vault") {
|
|
38
68
|
return ["vault", "remove", ...remaining];
|
|
39
69
|
}
|
|
70
|
+
if (command === "auth" && resource === "vault") {
|
|
71
|
+
return ["vault", "auth", ...remaining];
|
|
72
|
+
}
|
|
73
|
+
if (command === "logout" && resource === "vault") {
|
|
74
|
+
return ["vault", "logout", ...remaining];
|
|
75
|
+
}
|
|
40
76
|
if (command === "list" && resource === "profile") {
|
|
41
77
|
return ["profile", "list", ...remaining];
|
|
42
78
|
}
|
|
@@ -58,6 +94,9 @@ function normalizeCommand(argv) {
|
|
|
58
94
|
if ((command === "delete" || command === "remove") && resource === "value") {
|
|
59
95
|
return ["value", "delete", ...remaining];
|
|
60
96
|
}
|
|
97
|
+
if (normalizedVerb && resource && !["profile", "vault", "secret", "value"].includes(resource)) {
|
|
98
|
+
return [resource, normalizedVerb, ...remaining];
|
|
99
|
+
}
|
|
61
100
|
if (command === "list" && resource === "value") {
|
|
62
101
|
return ["value", "list", ...remaining];
|
|
63
102
|
}
|
|
@@ -164,6 +203,9 @@ function parseArgs(argv) {
|
|
|
164
203
|
};
|
|
165
204
|
}
|
|
166
205
|
|
|
206
|
+
// src/commands/define.ts
|
|
207
|
+
import path3 from "path";
|
|
208
|
+
|
|
167
209
|
// src/cli/commandOptions.ts
|
|
168
210
|
function consumeFlag(args, flag) {
|
|
169
211
|
const index = args.indexOf(flag);
|
|
@@ -191,172 +233,51 @@ function consumeOption(args, flag) {
|
|
|
191
233
|
return void 0;
|
|
192
234
|
}
|
|
193
235
|
|
|
236
|
+
// src/format/displayPath.ts
|
|
237
|
+
import path from "path";
|
|
238
|
+
function displayPath(filePath, root = process.cwd()) {
|
|
239
|
+
const absoluteRoot = path.resolve(root);
|
|
240
|
+
const absoluteFile = path.resolve(filePath);
|
|
241
|
+
const relative = path.relative(absoluteRoot, absoluteFile);
|
|
242
|
+
if (!relative || relative === "") {
|
|
243
|
+
return ".";
|
|
244
|
+
}
|
|
245
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
246
|
+
return absoluteFile;
|
|
247
|
+
}
|
|
248
|
+
return relative;
|
|
249
|
+
}
|
|
250
|
+
|
|
194
251
|
// src/format/printJson.ts
|
|
195
252
|
function printJson(value) {
|
|
196
253
|
return JSON.stringify(value, null, 2);
|
|
197
254
|
}
|
|
198
255
|
|
|
199
256
|
// src/services/writes.ts
|
|
200
|
-
import { mkdir, readFile
|
|
257
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
201
258
|
import path2 from "path";
|
|
202
259
|
import {
|
|
203
|
-
|
|
260
|
+
getNamespaceDefinition,
|
|
261
|
+
createSecretVaultProvider,
|
|
204
262
|
parseYaml,
|
|
205
263
|
resolveConfigDocumentPath,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
resolveConfiguredVaultPassphrase as resolveConfiguredVaultPassphrase2,
|
|
209
|
-
resolveSecretStoreRoot as resolveSecretStoreRoot2
|
|
264
|
+
resolveVaultAuth,
|
|
265
|
+
stringifyYaml
|
|
210
266
|
} from "@kitsy/cnos/internal";
|
|
211
267
|
|
|
212
268
|
// src/services/runtime.ts
|
|
213
|
-
import { createCnos } from "@kitsy/cnos";
|
|
269
|
+
import { createCnos } from "@kitsy/cnos/configure";
|
|
214
270
|
async function createRuntimeService(options = {}) {
|
|
215
|
-
const createOptions = {
|
|
216
|
-
...options.root ? {
|
|
217
|
-
root: options.root
|
|
218
|
-
} : {},
|
|
219
|
-
...options.workspace ? {
|
|
220
|
-
workspace: options.workspace
|
|
221
|
-
} : {},
|
|
222
|
-
...options.profile ? {
|
|
223
|
-
profile: options.profile
|
|
224
|
-
} : {},
|
|
225
|
-
...options.globalRoot ? {
|
|
226
|
-
globalRoot: options.globalRoot
|
|
227
|
-
} : {},
|
|
228
|
-
...options.cliArgs && options.cliArgs.length > 0 ? {
|
|
229
|
-
cliArgs: options.cliArgs
|
|
230
|
-
} : {},
|
|
231
|
-
...options.processEnv ? {
|
|
232
|
-
processEnv: options.processEnv
|
|
233
|
-
} : {
|
|
234
|
-
processEnv: process.env
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
271
|
return createCnos({
|
|
238
|
-
...
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
import {
|
|
246
|
-
createSecretVault,
|
|
247
|
-
loadManifest,
|
|
248
|
-
listSecretVaults,
|
|
249
|
-
resolveConfiguredVaultPassphrase,
|
|
250
|
-
resolveSecretStoreRoot,
|
|
251
|
-
resolveSecretVaultFile,
|
|
252
|
-
resolveVaultDefinition,
|
|
253
|
-
stringifyYaml
|
|
254
|
-
} from "@kitsy/cnos/internal";
|
|
255
|
-
function sortVaults(vaults) {
|
|
256
|
-
return Object.fromEntries(Object.entries(vaults).sort(([left], [right]) => left.localeCompare(right)));
|
|
257
|
-
}
|
|
258
|
-
async function createVaultDefinition(name, options = {}) {
|
|
259
|
-
const vault = name.trim() || "default";
|
|
260
|
-
const provider = options.provider?.trim() || "local";
|
|
261
|
-
const noPassphrase = options.noPassphrase ?? false;
|
|
262
|
-
if (provider === "local" && noPassphrase) {
|
|
263
|
-
throw new Error("Local vaults require a passphrase");
|
|
264
|
-
}
|
|
265
|
-
const loadedManifest = await loadManifest(options.root ? { root: options.root } : {});
|
|
266
|
-
const processEnv = options.processEnv ?? process.env;
|
|
267
|
-
const passphraseEnvVar = "CNOS_SECRET_PASSPHRASE";
|
|
268
|
-
const rawManifest = {
|
|
269
|
-
...loadedManifest.rawManifest,
|
|
270
|
-
vaults: {
|
|
271
|
-
...loadedManifest.rawManifest.vaults ?? {},
|
|
272
|
-
[vault]: provider === "local" ? {
|
|
273
|
-
provider: "local",
|
|
274
|
-
passphrase: `env:${passphraseEnvVar}`
|
|
275
|
-
} : {
|
|
276
|
-
provider
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
let storePath;
|
|
281
|
-
if (provider === "local") {
|
|
282
|
-
const passphrase = options.passphrase ?? resolveConfiguredVaultPassphrase(
|
|
283
|
-
{
|
|
284
|
-
provider: "local",
|
|
285
|
-
passphrase: `env:${passphraseEnvVar}`
|
|
286
|
-
},
|
|
287
|
-
vault,
|
|
288
|
-
processEnv
|
|
289
|
-
);
|
|
290
|
-
if (!passphrase) {
|
|
291
|
-
throw new Error(`Vault "${vault}" requires --passphrase or ${passphraseEnvVar}`);
|
|
292
|
-
}
|
|
293
|
-
storePath = await createSecretVault(resolveSecretStoreRoot(processEnv), vault, passphrase);
|
|
294
|
-
}
|
|
295
|
-
await writeFile(loadedManifest.manifestPath, stringifyYaml(rawManifest), "utf8");
|
|
296
|
-
return {
|
|
297
|
-
...resolveVaultDefinition(
|
|
298
|
-
{
|
|
299
|
-
[vault]: rawManifest.vaults[vault]
|
|
300
|
-
},
|
|
301
|
-
vault
|
|
302
|
-
),
|
|
303
|
-
passphrasePolicy: provider === "local" ? "required" : "none",
|
|
304
|
-
manifestPath: loadedManifest.manifestPath,
|
|
305
|
-
...storePath ? { storePath } : {}
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
async function listVaultDefinitions(options = {}) {
|
|
309
|
-
const loadedManifest = await loadManifest(options.root ? { root: options.root } : {});
|
|
310
|
-
return Object.keys(loadedManifest.manifest.vaults).sort((left, right) => left.localeCompare(right)).map((name) => {
|
|
311
|
-
const definition = resolveVaultDefinition(loadedManifest.manifest.vaults, name);
|
|
312
|
-
return {
|
|
313
|
-
...definition,
|
|
314
|
-
passphrasePolicy: definition.requiresPassphrase ? "required" : "none"
|
|
315
|
-
};
|
|
272
|
+
...options.root ? { root: options.root } : {},
|
|
273
|
+
...options.workspace ? { workspace: options.workspace } : {},
|
|
274
|
+
...options.profile ? { profile: options.profile } : {},
|
|
275
|
+
...options.globalRoot ? { globalRoot: options.globalRoot } : {},
|
|
276
|
+
...options.cliArgs && options.cliArgs.length > 0 ? { cliArgs: options.cliArgs } : {},
|
|
277
|
+
...options.secretResolution ? { secretResolution: options.secretResolution } : {},
|
|
278
|
+
processEnv: options.processEnv ?? process.env
|
|
316
279
|
});
|
|
317
280
|
}
|
|
318
|
-
async function removeVaultDefinition(name, options = {}) {
|
|
319
|
-
const vault = name.trim() || "default";
|
|
320
|
-
const loadedManifest = await loadManifest(options.root ? { root: options.root } : {});
|
|
321
|
-
if (!loadedManifest.rawManifest.vaults?.[vault]) {
|
|
322
|
-
return {
|
|
323
|
-
name: vault,
|
|
324
|
-
deleted: false,
|
|
325
|
-
manifestPath: loadedManifest.manifestPath
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
const nextVaults = { ...loadedManifest.rawManifest.vaults ?? {} };
|
|
329
|
-
delete nextVaults[vault];
|
|
330
|
-
const rawManifest = {
|
|
331
|
-
...loadedManifest.rawManifest,
|
|
332
|
-
...Object.keys(nextVaults).length > 0 ? { vaults: sortVaults(nextVaults) } : {}
|
|
333
|
-
};
|
|
334
|
-
if (Object.keys(nextVaults).length === 0) {
|
|
335
|
-
delete rawManifest.vaults;
|
|
336
|
-
}
|
|
337
|
-
await writeFile(loadedManifest.manifestPath, stringifyYaml(rawManifest), "utf8");
|
|
338
|
-
const storeRoot = resolveSecretStoreRoot(options.processEnv);
|
|
339
|
-
const vaultFile = resolveSecretVaultFile(storeRoot, vault);
|
|
340
|
-
const vaultStoreRoot = path.join(storeRoot, "vaults", vault);
|
|
341
|
-
let removedStore;
|
|
342
|
-
try {
|
|
343
|
-
await readFile(vaultFile, "utf8");
|
|
344
|
-
await rm(vaultFile, { force: true });
|
|
345
|
-
await rm(vaultStoreRoot, { recursive: true, force: true });
|
|
346
|
-
removedStore = vaultStoreRoot;
|
|
347
|
-
} catch {
|
|
348
|
-
removedStore = void 0;
|
|
349
|
-
}
|
|
350
|
-
return {
|
|
351
|
-
name: vault,
|
|
352
|
-
deleted: true,
|
|
353
|
-
manifestPath: loadedManifest.manifestPath,
|
|
354
|
-
...removedStore ? { removedStore } : {}
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
async function listLocalStoreVaults(options = {}) {
|
|
358
|
-
return listSecretVaults(resolveSecretStoreRoot(options.processEnv));
|
|
359
|
-
}
|
|
360
281
|
|
|
361
282
|
// src/services/writes.ts
|
|
362
283
|
function setNestedValue(target, pathSegments, value) {
|
|
@@ -405,7 +326,7 @@ function isSecretReference(value) {
|
|
|
405
326
|
}
|
|
406
327
|
async function readYamlDocument(filePath) {
|
|
407
328
|
try {
|
|
408
|
-
return parseYaml(await
|
|
329
|
+
return parseYaml(await readFile(filePath, "utf8")) ?? {};
|
|
409
330
|
} catch {
|
|
410
331
|
return {};
|
|
411
332
|
}
|
|
@@ -433,11 +354,25 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
|
|
|
433
354
|
filePath: secret.filePath,
|
|
434
355
|
value: {
|
|
435
356
|
provider: secret.provider,
|
|
436
|
-
ref: secret.ref
|
|
357
|
+
ref: secret.ref,
|
|
358
|
+
...secret.vault ? { vault: secret.vault } : {}
|
|
437
359
|
}
|
|
438
360
|
};
|
|
439
361
|
}
|
|
440
362
|
const runtime = await createRuntimeService(options);
|
|
363
|
+
if (namespace !== "value" && namespace !== "secret" && !runtime.manifest.namespaces[namespace]) {
|
|
364
|
+
throw new Error(`Cannot write ${namespace}.${configPath} because namespace "${namespace}" is not declared in .cnos/cnos.yml.`);
|
|
365
|
+
}
|
|
366
|
+
const namespaceDefinition = getNamespaceDefinition(runtime.manifest, namespace);
|
|
367
|
+
if (namespaceDefinition.kind !== "data") {
|
|
368
|
+
throw new Error(`Cannot write ${namespace}.${configPath} because namespace "${namespace}" is not a data namespace.`);
|
|
369
|
+
}
|
|
370
|
+
if (namespaceDefinition.readonly) {
|
|
371
|
+
throw new Error(`Cannot write ${namespace}.${configPath} because namespace "${namespace}" is readonly.`);
|
|
372
|
+
}
|
|
373
|
+
if (namespaceDefinition.sensitive) {
|
|
374
|
+
throw new Error(`Cannot write ${namespace}.${configPath} with the generic data writer because namespace "${namespace}" is sensitive.`);
|
|
375
|
+
}
|
|
441
376
|
const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
|
|
442
377
|
const profile = options.profile ?? runtime.graph.profile;
|
|
443
378
|
const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
|
|
@@ -445,7 +380,7 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
|
|
|
445
380
|
const parsedValue = parseScalarValue(rawValue);
|
|
446
381
|
setNestedValue(document, configPath.split("."), parsedValue);
|
|
447
382
|
await mkdir(path2.dirname(filePath), { recursive: true });
|
|
448
|
-
await
|
|
383
|
+
await writeFile(filePath, stringifyYaml(document), "utf8");
|
|
449
384
|
return {
|
|
450
385
|
filePath,
|
|
451
386
|
value: parsedValue
|
|
@@ -459,53 +394,36 @@ async function setSecret(configPath, rawValue, options = {}) {
|
|
|
459
394
|
const document = await readYamlDocument(filePath);
|
|
460
395
|
const vault = options.vault?.trim() || "default";
|
|
461
396
|
const vaultDefinition = runtime.manifest.vaults[vault];
|
|
462
|
-
|
|
463
|
-
|
|
397
|
+
if (!vaultDefinition) {
|
|
398
|
+
throw new Error(`Unknown vault "${vault}". Create it first with cnos vault create ${vault}.`);
|
|
399
|
+
}
|
|
400
|
+
const mode = options.mode ?? (vaultDefinition.provider === "local" ? "local" : vaultDefinition.provider === "github-secrets" ? "ref" : "remote");
|
|
464
401
|
let reference;
|
|
465
|
-
let storePath;
|
|
466
402
|
if (mode === "local") {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const passphrase = resolveConfiguredVaultPassphrase2(
|
|
472
|
-
vaultDefinition ? { provider: "local", ...vaultDefinition.passphrase ? { passphrase: vaultDefinition.passphrase } : {} } : { provider: "local" },
|
|
473
|
-
vault,
|
|
474
|
-
{
|
|
475
|
-
...options.processEnv ?? process.env,
|
|
476
|
-
...options.passphrase ? {
|
|
477
|
-
[getVaultPassphraseEnvVar(vault)]: options.passphrase
|
|
478
|
-
} : {}
|
|
479
|
-
}
|
|
480
|
-
) ?? options.passphrase;
|
|
481
|
-
if (!passphrase) {
|
|
482
|
-
throw new Error(`Vault "${vault}" requires --passphrase or its configured passphrase env var`);
|
|
483
|
-
}
|
|
484
|
-
const ref = configPath;
|
|
485
|
-
storePath = await writeLocalSecret(resolveSecretStoreRoot2(options.processEnv), ref, rawValue, passphrase, vault);
|
|
403
|
+
const auth = await resolveVaultAuth(vault, vaultDefinition, options.processEnv ?? process.env);
|
|
404
|
+
const provider = createSecretVaultProvider(vault, vaultDefinition, options.processEnv ?? process.env);
|
|
405
|
+
await provider.authenticate(auth);
|
|
406
|
+
await provider.set(configPath, rawValue);
|
|
486
407
|
reference = {
|
|
487
408
|
provider: "local",
|
|
488
|
-
ref,
|
|
409
|
+
ref: configPath,
|
|
489
410
|
vault
|
|
490
411
|
};
|
|
491
412
|
} else {
|
|
492
413
|
reference = {
|
|
493
|
-
provider:
|
|
414
|
+
provider: options.provider?.trim() || vaultDefinition.provider,
|
|
494
415
|
ref: rawValue || configPath.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase(),
|
|
495
|
-
|
|
496
|
-
vault
|
|
497
|
-
} : {}
|
|
416
|
+
vault
|
|
498
417
|
};
|
|
499
418
|
}
|
|
500
419
|
setNestedValue(document, configPath.split("."), reference);
|
|
501
420
|
await mkdir(path2.dirname(filePath), { recursive: true });
|
|
502
|
-
await
|
|
421
|
+
await writeFile(filePath, stringifyYaml(document), "utf8");
|
|
503
422
|
return {
|
|
504
423
|
filePath,
|
|
505
424
|
provider: reference.provider,
|
|
506
425
|
ref: reference.ref,
|
|
507
|
-
...reference.vault ? { vault: reference.vault } : {}
|
|
508
|
-
...storePath ? { storePath } : {}
|
|
426
|
+
...reference.vault ? { vault: reference.vault } : {}
|
|
509
427
|
};
|
|
510
428
|
}
|
|
511
429
|
async function deleteSecret(configPath, options = {}) {
|
|
@@ -522,24 +440,20 @@ async function deleteSecret(configPath, options = {}) {
|
|
|
522
440
|
deleted: false
|
|
523
441
|
};
|
|
524
442
|
}
|
|
525
|
-
await
|
|
526
|
-
let removedStore;
|
|
443
|
+
await writeFile(filePath, stringifyYaml(document), "utf8");
|
|
527
444
|
const secretRef = metadata?.secretRef;
|
|
528
445
|
if (isSecretReference(secretRef) && secretRef.provider === "local") {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
"
|
|
532
|
-
secretRef.vault ?? "default",
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
await rm2(storePath, { force: true });
|
|
537
|
-
removedStore = storePath;
|
|
446
|
+
const definition = runtime.manifest.vaults[secretRef.vault ?? "default"];
|
|
447
|
+
if (definition) {
|
|
448
|
+
const auth = await resolveVaultAuth(secretRef.vault ?? "default", definition, options.processEnv ?? process.env);
|
|
449
|
+
const provider = createSecretVaultProvider(secretRef.vault ?? "default", definition, options.processEnv ?? process.env);
|
|
450
|
+
await provider.authenticate(auth);
|
|
451
|
+
await provider.delete(secretRef.ref);
|
|
452
|
+
}
|
|
538
453
|
}
|
|
539
454
|
return {
|
|
540
455
|
filePath,
|
|
541
|
-
deleted: true
|
|
542
|
-
...removedStore ? { removedStore } : {}
|
|
456
|
+
deleted: true
|
|
543
457
|
};
|
|
544
458
|
}
|
|
545
459
|
async function deleteValue(namespace, configPath, options = {}) {
|
|
@@ -547,6 +461,19 @@ async function deleteValue(namespace, configPath, options = {}) {
|
|
|
547
461
|
return deleteSecret(configPath, options);
|
|
548
462
|
}
|
|
549
463
|
const runtime = await createRuntimeService(options);
|
|
464
|
+
if (namespace !== "value" && namespace !== "secret" && !runtime.manifest.namespaces[namespace]) {
|
|
465
|
+
throw new Error(`Cannot delete ${namespace}.${configPath} because namespace "${namespace}" is not declared in .cnos/cnos.yml.`);
|
|
466
|
+
}
|
|
467
|
+
const namespaceDefinition = getNamespaceDefinition(runtime.manifest, namespace);
|
|
468
|
+
if (namespaceDefinition.kind !== "data") {
|
|
469
|
+
throw new Error(`Cannot delete ${namespace}.${configPath} because namespace "${namespace}" is not a data namespace.`);
|
|
470
|
+
}
|
|
471
|
+
if (namespaceDefinition.readonly) {
|
|
472
|
+
throw new Error(`Cannot delete ${namespace}.${configPath} because namespace "${namespace}" is readonly.`);
|
|
473
|
+
}
|
|
474
|
+
if (namespaceDefinition.sensitive) {
|
|
475
|
+
throw new Error(`Cannot delete ${namespace}.${configPath} with the generic data writer because namespace "${namespace}" is sensitive.`);
|
|
476
|
+
}
|
|
550
477
|
const workspaceRoot = getSelectedWorkspaceRoot(options, runtime);
|
|
551
478
|
const profile = options.profile ?? runtime.graph.profile;
|
|
552
479
|
const filePath = resolveConfigDocumentPath(workspaceRoot, namespace, configPath, profile);
|
|
@@ -558,7 +485,7 @@ async function deleteValue(namespace, configPath, options = {}) {
|
|
|
558
485
|
deleted: false
|
|
559
486
|
};
|
|
560
487
|
}
|
|
561
|
-
await
|
|
488
|
+
await writeFile(filePath, stringifyYaml(document), "utf8");
|
|
562
489
|
return {
|
|
563
490
|
filePath,
|
|
564
491
|
deleted: true
|
|
@@ -568,6 +495,7 @@ async function deleteValue(namespace, configPath, options = {}) {
|
|
|
568
495
|
// src/commands/define.ts
|
|
569
496
|
async function runDefine(namespace, configPath, rawValue, options = {}) {
|
|
570
497
|
const cliArgs = [...options.cliArgs ?? []];
|
|
498
|
+
const root = path3.resolve(options.root ?? process.cwd());
|
|
571
499
|
const target = consumeOption(cliArgs, "--target") ?? "local";
|
|
572
500
|
const local = consumeFlag(cliArgs, "--local");
|
|
573
501
|
const remote = consumeFlag(cliArgs, "--remote");
|
|
@@ -593,7 +521,18 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
|
|
|
593
521
|
value: result.value
|
|
594
522
|
});
|
|
595
523
|
}
|
|
596
|
-
return `defined ${namespace}.${configPath} in ${result.filePath}`;
|
|
524
|
+
return `defined ${namespace}.${configPath} in ${displayPath(result.filePath, root)}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/commands/drift.ts
|
|
528
|
+
import { compareSchemaToGraph, formatDriftReport } from "@kitsy/cnos/internal";
|
|
529
|
+
async function runDrift(options = {}) {
|
|
530
|
+
const runtime = await createRuntimeService(options);
|
|
531
|
+
const report = compareSchemaToGraph(runtime);
|
|
532
|
+
if (options.json) {
|
|
533
|
+
return printJson(report);
|
|
534
|
+
}
|
|
535
|
+
return formatDriftReport(report);
|
|
597
536
|
}
|
|
598
537
|
|
|
599
538
|
// src/commands/diff.ts
|
|
@@ -634,8 +573,15 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
|
|
|
634
573
|
}
|
|
635
574
|
|
|
636
575
|
// src/services/doctor.ts
|
|
637
|
-
import { readFile as
|
|
638
|
-
import
|
|
576
|
+
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
577
|
+
import path4 from "path";
|
|
578
|
+
import {
|
|
579
|
+
detectLegacyVaultFormat,
|
|
580
|
+
isSecretReference as isSecretReference2,
|
|
581
|
+
parseYaml as parseYaml2,
|
|
582
|
+
readKeychain,
|
|
583
|
+
resolveSecretStoreRoot
|
|
584
|
+
} from "@kitsy/cnos/internal";
|
|
639
585
|
|
|
640
586
|
// src/services/validation.ts
|
|
641
587
|
import { validateRuntime } from "@kitsy/cnos/internal";
|
|
@@ -650,7 +596,7 @@ async function createValidationSummary(options = {}) {
|
|
|
650
596
|
|
|
651
597
|
// src/services/doctor.ts
|
|
652
598
|
async function checkGitignore(root) {
|
|
653
|
-
const gitignorePath =
|
|
599
|
+
const gitignorePath = path4.join(root, ".gitignore");
|
|
654
600
|
const expected = [
|
|
655
601
|
".cnos/env/.env",
|
|
656
602
|
".cnos/env/.env.*",
|
|
@@ -662,7 +608,7 @@ async function checkGitignore(root) {
|
|
|
662
608
|
"!.cnos/workspaces/*/env/.env.*.example"
|
|
663
609
|
];
|
|
664
610
|
try {
|
|
665
|
-
const content = await
|
|
611
|
+
const content = await readFile2(gitignorePath, "utf8");
|
|
666
612
|
const missing = expected.filter((entry) => !content.includes(entry));
|
|
667
613
|
return {
|
|
668
614
|
name: "gitignore",
|
|
@@ -680,11 +626,76 @@ async function checkGitignore(root) {
|
|
|
680
626
|
function issueSummary(issues) {
|
|
681
627
|
return issues.length === 0 ? "no issues" : issues.map((issue) => issue.message).join("; ");
|
|
682
628
|
}
|
|
629
|
+
async function collectYamlFiles(root) {
|
|
630
|
+
try {
|
|
631
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
632
|
+
const results = [];
|
|
633
|
+
for (const entry of entries) {
|
|
634
|
+
const target = path4.join(root, entry.name);
|
|
635
|
+
if (entry.isDirectory()) {
|
|
636
|
+
results.push(...await collectYamlFiles(target));
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (entry.isFile() && [".yml", ".yaml"].includes(path4.extname(entry.name).toLowerCase())) {
|
|
640
|
+
results.push(target);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return results;
|
|
644
|
+
} catch {
|
|
645
|
+
return [];
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function hasPlaintextSecret(value) {
|
|
649
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
650
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
651
|
+
}
|
|
652
|
+
if (isSecretReference2(value)) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
return Object.values(value).some((entry) => hasPlaintextSecret(entry));
|
|
656
|
+
}
|
|
657
|
+
async function checkSecretSecurity(options, runtime) {
|
|
658
|
+
const storeRoot = resolveSecretStoreRoot(options.processEnv);
|
|
659
|
+
const legacyPaths = await Promise.all(
|
|
660
|
+
Object.entries(runtime.manifest.vaults).filter(([, definition]) => definition.provider === "local").map(async ([vault]) => ({ vault, path: await detectLegacyVaultFormat(storeRoot, vault) }))
|
|
661
|
+
);
|
|
662
|
+
const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
|
|
663
|
+
const secretFiles = await Promise.all(
|
|
664
|
+
runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path4.join(root.path, "secrets")))
|
|
665
|
+
);
|
|
666
|
+
const plaintextFiles = [];
|
|
667
|
+
for (const file of secretFiles.flat()) {
|
|
668
|
+
try {
|
|
669
|
+
const parsed = parseYaml2(await readFile2(file, "utf8"));
|
|
670
|
+
if (hasPlaintextSecret(parsed)) {
|
|
671
|
+
plaintextFiles.push(file);
|
|
672
|
+
}
|
|
673
|
+
} catch {
|
|
674
|
+
plaintextFiles.push(file);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const keychainWarnings = await Promise.all(
|
|
678
|
+
Object.entries(runtime.manifest.vaults).filter(([, definition]) => definition.provider === "local").flatMap(
|
|
679
|
+
([vault, definition]) => (definition.auth?.passphrase?.from ?? []).filter((source) => source.startsWith("keychain:")).map(async (source) => ({ vault, source, value: await readKeychain(source.slice("keychain:".length)) }))
|
|
680
|
+
)
|
|
681
|
+
);
|
|
682
|
+
const warnings = [
|
|
683
|
+
...legacyDetected.map((entry) => `legacy vault ${entry.vault}: ${entry.path}`),
|
|
684
|
+
...plaintextFiles.map((file) => `plaintext secret file: ${file}`),
|
|
685
|
+
...keychainWarnings.filter((entry) => !entry.value).map((entry) => `no keychain entry for vault ${entry.vault} (${entry.source})`)
|
|
686
|
+
];
|
|
687
|
+
return {
|
|
688
|
+
name: "security",
|
|
689
|
+
ok: warnings.length === 0,
|
|
690
|
+
details: warnings.length === 0 ? "no legacy vaults, plaintext secret files, or missing keychain entries" : warnings.join("; ")
|
|
691
|
+
};
|
|
692
|
+
}
|
|
683
693
|
async function evaluateDoctor(options = {}) {
|
|
684
|
-
const root =
|
|
694
|
+
const root = path4.resolve(options.root ?? process.cwd());
|
|
685
695
|
const { runtime, summary } = await createValidationSummary(options);
|
|
686
696
|
const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
|
|
687
697
|
const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
|
|
698
|
+
const declaredCustomNamespaces = Object.entries(runtime.manifest.namespaces).filter(([namespace]) => !["value", "secret", "meta", "process", "public", "env"].includes(namespace)).map(([namespace, definition]) => `${namespace}(${definition.kind}${definition.shareable ? ",shareable" : ""}${definition.readonly ? ",readonly" : ""})`).sort((left, right) => left.localeCompare(right));
|
|
688
699
|
return [
|
|
689
700
|
{
|
|
690
701
|
name: "manifest",
|
|
@@ -696,6 +707,11 @@ async function evaluateDoctor(options = {}) {
|
|
|
696
707
|
ok: true,
|
|
697
708
|
details: `${runtime.graph.workspace.workspaceId} via ${runtime.graph.workspace.workspaceSource}`
|
|
698
709
|
},
|
|
710
|
+
{
|
|
711
|
+
name: "namespaces",
|
|
712
|
+
ok: true,
|
|
713
|
+
details: declaredCustomNamespaces.length === 0 ? "built-ins: value, secret, meta, process, public, env" : `built-ins: value, secret, meta, process, public, env | custom: ${declaredCustomNamespaces.join(", ")}`
|
|
714
|
+
},
|
|
699
715
|
{
|
|
700
716
|
name: "source-roots",
|
|
701
717
|
ok: Boolean(localRoot),
|
|
@@ -711,6 +727,7 @@ async function evaluateDoctor(options = {}) {
|
|
|
711
727
|
ok: !runtime.manifest.workspaces.global.enabled || Boolean(runtime.graph.workspace.globalRoot),
|
|
712
728
|
details: runtime.manifest.workspaces.global.enabled ? runtime.graph.workspace.globalRoot ? `enabled at ${runtime.graph.workspace.globalRoot}` : "enabled but no global root resolved" : "disabled"
|
|
713
729
|
},
|
|
730
|
+
await checkSecretSecurity(options, runtime),
|
|
714
731
|
await checkGitignore(root)
|
|
715
732
|
];
|
|
716
733
|
}
|
|
@@ -729,7 +746,7 @@ async function runDoctor(options = {}) {
|
|
|
729
746
|
}
|
|
730
747
|
|
|
731
748
|
// src/commands/dump.ts
|
|
732
|
-
import { writeDump } from "@kitsy/cnos";
|
|
749
|
+
import { writeDump } from "@kitsy/cnos/configure";
|
|
733
750
|
async function runDump(options = {}) {
|
|
734
751
|
const cliArgs = [...options.cliArgs ?? []];
|
|
735
752
|
const flatten = consumeFlag(cliArgs, "--flatten");
|
|
@@ -751,9 +768,51 @@ async function runDump(options = {}) {
|
|
|
751
768
|
return `dumped ${result.files.length} files to ${result.root}`;
|
|
752
769
|
}
|
|
753
770
|
|
|
771
|
+
// src/commands/codegen.ts
|
|
772
|
+
import { watchSchema, writeCodegenOutput } from "@kitsy/cnos/internal";
|
|
773
|
+
async function runCodegen(options = {}) {
|
|
774
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
775
|
+
const out = consumeOption(cliArgs, "--out");
|
|
776
|
+
const watch2 = consumeFlag(cliArgs, "--watch");
|
|
777
|
+
if (cliArgs.length > 0) {
|
|
778
|
+
throw new Error(`Unknown codegen options: ${cliArgs.join(" ")}`);
|
|
779
|
+
}
|
|
780
|
+
if (watch2) {
|
|
781
|
+
const watcher = await watchSchema({
|
|
782
|
+
...options.root ? {
|
|
783
|
+
root: options.root
|
|
784
|
+
} : {},
|
|
785
|
+
...out ? {
|
|
786
|
+
out
|
|
787
|
+
} : {},
|
|
788
|
+
onError(error) {
|
|
789
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
790
|
+
process.stderr.write(`${message}
|
|
791
|
+
`);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
const closeWatcher = () => {
|
|
795
|
+
watcher.close();
|
|
796
|
+
};
|
|
797
|
+
process.once("SIGINT", closeWatcher);
|
|
798
|
+
process.once("SIGTERM", closeWatcher);
|
|
799
|
+
return `watching schema changes -> ${out ?? ".cnos/types/cnos.d.ts"}`;
|
|
800
|
+
}
|
|
801
|
+
const result = await writeCodegenOutput({
|
|
802
|
+
...options.root ? {
|
|
803
|
+
root: options.root
|
|
804
|
+
} : {},
|
|
805
|
+
...out ? {
|
|
806
|
+
out
|
|
807
|
+
} : {}
|
|
808
|
+
});
|
|
809
|
+
const summary = result.hasSchema ? `generated types from ${result.schemaEntryCount} schema entr${result.schemaEntryCount === 1 ? "y" : "ies"}` : "generated empty types (no schema section found)";
|
|
810
|
+
return `${summary} -> ${result.typesPath} and ${result.runtimePath}`;
|
|
811
|
+
}
|
|
812
|
+
|
|
754
813
|
// src/commands/exportEnv.ts
|
|
755
|
-
import { mkdir as mkdir2, writeFile as
|
|
756
|
-
import
|
|
814
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
815
|
+
import path5 from "path";
|
|
757
816
|
function formatEnvOutput(env) {
|
|
758
817
|
return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
|
|
759
818
|
}
|
|
@@ -773,9 +832,9 @@ async function runExportEnv(options = {}) {
|
|
|
773
832
|
}) : runtime.toEnv();
|
|
774
833
|
const output = formatEnvOutput(env);
|
|
775
834
|
if (to) {
|
|
776
|
-
const targetPath =
|
|
777
|
-
await mkdir2(
|
|
778
|
-
await
|
|
835
|
+
const targetPath = path5.resolve(options.root ?? process.cwd(), to);
|
|
836
|
+
await mkdir2(path5.dirname(targetPath), { recursive: true });
|
|
837
|
+
await writeFile2(targetPath, output, "utf8");
|
|
779
838
|
if (options.json) {
|
|
780
839
|
return printJson({
|
|
781
840
|
to: targetPath,
|
|
@@ -852,6 +911,23 @@ var COMMANDS = [
|
|
|
852
911
|
],
|
|
853
912
|
examples: ["cnos onboard", "cnos onboard --workspace webapp", "cnos onboard --root ../my-app --workspace app --move"]
|
|
854
913
|
},
|
|
914
|
+
{
|
|
915
|
+
id: "codegen",
|
|
916
|
+
summary: "Generate typed CNOS access wrappers from schema.",
|
|
917
|
+
usage: "cnos codegen [--out <path>] [--watch] [--root <path>]",
|
|
918
|
+
description: "Reads schema from .cnos/cnos.yml and generates typed CNOS declaration output plus a typed createCnos wrapper.",
|
|
919
|
+
options: [
|
|
920
|
+
{
|
|
921
|
+
flag: "--out <path>",
|
|
922
|
+
description: "Custom path for the generated type declaration file. runtime.ts is emitted beside it."
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
flag: "--watch",
|
|
926
|
+
description: "Watch the manifest schema and regenerate output when it changes."
|
|
927
|
+
}
|
|
928
|
+
],
|
|
929
|
+
examples: ["cnos codegen", "cnos codegen --out src/cnos-config.d.ts", "cnos codegen --watch"]
|
|
930
|
+
},
|
|
855
931
|
{
|
|
856
932
|
id: "read",
|
|
857
933
|
summary: "Read any fully-qualified CNOS key.",
|
|
@@ -943,18 +1019,19 @@ var COMMANDS = [
|
|
|
943
1019
|
flag: "--provider <name>",
|
|
944
1020
|
description: "Provider name for --remote or --ref secret writes."
|
|
945
1021
|
},
|
|
946
|
-
{
|
|
947
|
-
flag: "--passphrase <value>",
|
|
948
|
-
description: "Passphrase used to encrypt local secret material when --local is selected."
|
|
949
|
-
},
|
|
950
1022
|
{
|
|
951
1023
|
flag: "--vault <name>",
|
|
952
1024
|
description: "Use a manifest-defined vault. Provider behavior is inferred from the vault definition."
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
flag: "--reveal",
|
|
1028
|
+
description: "Reveal the resolved secret value for get-style reads. Output is masked by default."
|
|
953
1029
|
}
|
|
954
1030
|
],
|
|
955
1031
|
examples: [
|
|
956
1032
|
"cnos secret app.token",
|
|
957
|
-
"cnos vault create local-dev
|
|
1033
|
+
"cnos vault create local-dev",
|
|
1034
|
+
"cnos vault auth local-dev",
|
|
958
1035
|
"cnos secret set app.token super-secret --vault local-dev",
|
|
959
1036
|
"cnos vault create github-ci --provider github-secrets --no-passphrase",
|
|
960
1037
|
"cnos secret set app.token APP_TOKEN --vault github-ci"
|
|
@@ -970,17 +1047,14 @@ var COMMANDS = [
|
|
|
970
1047
|
flag: "--provider <local|github-secrets>",
|
|
971
1048
|
description: "Vault provider. Defaults to local."
|
|
972
1049
|
},
|
|
973
|
-
{
|
|
974
|
-
flag: "--passphrase <value>",
|
|
975
|
-
description: "Required for local vault creation unless already available in the configured passphrase env var."
|
|
976
|
-
},
|
|
977
1050
|
{
|
|
978
1051
|
flag: "--no-passphrase",
|
|
979
1052
|
description: "Allowed for passwordless providers such as github-secrets."
|
|
980
1053
|
}
|
|
981
1054
|
],
|
|
982
1055
|
examples: [
|
|
983
|
-
"cnos vault create local-dev
|
|
1056
|
+
"cnos vault create local-dev",
|
|
1057
|
+
"cnos vault auth local-dev",
|
|
984
1058
|
"cnos vault create github-ci --provider github-secrets --no-passphrase",
|
|
985
1059
|
"cnos vault list",
|
|
986
1060
|
"cnos vault remove local-dev"
|
|
@@ -989,13 +1063,33 @@ var COMMANDS = [
|
|
|
989
1063
|
{
|
|
990
1064
|
id: "vault create",
|
|
991
1065
|
summary: "Create a manifest-defined vault.",
|
|
992
|
-
usage: "cnos vault create <name> [--provider <local|github-secrets>] [--
|
|
993
|
-
description: "Creates a vault definition in .cnos/cnos.yml and, for local vaults, initializes the encrypted store under ~/.cnos/secrets.",
|
|
1066
|
+
usage: "cnos vault create <name> [--provider <local|github-secrets>] [--no-passphrase] [global-options]",
|
|
1067
|
+
description: "Creates a vault definition in .cnos/cnos.yml and, for local vaults, initializes the encrypted store under ~/.cnos/secrets. CNOS prompts for a passphrase when one is not already available from env or keychain.",
|
|
994
1068
|
examples: [
|
|
995
|
-
"cnos vault create local-dev
|
|
1069
|
+
"cnos vault create local-dev",
|
|
996
1070
|
"cnos vault create github-ci --provider github-secrets --no-passphrase"
|
|
997
1071
|
]
|
|
998
1072
|
},
|
|
1073
|
+
{
|
|
1074
|
+
id: "vault auth",
|
|
1075
|
+
summary: "Authenticate a vault for the current shell session.",
|
|
1076
|
+
usage: "cnos vault auth <name> [--store-keychain] [global-options]",
|
|
1077
|
+
description: "Authenticates an existing local vault using env, keychain, or prompt-based auth and stores a derived session key for later CNOS commands in the same shell. Wrong passphrases fail authentication.",
|
|
1078
|
+
examples: ["cnos vault auth local-dev", "cnos vault auth local-dev --store-keychain"]
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
id: "vault logout",
|
|
1082
|
+
summary: "Clear vault auth state for the current shell session.",
|
|
1083
|
+
usage: "cnos vault logout <name> [global-options]",
|
|
1084
|
+
description: "Removes active vault session auth for the selected vault or all vaults when used with --all.",
|
|
1085
|
+
options: [
|
|
1086
|
+
{
|
|
1087
|
+
flag: "--all",
|
|
1088
|
+
description: "Clear all active vault auth sessions for the current shell context."
|
|
1089
|
+
}
|
|
1090
|
+
],
|
|
1091
|
+
examples: ["cnos vault logout local-dev", "cnos vault logout --all"]
|
|
1092
|
+
},
|
|
999
1093
|
{
|
|
1000
1094
|
id: "vault list",
|
|
1001
1095
|
summary: "List manifest-defined vaults.",
|
|
@@ -1053,11 +1147,11 @@ var COMMANDS = [
|
|
|
1053
1147
|
{
|
|
1054
1148
|
id: "list",
|
|
1055
1149
|
summary: "List resolved config entries.",
|
|
1056
|
-
usage: "cnos list [
|
|
1057
|
-
description: "Lists stored config or derived projections across one namespace or the full effective graph, with optional prefix filtering.",
|
|
1150
|
+
usage: "cnos list [<namespace>|all] [--prefix <path>] [--framework <name>] [global-options]",
|
|
1151
|
+
description: "Lists stored config or derived projections across one namespace or the full effective graph, with optional prefix filtering. Custom data namespaces such as flags are supported, and process exposes server-only ambient runtime state.",
|
|
1058
1152
|
options: [
|
|
1059
1153
|
{
|
|
1060
|
-
flag: "--namespace <
|
|
1154
|
+
flag: "--namespace <name>",
|
|
1061
1155
|
description: "Explicit namespace selector when not using a positional namespace argument."
|
|
1062
1156
|
},
|
|
1063
1157
|
{
|
|
@@ -1069,7 +1163,7 @@ var COMMANDS = [
|
|
|
1069
1163
|
description: "When listing public output, apply framework-specific prefixes such as vite or next."
|
|
1070
1164
|
}
|
|
1071
1165
|
],
|
|
1072
|
-
examples: ["cnos list", "cnos list value --prefix app.", "cnos list env", "cnos list public --framework vite"]
|
|
1166
|
+
examples: ["cnos list", "cnos list value --prefix app.", "cnos list flags", "cnos list process --prefix env.PATH", "cnos list env", "cnos list public --framework vite"]
|
|
1073
1167
|
},
|
|
1074
1168
|
{
|
|
1075
1169
|
id: "profile",
|
|
@@ -1079,11 +1173,16 @@ var COMMANDS = [
|
|
|
1079
1173
|
options: [
|
|
1080
1174
|
{
|
|
1081
1175
|
flag: "--inherit <name>",
|
|
1082
|
-
description: "Parent profile to extend when creating a profile."
|
|
1176
|
+
description: "Parent profile to extend when creating a profile. Base inheritance is implicit by default."
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
flag: "--no-inherit",
|
|
1180
|
+
description: "Create a clean profile that does not inherit base fallback layers."
|
|
1083
1181
|
}
|
|
1084
1182
|
],
|
|
1085
1183
|
examples: [
|
|
1086
|
-
"cnos profile create stage
|
|
1184
|
+
"cnos profile create stage",
|
|
1185
|
+
"cnos profile create isolated --no-inherit",
|
|
1087
1186
|
"cnos profile list",
|
|
1088
1187
|
"cnos profile use stage"
|
|
1089
1188
|
]
|
|
@@ -1091,9 +1190,9 @@ var COMMANDS = [
|
|
|
1091
1190
|
{
|
|
1092
1191
|
id: "profile create",
|
|
1093
1192
|
summary: "Create a profile definition.",
|
|
1094
|
-
usage: "cnos profile create <name> [--inherit <name>] [--root <path>] [--json]",
|
|
1095
|
-
description: "Creates .cnos/profiles/<name>.yml for an explicit profile overlay.",
|
|
1096
|
-
examples: ["cnos profile create stage --inherit
|
|
1193
|
+
usage: "cnos profile create <name> [--inherit <name> | --no-inherit] [--root <path>] [--json]",
|
|
1194
|
+
description: "Creates .cnos/profiles/<name>.yml for an explicit profile overlay. New profiles inherit base by default unless --no-inherit is set.",
|
|
1195
|
+
examples: ["cnos profile create stage", "cnos profile create isolated --no-inherit"]
|
|
1097
1196
|
},
|
|
1098
1197
|
{
|
|
1099
1198
|
id: "profile list",
|
|
@@ -1120,7 +1219,7 @@ var COMMANDS = [
|
|
|
1120
1219
|
id: "promote",
|
|
1121
1220
|
summary: "Promote shareable config into public or env projection surfaces.",
|
|
1122
1221
|
usage: "cnos promote <key...> --to <public|env> [--as <ENV_VAR>] [global-options]",
|
|
1123
|
-
description: "Adds keys to public.promote or envMapping.explicit in .cnos/cnos.yml. Sensitive or non-shareable namespaces are rejected.",
|
|
1222
|
+
description: "Adds keys to public.promote or envMapping.explicit in .cnos/cnos.yml. Sensitive or non-shareable namespaces are rejected, but declared shareable data namespaces such as flags are allowed.",
|
|
1124
1223
|
options: [
|
|
1125
1224
|
{
|
|
1126
1225
|
flag: "--to <public|env>",
|
|
@@ -1133,16 +1232,18 @@ var COMMANDS = [
|
|
|
1133
1232
|
],
|
|
1134
1233
|
examples: [
|
|
1135
1234
|
"cnos promote value.flag.auth.upi_enabled --to public",
|
|
1235
|
+
"cnos promote flags.upi_enabled --to public",
|
|
1136
1236
|
"cnos promote value.server.port --to env --as PORT"
|
|
1137
1237
|
]
|
|
1138
1238
|
},
|
|
1139
1239
|
{
|
|
1140
1240
|
id: "secret set",
|
|
1141
1241
|
summary: "Write a secret securely.",
|
|
1142
|
-
usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [
|
|
1242
|
+
usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [global-options]",
|
|
1143
1243
|
description: "Writes a secret reference into the repo. When a local vault is selected, CNOS stores encrypted secret material outside the repo under ~/.cnos/secrets/vaults/<vault>; when a github-secrets vault is selected, CNOS writes a CI env-backed ref.",
|
|
1144
1244
|
examples: [
|
|
1145
|
-
"cnos vault create db
|
|
1245
|
+
"cnos vault create db",
|
|
1246
|
+
"cnos vault auth db",
|
|
1146
1247
|
"cnos secret set app.token super-secret --vault db",
|
|
1147
1248
|
"cnos vault create github-ci --provider github-secrets --no-passphrase",
|
|
1148
1249
|
"cnos secret set app.token APP_TOKEN --vault github-ci"
|
|
@@ -1151,9 +1252,9 @@ var COMMANDS = [
|
|
|
1151
1252
|
{
|
|
1152
1253
|
id: "secret create vault",
|
|
1153
1254
|
summary: "Create a local secret vault.",
|
|
1154
|
-
usage: "cnos secret create vault <name>
|
|
1255
|
+
usage: "cnos secret create vault <name> [global-options]",
|
|
1155
1256
|
description: "Alias for cnos vault create <name>.",
|
|
1156
|
-
examples: ["cnos secret create vault db
|
|
1257
|
+
examples: ["cnos secret create vault db"]
|
|
1157
1258
|
},
|
|
1158
1259
|
{
|
|
1159
1260
|
id: "secret list",
|
|
@@ -1310,6 +1411,60 @@ var COMMANDS = [
|
|
|
1310
1411
|
description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace.",
|
|
1311
1412
|
examples: ["cnos doctor", "cnos doctor --workspace api --json"]
|
|
1312
1413
|
},
|
|
1414
|
+
{
|
|
1415
|
+
id: "drift",
|
|
1416
|
+
summary: "Compare resolved config against schema and report drift.",
|
|
1417
|
+
usage: "cnos drift [--workspace <id>] [--profile <name>] [--json]",
|
|
1418
|
+
description: "Reports missing required keys, undeclared keys, type mismatches, and defaults applied for the selected workspace/profile.",
|
|
1419
|
+
examples: ["cnos drift", "cnos drift --workspace api --profile stage", "cnos drift --json"]
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
id: "watch",
|
|
1423
|
+
summary: "Watch CNOS inputs and either restart a process or emit changed keys.",
|
|
1424
|
+
usage: "cnos watch [--signal] [--debounce <ms>] [global-options] -- <command...>",
|
|
1425
|
+
description: "Watches the active manifest, workspace roots, env files, and config documents. In restart mode it respawns the child command after changes; in signal mode it prints changed keys as JSON.",
|
|
1426
|
+
options: [
|
|
1427
|
+
{
|
|
1428
|
+
flag: "--signal",
|
|
1429
|
+
description: "Emit changed keys as JSON instead of restarting a child process."
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
flag: "--debounce <ms>",
|
|
1433
|
+
description: "Debounce change handling before re-resolving the graph. Defaults to 300ms."
|
|
1434
|
+
}
|
|
1435
|
+
],
|
|
1436
|
+
examples: ["cnos watch -- node server.js", "cnos watch --signal", "cnos watch --debounce 100 -- node server.js"]
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
id: "migrate",
|
|
1440
|
+
summary: "Scan env usage and propose CNOS manifest mappings.",
|
|
1441
|
+
usage: "cnos migrate [--scan <path>] [--dry-run] [--apply] [--rewrite] [global-options]",
|
|
1442
|
+
description: "Scans JS/TS source for process.env and import.meta.env usage, proposes logical CNOS mappings, updates envMapping/public promote entries, and can rewrite supported source files with backups.",
|
|
1443
|
+
options: [
|
|
1444
|
+
{
|
|
1445
|
+
flag: "--scan <path>",
|
|
1446
|
+
description: "Directory to scan. Defaults to ./src relative to the repo root."
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
flag: "--dry-run",
|
|
1450
|
+
description: "Preview the proposed mappings without changing the manifest."
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
flag: "--apply",
|
|
1454
|
+
description: "Write proposed env mappings and public promotions into .cnos/cnos.yml."
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
flag: "--rewrite",
|
|
1458
|
+
description: "With --apply, rewrite supported process.env usages in source files and create .bak backups."
|
|
1459
|
+
}
|
|
1460
|
+
],
|
|
1461
|
+
examples: [
|
|
1462
|
+
"cnos migrate",
|
|
1463
|
+
"cnos migrate --scan src --dry-run",
|
|
1464
|
+
"cnos migrate --scan apps/api/src --apply",
|
|
1465
|
+
"cnos migrate --apply --rewrite"
|
|
1466
|
+
]
|
|
1467
|
+
},
|
|
1313
1468
|
{
|
|
1314
1469
|
id: "help",
|
|
1315
1470
|
summary: "Show human-readable CLI help.",
|
|
@@ -1500,11 +1655,11 @@ function runHelpAi(topic, cliArgs = []) {
|
|
|
1500
1655
|
}
|
|
1501
1656
|
|
|
1502
1657
|
// src/commands/init.ts
|
|
1503
|
-
import
|
|
1658
|
+
import path7 from "path";
|
|
1504
1659
|
|
|
1505
1660
|
// src/services/scaffold.ts
|
|
1506
|
-
import { mkdir as mkdir3, readFile as
|
|
1507
|
-
import
|
|
1661
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1662
|
+
import path6 from "path";
|
|
1508
1663
|
function scaffoldManifest(projectName, workspace) {
|
|
1509
1664
|
const lines = [
|
|
1510
1665
|
"version: 1",
|
|
@@ -1535,15 +1690,15 @@ function scaffoldManifest(projectName, workspace) {
|
|
|
1535
1690
|
}
|
|
1536
1691
|
async function ensureFile(filePath, content) {
|
|
1537
1692
|
try {
|
|
1538
|
-
await
|
|
1693
|
+
await readFile3(filePath, "utf8");
|
|
1539
1694
|
return false;
|
|
1540
1695
|
} catch {
|
|
1541
|
-
await
|
|
1696
|
+
await writeFile3(filePath, content, "utf8");
|
|
1542
1697
|
return true;
|
|
1543
1698
|
}
|
|
1544
1699
|
}
|
|
1545
1700
|
async function ensureGitignore(root) {
|
|
1546
|
-
const gitignorePath =
|
|
1701
|
+
const gitignorePath = path6.join(root, ".gitignore");
|
|
1547
1702
|
const requiredEntries = [
|
|
1548
1703
|
".cnos/env/.env",
|
|
1549
1704
|
".cnos/env/.env.*",
|
|
@@ -1556,7 +1711,7 @@ async function ensureGitignore(root) {
|
|
|
1556
1711
|
];
|
|
1557
1712
|
let current = "";
|
|
1558
1713
|
try {
|
|
1559
|
-
current = await
|
|
1714
|
+
current = await readFile3(gitignorePath, "utf8");
|
|
1560
1715
|
} catch {
|
|
1561
1716
|
current = "";
|
|
1562
1717
|
}
|
|
@@ -1566,18 +1721,18 @@ async function ensureGitignore(root) {
|
|
|
1566
1721
|
}
|
|
1567
1722
|
const prefix = current.trim().length > 0 ? `${current.trimEnd()}
|
|
1568
1723
|
` : "";
|
|
1569
|
-
await
|
|
1724
|
+
await writeFile3(gitignorePath, `${prefix}${missingEntries.join("\n")}
|
|
1570
1725
|
`, "utf8");
|
|
1571
1726
|
return true;
|
|
1572
1727
|
}
|
|
1573
1728
|
async function scaffoldWorkspace(root, workspace) {
|
|
1574
|
-
const cnosRoot =
|
|
1575
|
-
const workspaceRoot = workspace ?
|
|
1729
|
+
const cnosRoot = path6.join(root, ".cnos");
|
|
1730
|
+
const workspaceRoot = workspace ? path6.join(cnosRoot, "workspaces", workspace) : cnosRoot;
|
|
1576
1731
|
const createdPaths = [];
|
|
1577
|
-
await mkdir3(
|
|
1578
|
-
await mkdir3(
|
|
1579
|
-
await mkdir3(
|
|
1580
|
-
await mkdir3(
|
|
1732
|
+
await mkdir3(path6.join(workspaceRoot, "profiles"), { recursive: true });
|
|
1733
|
+
await mkdir3(path6.join(workspaceRoot, "values"), { recursive: true });
|
|
1734
|
+
await mkdir3(path6.join(workspaceRoot, "secrets"), { recursive: true });
|
|
1735
|
+
await mkdir3(path6.join(workspaceRoot, "env"), { recursive: true });
|
|
1581
1736
|
const relativePaths = workspace ? [
|
|
1582
1737
|
["workspaces", workspace, "profiles", ".gitkeep"],
|
|
1583
1738
|
["workspaces", workspace, "values", ".gitkeep"],
|
|
@@ -1590,15 +1745,15 @@ async function scaffoldWorkspace(root, workspace) {
|
|
|
1590
1745
|
["env", ".gitkeep"]
|
|
1591
1746
|
];
|
|
1592
1747
|
for (const relativePath of relativePaths) {
|
|
1593
|
-
const filePath =
|
|
1748
|
+
const filePath = path6.join(cnosRoot, ...relativePath);
|
|
1594
1749
|
if (await ensureFile(filePath, "")) {
|
|
1595
|
-
createdPaths.push(
|
|
1750
|
+
createdPaths.push(path6.relative(root, filePath).replace(/\\/g, "/"));
|
|
1596
1751
|
}
|
|
1597
1752
|
}
|
|
1598
|
-
if (await ensureFile(
|
|
1753
|
+
if (await ensureFile(path6.join(cnosRoot, "cnos.yml"), scaffoldManifest(path6.basename(root), workspace))) {
|
|
1599
1754
|
createdPaths.push(".cnos/cnos.yml");
|
|
1600
1755
|
}
|
|
1601
|
-
if (workspace && await ensureFile(
|
|
1756
|
+
if (workspace && await ensureFile(path6.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
|
|
1602
1757
|
globalRoot: ~/.cnos
|
|
1603
1758
|
`)) {
|
|
1604
1759
|
createdPaths.push(".cnos-workspace.yml");
|
|
@@ -1615,7 +1770,7 @@ globalRoot: ~/.cnos
|
|
|
1615
1770
|
|
|
1616
1771
|
// src/commands/init.ts
|
|
1617
1772
|
async function runInit(options = {}) {
|
|
1618
|
-
const root =
|
|
1773
|
+
const root = path7.resolve(options.root ?? process.cwd());
|
|
1619
1774
|
const result = await scaffoldWorkspace(root, options.workspace);
|
|
1620
1775
|
if (options.json) {
|
|
1621
1776
|
return printJson(result);
|
|
@@ -1626,6 +1781,12 @@ async function runInit(options = {}) {
|
|
|
1626
1781
|
return `initialized CNOS project at ${root}`;
|
|
1627
1782
|
}
|
|
1628
1783
|
|
|
1784
|
+
// src/format/maskSecret.ts
|
|
1785
|
+
var MASKED_SECRET_VALUE = "****";
|
|
1786
|
+
function maskSecretValue(value) {
|
|
1787
|
+
return value === void 0 ? "" : MASKED_SECRET_VALUE;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1629
1790
|
// src/format/printInspect.ts
|
|
1630
1791
|
function printInspect(record) {
|
|
1631
1792
|
const lines = [
|
|
@@ -1650,12 +1811,22 @@ function printInspect(record) {
|
|
|
1650
1811
|
|
|
1651
1812
|
// src/commands/inspect.ts
|
|
1652
1813
|
async function runInspect(key, options = {}) {
|
|
1814
|
+
const reveal = options.cliArgs?.includes("--reveal") ?? false;
|
|
1653
1815
|
const runtime = await createRuntimeService(options);
|
|
1654
1816
|
const inspectResult = runtime.inspect(key);
|
|
1817
|
+
const value = key.startsWith("secret.") && !reveal ? maskSecretValue(inspectResult.value) : inspectResult.value;
|
|
1818
|
+
const printable = {
|
|
1819
|
+
...inspectResult,
|
|
1820
|
+
value,
|
|
1821
|
+
overridden: inspectResult.overridden.map((entry) => ({
|
|
1822
|
+
...entry,
|
|
1823
|
+
value: key.startsWith("secret.") && !reveal ? maskSecretValue(entry.value) : entry.value
|
|
1824
|
+
}))
|
|
1825
|
+
};
|
|
1655
1826
|
if (options.json) {
|
|
1656
|
-
return printJson(
|
|
1827
|
+
return printJson(printable);
|
|
1657
1828
|
}
|
|
1658
|
-
return printInspect(
|
|
1829
|
+
return printInspect(printable);
|
|
1659
1830
|
}
|
|
1660
1831
|
|
|
1661
1832
|
// src/format/printValue.ts
|
|
@@ -1691,7 +1862,7 @@ function matchesPrefix(key, prefix) {
|
|
|
1691
1862
|
return key.startsWith(prefix) || key.split(".").slice(1).join(".").startsWith(prefix);
|
|
1692
1863
|
}
|
|
1693
1864
|
function toStoredEntry(namespace, entry, filter = {}) {
|
|
1694
|
-
const sourceId = namespace === "
|
|
1865
|
+
const sourceId = namespace === "secret" ? "filesystem-secrets" : "filesystem-values";
|
|
1695
1866
|
const candidates = [entry.winner, ...entry.overridden].filter((candidate) => candidate.sourceId === sourceId);
|
|
1696
1867
|
if (candidates.length === 0) {
|
|
1697
1868
|
return void 0;
|
|
@@ -1712,16 +1883,16 @@ function listStoredNamespace(namespace, options) {
|
|
|
1712
1883
|
}
|
|
1713
1884
|
function listProjectedNamespace(namespace, options) {
|
|
1714
1885
|
return createRuntimeService(options).then((runtime) => {
|
|
1715
|
-
const projected = namespace === "meta" ? flattenObject2(runtime.toNamespace("meta")) : namespace === "env" ? runtime.toEnv() : runtime.toPublicEnv({
|
|
1886
|
+
const projected = namespace === "meta" ? flattenObject2(runtime.toNamespace("meta")) : namespace === "env" ? runtime.toEnv() : namespace === "public" ? runtime.toPublicEnv({
|
|
1716
1887
|
...options.framework ? {
|
|
1717
1888
|
framework: options.framework
|
|
1718
1889
|
} : {}
|
|
1719
|
-
});
|
|
1890
|
+
}) : flattenObject2(runtime.toNamespace(namespace));
|
|
1720
1891
|
const entries = namespace === "env" ? Object.entries(projected).map(([envVar, value]) => ({
|
|
1721
1892
|
key: envVar,
|
|
1722
1893
|
value
|
|
1723
1894
|
})) : Object.entries(projected).map(([key, value]) => ({
|
|
1724
|
-
key: namespace === "meta" ?
|
|
1895
|
+
key: namespace === "meta" || namespace === "process" ? `${namespace}.${key}` : key,
|
|
1725
1896
|
value
|
|
1726
1897
|
}));
|
|
1727
1898
|
return entries.filter((entry) => entry.value !== void 0).filter((entry) => matchesPrefix(entry.key, options.prefix)).sort((left, right) => left.key.localeCompare(right.key));
|
|
@@ -1731,15 +1902,21 @@ async function listConfigEntries(namespace, options = {}) {
|
|
|
1731
1902
|
if (namespace === "value" || namespace === "secret") {
|
|
1732
1903
|
return listStoredNamespace(namespace, options);
|
|
1733
1904
|
}
|
|
1734
|
-
if (namespace === "meta" || namespace === "env" || namespace === "public") {
|
|
1905
|
+
if (namespace === "meta" || namespace === "env" || namespace === "public" || namespace === "process") {
|
|
1735
1906
|
return listProjectedNamespace(namespace, options);
|
|
1736
1907
|
}
|
|
1737
|
-
|
|
1738
|
-
listStoredNamespace(
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1908
|
+
if (namespace !== "all") {
|
|
1909
|
+
return listStoredNamespace(namespace, options);
|
|
1910
|
+
}
|
|
1911
|
+
const runtime = await createRuntimeService(options);
|
|
1912
|
+
const namespaces = Array.from(
|
|
1913
|
+
new Set(
|
|
1914
|
+
Array.from(runtime.graph.entries.values()).map((entry) => entry.namespace).filter((entry) => entry !== "meta" && entry !== "env" && entry !== "public")
|
|
1915
|
+
)
|
|
1916
|
+
).sort((left, right) => left.localeCompare(right));
|
|
1917
|
+
const stored = await Promise.all(namespaces.map((entry) => listStoredNamespace(entry, options)));
|
|
1918
|
+
const meta = await listProjectedNamespace("meta", options);
|
|
1919
|
+
return [...stored.flat(), ...meta].sort((left, right) => left.key.localeCompare(right.key));
|
|
1743
1920
|
}
|
|
1744
1921
|
|
|
1745
1922
|
// src/commands/list.ts
|
|
@@ -1756,13 +1933,16 @@ function normalizeNamespace(value) {
|
|
|
1756
1933
|
if (value === "meta") {
|
|
1757
1934
|
return "meta";
|
|
1758
1935
|
}
|
|
1936
|
+
if (value === "process") {
|
|
1937
|
+
return "process";
|
|
1938
|
+
}
|
|
1759
1939
|
if (value === "env") {
|
|
1760
1940
|
return "env";
|
|
1761
1941
|
}
|
|
1762
1942
|
if (value === "public") {
|
|
1763
1943
|
return "public";
|
|
1764
1944
|
}
|
|
1765
|
-
|
|
1945
|
+
return value;
|
|
1766
1946
|
}
|
|
1767
1947
|
async function runList(args = [], options = {}) {
|
|
1768
1948
|
const cliArgs = [...options.cliArgs ?? []];
|
|
@@ -1784,35 +1964,200 @@ async function runList(args = [], options = {}) {
|
|
|
1784
1964
|
return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
|
|
1785
1965
|
}
|
|
1786
1966
|
|
|
1787
|
-
// src/commands/
|
|
1788
|
-
import
|
|
1789
|
-
import
|
|
1967
|
+
// src/commands/migrate.ts
|
|
1968
|
+
import path8 from "path";
|
|
1969
|
+
import {
|
|
1970
|
+
applyManifestMappings,
|
|
1971
|
+
loadManifest,
|
|
1972
|
+
proposeMapping,
|
|
1973
|
+
rewriteSourceFiles,
|
|
1974
|
+
scanEnvUsage
|
|
1975
|
+
} from "@kitsy/cnos/internal";
|
|
1976
|
+
async function runMigrate(options = {}) {
|
|
1977
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
1978
|
+
const scan = consumeOption(cliArgs, "--scan");
|
|
1979
|
+
const apply = consumeFlag(cliArgs, "--apply");
|
|
1980
|
+
const dryRun = consumeFlag(cliArgs, "--dry-run") || !apply;
|
|
1981
|
+
const rewrite = consumeFlag(cliArgs, "--rewrite");
|
|
1982
|
+
if (cliArgs.length > 0) {
|
|
1983
|
+
throw new Error(`Unknown migrate options: ${cliArgs.join(" ")}`);
|
|
1984
|
+
}
|
|
1985
|
+
const manifest = await loadManifest(options.root ? { root: options.root } : {});
|
|
1986
|
+
const scanRoot = path8.resolve(manifest.repoRoot, scan ?? "src");
|
|
1987
|
+
const usages = await scanEnvUsage(scanRoot);
|
|
1988
|
+
const uniqueProposals = new Map(usages.map((usage) => [usage.envVar, proposeMapping(usage.envVar)]));
|
|
1989
|
+
const proposals = Array.from(uniqueProposals.values()).sort((left, right) => left.envVar.localeCompare(right.envVar));
|
|
1990
|
+
let manifestResult;
|
|
1991
|
+
let rewriteResult;
|
|
1992
|
+
if (apply) {
|
|
1993
|
+
manifestResult = await applyManifestMappings(proposals, options.root);
|
|
1994
|
+
if (rewrite) {
|
|
1995
|
+
rewriteResult = await rewriteSourceFiles(usages.filter((usage) => usage.kind === "process-env"), uniqueProposals);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (options.json) {
|
|
1999
|
+
return printJson({
|
|
2000
|
+
scanRoot,
|
|
2001
|
+
dryRun,
|
|
2002
|
+
apply,
|
|
2003
|
+
rewrite,
|
|
2004
|
+
usages,
|
|
2005
|
+
proposals,
|
|
2006
|
+
...manifestResult ? { manifest: manifestResult } : {},
|
|
2007
|
+
...rewriteResult ? { rewriteResult } : {}
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
const lines = [
|
|
2011
|
+
`Scanned ${usages.length} env usage${usages.length === 1 ? "" : "s"} in ${scanRoot}`,
|
|
2012
|
+
"",
|
|
2013
|
+
"Proposed mappings:",
|
|
2014
|
+
...proposals.map(
|
|
2015
|
+
(proposal) => ` ${proposal.envVar} -> ${proposal.logicalKey}${proposal.public ? " (promote to public)" : ""}`
|
|
2016
|
+
)
|
|
2017
|
+
];
|
|
2018
|
+
if (proposals.length === 0) {
|
|
2019
|
+
lines.push(" none");
|
|
2020
|
+
}
|
|
2021
|
+
if (dryRun) {
|
|
2022
|
+
lines.push("", "Dry run only. Re-run with --apply to update the manifest.");
|
|
2023
|
+
}
|
|
2024
|
+
if (manifestResult) {
|
|
2025
|
+
lines.push(
|
|
2026
|
+
"",
|
|
2027
|
+
`Updated ${manifestResult.manifestPath} with ${manifestResult.appliedMappings} env mapping${manifestResult.appliedMappings === 1 ? "" : "s"} and ${manifestResult.appliedPromotions} public promotion${manifestResult.appliedPromotions === 1 ? "" : "s"}.`
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
if (rewrite) {
|
|
2031
|
+
if (!apply) {
|
|
2032
|
+
lines.push("", "Source rewrite requested but skipped because --apply was not set.");
|
|
2033
|
+
} else if (rewriteResult) {
|
|
2034
|
+
lines.push(
|
|
2035
|
+
"",
|
|
2036
|
+
`Rewrote ${rewriteResult.rewrittenFiles.length} file${rewriteResult.rewrittenFiles.length === 1 ? "" : "s"} and created ${rewriteResult.backupFiles.length} backup${rewriteResult.backupFiles.length === 1 ? "" : "s"}.`
|
|
2037
|
+
);
|
|
2038
|
+
if (rewriteResult.skippedUsages.length > 0) {
|
|
2039
|
+
lines.push("Skipped usages:");
|
|
2040
|
+
lines.push(...rewriteResult.skippedUsages.map((entry) => ` ${entry}`));
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
return lines.join("\n");
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// src/commands/namespace.ts
|
|
2048
|
+
import path9 from "path";
|
|
2049
|
+
function normalizeCommand2(args) {
|
|
2050
|
+
const [actionOrPath, ...tail] = args;
|
|
2051
|
+
if (!actionOrPath) {
|
|
2052
|
+
return {
|
|
2053
|
+
action: "list",
|
|
2054
|
+
tail: []
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
if (["get", "set", "create", "add", "list", "delete", "remove"].includes(actionOrPath)) {
|
|
2058
|
+
return {
|
|
2059
|
+
action: actionOrPath === "remove" ? "delete" : actionOrPath === "create" || actionOrPath === "add" ? "set" : actionOrPath,
|
|
2060
|
+
tail
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
return {
|
|
2064
|
+
action: "get",
|
|
2065
|
+
tail: args
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
async function runNamespace(namespace, args = [], options = {}) {
|
|
2069
|
+
const { action, tail } = normalizeCommand2(args);
|
|
2070
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
2071
|
+
const root = path9.resolve(options.root ?? process.cwd());
|
|
2072
|
+
if (action === "list") {
|
|
2073
|
+
const prefix = consumeOption(cliArgs, "--prefix");
|
|
2074
|
+
const entries = await listConfigEntries(namespace, {
|
|
2075
|
+
...options,
|
|
2076
|
+
cliArgs,
|
|
2077
|
+
...prefix ? { prefix } : {}
|
|
2078
|
+
});
|
|
2079
|
+
if (options.json) {
|
|
2080
|
+
return printJson(entries);
|
|
2081
|
+
}
|
|
2082
|
+
return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
|
|
2083
|
+
}
|
|
2084
|
+
if (action === "set") {
|
|
2085
|
+
const configPath2 = tail[0] ?? "app.name";
|
|
2086
|
+
const rawValue = tail[1] ?? "";
|
|
2087
|
+
const target = consumeOption(cliArgs, "--target") ?? "local";
|
|
2088
|
+
const result = await defineValue(namespace, configPath2, rawValue, {
|
|
2089
|
+
...options,
|
|
2090
|
+
cliArgs,
|
|
2091
|
+
target
|
|
2092
|
+
});
|
|
2093
|
+
if (options.json) {
|
|
2094
|
+
return printJson({
|
|
2095
|
+
namespace,
|
|
2096
|
+
path: configPath2,
|
|
2097
|
+
target,
|
|
2098
|
+
filePath: result.filePath,
|
|
2099
|
+
value: result.value
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
return `set ${namespace}.${configPath2} in ${displayPath(result.filePath, root)}`;
|
|
2103
|
+
}
|
|
2104
|
+
if (action === "delete") {
|
|
2105
|
+
const configPath2 = tail[0] ?? "app.name";
|
|
2106
|
+
const target = consumeOption(cliArgs, "--target") ?? "local";
|
|
2107
|
+
const result = await deleteValue(namespace, configPath2, {
|
|
2108
|
+
...options,
|
|
2109
|
+
cliArgs,
|
|
2110
|
+
target
|
|
2111
|
+
});
|
|
2112
|
+
if (options.json) {
|
|
2113
|
+
return printJson(result);
|
|
2114
|
+
}
|
|
2115
|
+
return result.deleted ? `deleted ${namespace}.${configPath2} from ${displayPath(result.filePath, root)}` : `no ${namespace}.${configPath2} found in ${displayPath(result.filePath, root)}`;
|
|
2116
|
+
}
|
|
2117
|
+
const runtime = await createRuntimeService(options);
|
|
2118
|
+
const configPath = tail[0] ?? "app.name";
|
|
2119
|
+
const value = runtime.read(`${namespace}.${configPath}`);
|
|
2120
|
+
if (value === void 0) {
|
|
2121
|
+
throw new Error(`Missing CNOS ${namespace} path: ${configPath}`);
|
|
2122
|
+
}
|
|
2123
|
+
if (options.json) {
|
|
2124
|
+
return printJson({
|
|
2125
|
+
key: `${namespace}.${configPath}`,
|
|
2126
|
+
value
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
return printValue(value);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// src/commands/onboard.ts
|
|
2133
|
+
import { copyFile, readdir as readdir2, rm } from "fs/promises";
|
|
2134
|
+
import path10 from "path";
|
|
1790
2135
|
var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
|
|
1791
2136
|
async function listRootEnvFiles(root) {
|
|
1792
|
-
const entries = await
|
|
2137
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
1793
2138
|
return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
1794
2139
|
}
|
|
1795
2140
|
async function runOnboard(options = {}) {
|
|
1796
|
-
const root =
|
|
1797
|
-
const workspace = options.workspace ??
|
|
2141
|
+
const root = path10.resolve(options.root ?? process.cwd());
|
|
2142
|
+
const workspace = options.workspace ?? path10.basename(root);
|
|
1798
2143
|
const cliArgs = [...options.cliArgs ?? []];
|
|
1799
2144
|
const move = consumeFlag(cliArgs, "--move");
|
|
1800
2145
|
if (cliArgs.length > 0) {
|
|
1801
2146
|
throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
|
|
1802
2147
|
}
|
|
1803
2148
|
const scaffold = await scaffoldWorkspace(root, workspace);
|
|
1804
|
-
const envRoot =
|
|
2149
|
+
const envRoot = path10.join(root, ".cnos", "workspaces", workspace, "env");
|
|
1805
2150
|
const rootFiles = await listRootEnvFiles(root);
|
|
1806
2151
|
const imported = [];
|
|
1807
2152
|
const skipped = [];
|
|
1808
2153
|
for (const fileName of rootFiles) {
|
|
1809
|
-
const sourcePath =
|
|
1810
|
-
const targetPath =
|
|
2154
|
+
const sourcePath = path10.join(root, fileName);
|
|
2155
|
+
const targetPath = path10.join(envRoot, fileName);
|
|
1811
2156
|
try {
|
|
1812
2157
|
await copyFile(sourcePath, targetPath);
|
|
1813
|
-
imported.push(
|
|
2158
|
+
imported.push(path10.relative(root, targetPath).replace(/\\/g, "/"));
|
|
1814
2159
|
if (move) {
|
|
1815
|
-
await
|
|
2160
|
+
await rm(sourcePath);
|
|
1816
2161
|
}
|
|
1817
2162
|
} catch {
|
|
1818
2163
|
skipped.push(fileName);
|
|
@@ -1835,17 +2180,17 @@ async function runOnboard(options = {}) {
|
|
|
1835
2180
|
}
|
|
1836
2181
|
|
|
1837
2182
|
// src/commands/profile.ts
|
|
1838
|
-
import
|
|
2183
|
+
import path13 from "path";
|
|
1839
2184
|
|
|
1840
2185
|
// src/services/context.ts
|
|
1841
|
-
import { readFile as
|
|
1842
|
-
import
|
|
1843
|
-
import { parseYaml as
|
|
2186
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
2187
|
+
import path11 from "path";
|
|
2188
|
+
import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml2 } from "@kitsy/cnos/internal";
|
|
1844
2189
|
async function loadCliContext(root = process.cwd()) {
|
|
1845
|
-
const filePath =
|
|
2190
|
+
const filePath = path11.join(path11.resolve(root), ".cnos-workspace.yml");
|
|
1846
2191
|
try {
|
|
1847
|
-
const source = await
|
|
1848
|
-
const parsed =
|
|
2192
|
+
const source = await readFile4(filePath, "utf8");
|
|
2193
|
+
const parsed = parseYaml3(source);
|
|
1849
2194
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1850
2195
|
return {};
|
|
1851
2196
|
}
|
|
@@ -1855,8 +2200,8 @@ async function loadCliContext(root = process.cwd()) {
|
|
|
1855
2200
|
}
|
|
1856
2201
|
}
|
|
1857
2202
|
async function saveCliContext(options = {}) {
|
|
1858
|
-
const root =
|
|
1859
|
-
const filePath =
|
|
2203
|
+
const root = path11.resolve(options.root ?? process.cwd());
|
|
2204
|
+
const filePath = path11.join(root, ".cnos-workspace.yml");
|
|
1860
2205
|
const current = await loadCliContext(root);
|
|
1861
2206
|
const next = {
|
|
1862
2207
|
...current.workspace ? { workspace: current.workspace } : {},
|
|
@@ -1866,7 +2211,7 @@ async function saveCliContext(options = {}) {
|
|
|
1866
2211
|
...options.profile ? { profile: options.profile } : {},
|
|
1867
2212
|
...options.globalRoot ? { globalRoot: options.globalRoot } : {}
|
|
1868
2213
|
};
|
|
1869
|
-
await
|
|
2214
|
+
await writeFile4(filePath, stringifyYaml2(next), "utf8");
|
|
1870
2215
|
return {
|
|
1871
2216
|
filePath,
|
|
1872
2217
|
context: next
|
|
@@ -1874,29 +2219,37 @@ async function saveCliContext(options = {}) {
|
|
|
1874
2219
|
}
|
|
1875
2220
|
|
|
1876
2221
|
// src/services/profiles.ts
|
|
1877
|
-
import { mkdir as mkdir4, readdir as
|
|
1878
|
-
import
|
|
1879
|
-
import { parseYaml as
|
|
1880
|
-
async function createProfileDefinition(root = process.cwd(), profile, inherit) {
|
|
1881
|
-
const filePath =
|
|
1882
|
-
await mkdir4(
|
|
1883
|
-
const document =
|
|
2222
|
+
import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
2223
|
+
import path12 from "path";
|
|
2224
|
+
import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
|
|
2225
|
+
async function createProfileDefinition(root = process.cwd(), profile, inherit, options = {}) {
|
|
2226
|
+
const filePath = path12.join(path12.resolve(root), ".cnos", "profiles", `${profile}.yml`);
|
|
2227
|
+
await mkdir4(path12.dirname(filePath), { recursive: true });
|
|
2228
|
+
const document = options.noInherit ? {
|
|
2229
|
+
name: profile,
|
|
2230
|
+
activate: {
|
|
2231
|
+
values: [`profiles/${profile}/values`, `values/${profile}`],
|
|
2232
|
+
secrets: [`profiles/${profile}/secrets`, `secrets/${profile}`],
|
|
2233
|
+
envFiles: [`.env.${profile}`]
|
|
2234
|
+
}
|
|
2235
|
+
} : inherit && inherit !== "base" ? {
|
|
1884
2236
|
name: profile,
|
|
1885
2237
|
extends: [inherit]
|
|
1886
2238
|
} : {
|
|
1887
2239
|
name: profile
|
|
1888
2240
|
};
|
|
1889
|
-
await
|
|
2241
|
+
await writeFile5(filePath, stringifyYaml3(document), "utf8");
|
|
1890
2242
|
return {
|
|
1891
2243
|
filePath,
|
|
1892
2244
|
profile,
|
|
1893
|
-
...inherit ? { inherit } : {}
|
|
2245
|
+
...inherit ? { inherit } : {},
|
|
2246
|
+
...options.noInherit ? { noInherit: true } : {}
|
|
1894
2247
|
};
|
|
1895
2248
|
}
|
|
1896
2249
|
async function listProfiles(root = process.cwd()) {
|
|
1897
|
-
const profilesRoot =
|
|
2250
|
+
const profilesRoot = path12.join(path12.resolve(root), ".cnos", "profiles");
|
|
1898
2251
|
try {
|
|
1899
|
-
const entries = await
|
|
2252
|
+
const entries = await readdir3(profilesRoot, { withFileTypes: true });
|
|
1900
2253
|
const discovered = /* @__PURE__ */ new Set(["base"]);
|
|
1901
2254
|
for (const entry of entries) {
|
|
1902
2255
|
if (entry.isFile() && entry.name.endsWith(".yml")) {
|
|
@@ -1909,9 +2262,9 @@ async function listProfiles(root = process.cwd()) {
|
|
|
1909
2262
|
}
|
|
1910
2263
|
}
|
|
1911
2264
|
async function deleteProfileDefinition(root = process.cwd(), profile) {
|
|
1912
|
-
const filePath =
|
|
2265
|
+
const filePath = path12.join(path12.resolve(root), ".cnos", "profiles", `${profile}.yml`);
|
|
1913
2266
|
try {
|
|
1914
|
-
await
|
|
2267
|
+
await rm2(filePath);
|
|
1915
2268
|
return {
|
|
1916
2269
|
filePath,
|
|
1917
2270
|
deleted: true
|
|
@@ -1929,9 +2282,9 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
|
|
|
1929
2282
|
name: "base"
|
|
1930
2283
|
};
|
|
1931
2284
|
}
|
|
1932
|
-
const filePath =
|
|
2285
|
+
const filePath = path12.join(path12.resolve(root), ".cnos", "profiles", `${profile}.yml`);
|
|
1933
2286
|
try {
|
|
1934
|
-
return
|
|
2287
|
+
return parseYaml4(await readFile5(filePath, "utf8")) ?? void 0;
|
|
1935
2288
|
} catch {
|
|
1936
2289
|
return void 0;
|
|
1937
2290
|
}
|
|
@@ -1953,16 +2306,23 @@ function normalizeProfileAction(args) {
|
|
|
1953
2306
|
}
|
|
1954
2307
|
async function runProfile(args, options = {}) {
|
|
1955
2308
|
const { action, tail } = normalizeProfileAction(args);
|
|
1956
|
-
const root =
|
|
2309
|
+
const root = path13.resolve(options.root ?? process.cwd());
|
|
1957
2310
|
const cliArgs = [...options.cliArgs ?? []];
|
|
1958
2311
|
if (action === "create") {
|
|
1959
2312
|
const profile = tail[0] ?? "stage";
|
|
1960
2313
|
const inherit = consumeOption(cliArgs, "--inherit");
|
|
1961
|
-
const
|
|
2314
|
+
const noInherit = consumeFlag(cliArgs, "--no-inherit");
|
|
2315
|
+
if (inherit && noInherit) {
|
|
2316
|
+
throw new Error("profile create accepts either --inherit <name> or --no-inherit, not both");
|
|
2317
|
+
}
|
|
2318
|
+
const result = await createProfileDefinition(root, profile, inherit, { noInherit });
|
|
1962
2319
|
if (options.json) {
|
|
1963
2320
|
return printJson(result);
|
|
1964
2321
|
}
|
|
1965
|
-
|
|
2322
|
+
if (noInherit) {
|
|
2323
|
+
return `created profile ${profile} at ${displayPath(result.filePath, root)} without inheriting base`;
|
|
2324
|
+
}
|
|
2325
|
+
return `created profile ${profile} at ${displayPath(result.filePath, root)}; inherits values from base by default`;
|
|
1966
2326
|
}
|
|
1967
2327
|
if (action === "use") {
|
|
1968
2328
|
const profile = tail[0] ?? "base";
|
|
@@ -1973,7 +2333,7 @@ async function runProfile(args, options = {}) {
|
|
|
1973
2333
|
if (options.json) {
|
|
1974
2334
|
return printJson(result);
|
|
1975
2335
|
}
|
|
1976
|
-
return `active profile set to ${profile} in ${result.filePath}`;
|
|
2336
|
+
return `active profile set to ${profile} in ${displayPath(result.filePath, root)}`;
|
|
1977
2337
|
}
|
|
1978
2338
|
if (action === "delete") {
|
|
1979
2339
|
const profile = tail[0] ?? "base";
|
|
@@ -1997,11 +2357,12 @@ async function runProfile(args, options = {}) {
|
|
|
1997
2357
|
}
|
|
1998
2358
|
|
|
1999
2359
|
// src/commands/promote.ts
|
|
2000
|
-
import
|
|
2360
|
+
import path14 from "path";
|
|
2361
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
2001
2362
|
import {
|
|
2002
2363
|
ensureProjectionAllowed,
|
|
2003
2364
|
loadManifest as loadManifest2,
|
|
2004
|
-
stringifyYaml as
|
|
2365
|
+
stringifyYaml as stringifyYaml4
|
|
2005
2366
|
} from "@kitsy/cnos/internal";
|
|
2006
2367
|
function normalizeTarget(value) {
|
|
2007
2368
|
if (value === "public" || value === "env") {
|
|
@@ -2013,6 +2374,7 @@ function sortRecord(record) {
|
|
|
2013
2374
|
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
2014
2375
|
}
|
|
2015
2376
|
async function runPromote(args = [], options = {}) {
|
|
2377
|
+
const root = path14.resolve(options.root ?? process.cwd());
|
|
2016
2378
|
const cliArgs = [...options.cliArgs ?? []];
|
|
2017
2379
|
const target = normalizeTarget(consumeOption(cliArgs, "--to"));
|
|
2018
2380
|
const alias = consumeOption(cliArgs, "--as");
|
|
@@ -2051,7 +2413,7 @@ async function runPromote(args = [], options = {}) {
|
|
|
2051
2413
|
})
|
|
2052
2414
|
};
|
|
2053
2415
|
}
|
|
2054
|
-
await
|
|
2416
|
+
await writeFile6(loadedManifest.manifestPath, stringifyYaml4(rawManifest), "utf8");
|
|
2055
2417
|
if (options.json) {
|
|
2056
2418
|
return printJson({
|
|
2057
2419
|
target,
|
|
@@ -2060,7 +2422,7 @@ async function runPromote(args = [], options = {}) {
|
|
|
2060
2422
|
manifestPath: loadedManifest.manifestPath
|
|
2061
2423
|
});
|
|
2062
2424
|
}
|
|
2063
|
-
return target === "public" ? `promoted ${keys.join(", ")} to public in ${loadedManifest.manifestPath}` : `promoted ${keys[0]} to env as ${alias} in ${loadedManifest.manifestPath}`;
|
|
2425
|
+
return target === "public" ? `promoted ${keys.join(", ")} to public in ${displayPath(loadedManifest.manifestPath, root)}` : `promoted ${keys[0]} to env as ${alias} in ${displayPath(loadedManifest.manifestPath, root)}`;
|
|
2064
2426
|
}
|
|
2065
2427
|
|
|
2066
2428
|
// src/commands/read.ts
|
|
@@ -2070,16 +2432,23 @@ async function runRead(key, options = {}) {
|
|
|
2070
2432
|
if (value === void 0) {
|
|
2071
2433
|
throw new Error(`Missing CNOS config key: ${key}`);
|
|
2072
2434
|
}
|
|
2435
|
+
const isSecret = key.startsWith("secret.");
|
|
2436
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
2437
|
+
const reveal = consumeFlag(cliArgs, "--reveal");
|
|
2438
|
+
const valueForOutput = isSecret && !reveal ? maskSecretValue(value) : value;
|
|
2073
2439
|
if (options.json) {
|
|
2074
|
-
return printJson({ key, value });
|
|
2440
|
+
return printJson({ key, value: valueForOutput });
|
|
2075
2441
|
}
|
|
2076
|
-
return printValue(
|
|
2442
|
+
return printValue(valueForOutput);
|
|
2077
2443
|
}
|
|
2078
2444
|
|
|
2079
2445
|
// src/commands/run.ts
|
|
2080
2446
|
import { spawn } from "child_process";
|
|
2081
2447
|
import {
|
|
2082
2448
|
CNOS_GRAPH_ENV_VAR,
|
|
2449
|
+
CNOS_SECRET_PAYLOAD_ENV_VAR,
|
|
2450
|
+
CNOS_SESSION_KEY_ENV_VAR,
|
|
2451
|
+
serializeSecretPayload,
|
|
2083
2452
|
serializeRuntimeGraph
|
|
2084
2453
|
} from "@kitsy/cnos/internal";
|
|
2085
2454
|
function consumeOptions(args, flag) {
|
|
@@ -2118,6 +2487,7 @@ async function runCommand(command, options = {}) {
|
|
|
2118
2487
|
}
|
|
2119
2488
|
const cliArgs = [...options.cliArgs ?? []];
|
|
2120
2489
|
const isPublic = consumeFlag(cliArgs, "--public");
|
|
2490
|
+
const isAuthenticated = consumeFlag(cliArgs, "--auth");
|
|
2121
2491
|
const framework = consumeOption(cliArgs, "--framework");
|
|
2122
2492
|
const prefix = consumeOption(cliArgs, "--prefix");
|
|
2123
2493
|
const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
|
|
@@ -2125,13 +2495,21 @@ async function runCommand(command, options = {}) {
|
|
|
2125
2495
|
...options,
|
|
2126
2496
|
cliArgs: [...cliArgs, ...setOverrides]
|
|
2127
2497
|
});
|
|
2498
|
+
const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
|
|
2499
|
+
Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
|
|
2500
|
+
) : void 0;
|
|
2501
|
+
const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
|
|
2128
2502
|
const env = {
|
|
2129
2503
|
...process.env,
|
|
2130
2504
|
...isPublic ? runtime.toPublicEnv({
|
|
2131
2505
|
...framework ? { framework } : {},
|
|
2132
2506
|
...prefix ? { prefix } : {}
|
|
2133
2507
|
}) : runtime.toEnv(),
|
|
2134
|
-
[CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph)
|
|
2508
|
+
[CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph),
|
|
2509
|
+
...secretPayload ? {
|
|
2510
|
+
[CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
|
|
2511
|
+
[CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
|
|
2512
|
+
} : {}
|
|
2135
2513
|
};
|
|
2136
2514
|
return new Promise((resolve, reject) => {
|
|
2137
2515
|
const executable = command[0];
|
|
@@ -2166,12 +2544,203 @@ async function runCommand(command, options = {}) {
|
|
|
2166
2544
|
});
|
|
2167
2545
|
}
|
|
2168
2546
|
|
|
2547
|
+
// src/commands/secret.ts
|
|
2548
|
+
import path17 from "path";
|
|
2549
|
+
|
|
2550
|
+
// src/commands/vault.ts
|
|
2551
|
+
import path16 from "path";
|
|
2552
|
+
|
|
2553
|
+
// src/services/vaults.ts
|
|
2554
|
+
import { rm as rm3, writeFile as writeFile7 } from "fs/promises";
|
|
2555
|
+
import path15 from "path";
|
|
2556
|
+
import {
|
|
2557
|
+
clearAllVaultSessionKeys,
|
|
2558
|
+
clearVaultSessionKey,
|
|
2559
|
+
createSecretVault,
|
|
2560
|
+
deriveVaultKey,
|
|
2561
|
+
listLocalSecrets,
|
|
2562
|
+
loadManifest as loadManifest3,
|
|
2563
|
+
listSecretVaults,
|
|
2564
|
+
readVaultMetadata,
|
|
2565
|
+
resolveSecretStoreRoot as resolveSecretStoreRoot2,
|
|
2566
|
+
resolveVaultAuth as resolveVaultAuth2,
|
|
2567
|
+
resolveVaultDefinition,
|
|
2568
|
+
stringifyYaml as stringifyYaml5,
|
|
2569
|
+
writeKeychain,
|
|
2570
|
+
writeVaultSessionKey
|
|
2571
|
+
} from "@kitsy/cnos/internal";
|
|
2572
|
+
function buildVaultDefinition(vault, provider) {
|
|
2573
|
+
return provider === "local" ? {
|
|
2574
|
+
provider: "local",
|
|
2575
|
+
auth: {
|
|
2576
|
+
passphrase: {
|
|
2577
|
+
from: defaultLocalAuthSources(vault)
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
} : {
|
|
2581
|
+
provider,
|
|
2582
|
+
auth: {
|
|
2583
|
+
method: "environment"
|
|
2584
|
+
}
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
function sortVaults(vaults) {
|
|
2588
|
+
return Object.fromEntries(Object.entries(vaults).sort(([left], [right]) => left.localeCompare(right)));
|
|
2589
|
+
}
|
|
2590
|
+
function defaultLocalAuthSources(vault) {
|
|
2591
|
+
const token = vault.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
2592
|
+
return [`env:CNOS_SECRET_PASSPHRASE_${token}`, "env:CNOS_SECRET_PASSPHRASE", `keychain:cnos/${vault}`, "prompt"];
|
|
2593
|
+
}
|
|
2594
|
+
async function createVaultDefinition(name, options = {}) {
|
|
2595
|
+
const vault = name.trim() || "default";
|
|
2596
|
+
const provider = options.provider?.trim() || "local";
|
|
2597
|
+
if (provider === "local" && (options.noPassphrase ?? false)) {
|
|
2598
|
+
throw new Error("Local vaults cannot be passwordless.");
|
|
2599
|
+
}
|
|
2600
|
+
const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
|
|
2601
|
+
const vaultDefinition = buildVaultDefinition(vault, provider);
|
|
2602
|
+
const rawManifest = {
|
|
2603
|
+
...loadedManifest.rawManifest,
|
|
2604
|
+
vaults: {
|
|
2605
|
+
...loadedManifest.rawManifest.vaults ?? {},
|
|
2606
|
+
[vault]: vaultDefinition
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
|
|
2610
|
+
const definition = resolveVaultDefinition({ [vault]: vaultDefinition }, vault);
|
|
2611
|
+
if (provider === "local") {
|
|
2612
|
+
const auth = await resolveVaultAuth2(vault, vaultDefinition, options.processEnv ?? process.env);
|
|
2613
|
+
if (!auth.passphrase) {
|
|
2614
|
+
throw new Error(`Vault "${vault}" requires passphrase-based authentication during creation.`);
|
|
2615
|
+
}
|
|
2616
|
+
const storeRoot = resolveSecretStoreRoot2(options.processEnv);
|
|
2617
|
+
const existing = await readVaultMetadata(storeRoot, vault);
|
|
2618
|
+
if (!existing) {
|
|
2619
|
+
await createSecretVault(storeRoot, vault, auth.passphrase);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return {
|
|
2623
|
+
...definition,
|
|
2624
|
+
authMethod: definition.auth?.method ?? (provider === "local" ? "passphrase" : "environment"),
|
|
2625
|
+
localStore: provider === "local",
|
|
2626
|
+
manifestPath: loadedManifest.manifestPath
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
async function listVaultDefinitions(options = {}) {
|
|
2630
|
+
const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
|
|
2631
|
+
const localStoreVaults = await listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
|
|
2632
|
+
return Object.keys(loadedManifest.manifest.vaults).sort((left, right) => left.localeCompare(right)).map((name) => {
|
|
2633
|
+
const definition = resolveVaultDefinition(loadedManifest.manifest.vaults, name);
|
|
2634
|
+
return {
|
|
2635
|
+
...definition,
|
|
2636
|
+
authMethod: definition.auth?.method ?? (definition.provider === "local" ? "passphrase" : "environment"),
|
|
2637
|
+
localStore: localStoreVaults.includes(name)
|
|
2638
|
+
};
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
async function removeVaultDefinition(name, options = {}) {
|
|
2642
|
+
const vault = name.trim() || "default";
|
|
2643
|
+
const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
|
|
2644
|
+
if (!loadedManifest.rawManifest.vaults?.[vault]) {
|
|
2645
|
+
return {
|
|
2646
|
+
name: vault,
|
|
2647
|
+
deleted: false,
|
|
2648
|
+
manifestPath: loadedManifest.manifestPath
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
const nextVaults = { ...loadedManifest.rawManifest.vaults ?? {} };
|
|
2652
|
+
delete nextVaults[vault];
|
|
2653
|
+
const rawManifest = {
|
|
2654
|
+
...loadedManifest.rawManifest,
|
|
2655
|
+
...Object.keys(nextVaults).length > 0 ? { vaults: sortVaults(nextVaults) } : {}
|
|
2656
|
+
};
|
|
2657
|
+
if (Object.keys(nextVaults).length === 0) {
|
|
2658
|
+
delete rawManifest.vaults;
|
|
2659
|
+
}
|
|
2660
|
+
await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
|
|
2661
|
+
const vaultRoot = path15.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
|
|
2662
|
+
let removedStore;
|
|
2663
|
+
try {
|
|
2664
|
+
await rm3(vaultRoot, { recursive: true, force: true });
|
|
2665
|
+
removedStore = vaultRoot;
|
|
2666
|
+
} catch {
|
|
2667
|
+
removedStore = void 0;
|
|
2668
|
+
}
|
|
2669
|
+
await clearVaultSessionKey(vault, options.processEnv);
|
|
2670
|
+
return {
|
|
2671
|
+
name: vault,
|
|
2672
|
+
deleted: true,
|
|
2673
|
+
manifestPath: loadedManifest.manifestPath,
|
|
2674
|
+
...removedStore ? { removedStore } : {}
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
async function listLocalStoreVaults(options = {}) {
|
|
2678
|
+
return listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
|
|
2679
|
+
}
|
|
2680
|
+
async function authenticateVault(name, options = {}) {
|
|
2681
|
+
const vault = name.trim() || "default";
|
|
2682
|
+
const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
|
|
2683
|
+
const definition = loadedManifest.manifest.vaults[vault];
|
|
2684
|
+
if (!definition) {
|
|
2685
|
+
throw new Error(`Unknown vault "${vault}"`);
|
|
2686
|
+
}
|
|
2687
|
+
const auth = await resolveVaultAuth2(vault, definition, options.processEnv ?? process.env);
|
|
2688
|
+
const storeRoot = resolveSecretStoreRoot2(options.processEnv);
|
|
2689
|
+
if (definition.provider === "local") {
|
|
2690
|
+
if (!auth.passphrase) {
|
|
2691
|
+
throw new Error(`Vault "${vault}" requires passphrase-based authentication.`);
|
|
2692
|
+
}
|
|
2693
|
+
const metadata = await readVaultMetadata(storeRoot, vault);
|
|
2694
|
+
if (!metadata) {
|
|
2695
|
+
throw new Error(
|
|
2696
|
+
`Vault "${vault}" has not been initialized yet. Run cnos vault create ${vault} first.`
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
const derivedKey = deriveVaultKey(auth.passphrase, Buffer.from(metadata.salt, "base64"), metadata.iterations);
|
|
2700
|
+
await listLocalSecrets(
|
|
2701
|
+
storeRoot,
|
|
2702
|
+
{
|
|
2703
|
+
derivedKey,
|
|
2704
|
+
method: auth.method,
|
|
2705
|
+
...definition.auth?.config ? { config: definition.auth.config } : {}
|
|
2706
|
+
},
|
|
2707
|
+
vault
|
|
2708
|
+
);
|
|
2709
|
+
const sessionPath2 = await writeVaultSessionKey(vault, derivedKey, options.processEnv);
|
|
2710
|
+
if (options.storeKeychain) {
|
|
2711
|
+
await writeKeychain(`cnos/${vault}`, derivedKey.toString("hex"));
|
|
2712
|
+
}
|
|
2713
|
+
return {
|
|
2714
|
+
name: vault,
|
|
2715
|
+
method: auth.method,
|
|
2716
|
+
storedInKeychain: options.storeKeychain ?? false,
|
|
2717
|
+
sessionPath: sessionPath2
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
const sessionPath = await writeVaultSessionKey(vault, Buffer.from(vault, "utf8"), options.processEnv);
|
|
2721
|
+
return {
|
|
2722
|
+
name: vault,
|
|
2723
|
+
method: auth.method,
|
|
2724
|
+
storedInKeychain: false,
|
|
2725
|
+
sessionPath
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
async function logoutVault(name, options = {}) {
|
|
2729
|
+
if (options.all) {
|
|
2730
|
+
await clearAllVaultSessionKeys(options.processEnv);
|
|
2731
|
+
return { scope: "all" };
|
|
2732
|
+
}
|
|
2733
|
+
const vault = name?.trim() || "default";
|
|
2734
|
+
await clearVaultSessionKey(vault, options.processEnv);
|
|
2735
|
+
return { scope: vault };
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2169
2738
|
// src/commands/vault.ts
|
|
2170
2739
|
function normalizeVaultAction(args) {
|
|
2171
2740
|
const [action = "list", ...tail] = args;
|
|
2172
|
-
if (["create", "add", "list", "delete", "remove"].includes(action)) {
|
|
2741
|
+
if (["create", "add", "list", "delete", "remove", "auth", "logout"].includes(action)) {
|
|
2173
2742
|
return {
|
|
2174
|
-
action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : "list",
|
|
2743
|
+
action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : action === "auth" ? "auth" : action === "logout" ? "logout" : "list",
|
|
2175
2744
|
tail
|
|
2176
2745
|
};
|
|
2177
2746
|
}
|
|
@@ -2183,22 +2752,46 @@ function normalizeVaultAction(args) {
|
|
|
2183
2752
|
async function runVault(args = [], options = {}) {
|
|
2184
2753
|
const { action, tail } = normalizeVaultAction(args);
|
|
2185
2754
|
const cliArgs = [...options.cliArgs ?? []];
|
|
2755
|
+
const root = path16.resolve(options.root ?? process.cwd());
|
|
2756
|
+
if (consumeOption(cliArgs, "--passphrase")) {
|
|
2757
|
+
throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
|
|
2758
|
+
}
|
|
2186
2759
|
if (action === "create") {
|
|
2187
2760
|
const name = tail[0] ?? "default";
|
|
2188
2761
|
const provider = consumeOption(cliArgs, "--provider") ?? "local";
|
|
2189
|
-
const passphrase = consumeOption(cliArgs, "--passphrase");
|
|
2190
2762
|
const noPassphrase = consumeFlag(cliArgs, "--no-passphrase");
|
|
2191
2763
|
const result = await createVaultDefinition(name, {
|
|
2192
2764
|
...options,
|
|
2193
2765
|
cliArgs,
|
|
2194
2766
|
provider,
|
|
2195
|
-
...passphrase ? { passphrase } : {},
|
|
2196
2767
|
...noPassphrase ? { noPassphrase: true } : {}
|
|
2197
2768
|
});
|
|
2198
2769
|
if (options.json) {
|
|
2199
2770
|
return printJson(result);
|
|
2200
2771
|
}
|
|
2201
|
-
return `created vault "${result.name}" with provider "${result.provider}" in ${result.manifestPath}`;
|
|
2772
|
+
return `created vault "${result.name}" with provider "${result.provider}" in ${displayPath(result.manifestPath, root)}`;
|
|
2773
|
+
}
|
|
2774
|
+
if (action === "auth") {
|
|
2775
|
+
const result = await authenticateVault(tail[0] ?? "default", {
|
|
2776
|
+
...options,
|
|
2777
|
+
cliArgs,
|
|
2778
|
+
storeKeychain: consumeFlag(cliArgs, "--store-keychain")
|
|
2779
|
+
});
|
|
2780
|
+
if (options.json) {
|
|
2781
|
+
return printJson(result);
|
|
2782
|
+
}
|
|
2783
|
+
return `authenticated vault "${result.name}" via ${result.method}`;
|
|
2784
|
+
}
|
|
2785
|
+
if (action === "logout") {
|
|
2786
|
+
const result = await logoutVault(tail[0], {
|
|
2787
|
+
...options,
|
|
2788
|
+
cliArgs,
|
|
2789
|
+
all: consumeFlag(cliArgs, "--all")
|
|
2790
|
+
});
|
|
2791
|
+
if (options.json) {
|
|
2792
|
+
return printJson(result);
|
|
2793
|
+
}
|
|
2794
|
+
return result.scope === "all" ? "logged out all vault sessions" : `logged out vault "${result.scope}"`;
|
|
2202
2795
|
}
|
|
2203
2796
|
if (action === "remove") {
|
|
2204
2797
|
const name = tail[0] ?? "default";
|
|
@@ -2224,16 +2817,11 @@ async function runVault(args = [], options = {}) {
|
|
|
2224
2817
|
return "";
|
|
2225
2818
|
}
|
|
2226
2819
|
return manifestVaults.map(
|
|
2227
|
-
(vault) => `${vault.name} provider=${vault.provider}
|
|
2820
|
+
(vault) => `${vault.name} provider=${vault.provider} auth=${vault.authMethod}${localStoreVaults.includes(vault.name) ? " local-store=true" : ""}`
|
|
2228
2821
|
).join("\n");
|
|
2229
2822
|
}
|
|
2230
2823
|
|
|
2231
2824
|
// src/commands/secret.ts
|
|
2232
|
-
function isSecretRef(value) {
|
|
2233
|
-
return Boolean(
|
|
2234
|
-
value && typeof value === "object" && !Array.isArray(value) && typeof value.provider === "string" && typeof value.ref === "string"
|
|
2235
|
-
);
|
|
2236
|
-
}
|
|
2237
2825
|
function normalizeSecretCommand(args) {
|
|
2238
2826
|
const [actionOrPath, next, ...tail] = args;
|
|
2239
2827
|
if (!actionOrPath) {
|
|
@@ -2259,53 +2847,74 @@ function normalizeSecretCommand(args) {
|
|
|
2259
2847
|
tail: args
|
|
2260
2848
|
};
|
|
2261
2849
|
}
|
|
2850
|
+
async function readStdinValue() {
|
|
2851
|
+
const chunks = [];
|
|
2852
|
+
for await (const chunk of process.stdin) {
|
|
2853
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2854
|
+
}
|
|
2855
|
+
return Buffer.concat(chunks).toString("utf8").trimEnd();
|
|
2856
|
+
}
|
|
2262
2857
|
async function runSecret(argsOrPath, options = {}) {
|
|
2263
2858
|
const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
|
|
2264
2859
|
const { action, tail } = normalizeSecretCommand(args);
|
|
2265
2860
|
const cliArgs = [...options.cliArgs ?? []];
|
|
2861
|
+
const root = path17.resolve(options.root ?? process.cwd());
|
|
2862
|
+
if (consumeOption(cliArgs, "--passphrase")) {
|
|
2863
|
+
throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
|
|
2864
|
+
}
|
|
2266
2865
|
if (action === "create-vault") {
|
|
2267
2866
|
return runVault(["create", tail[0] ?? "default"], options);
|
|
2268
2867
|
}
|
|
2269
2868
|
if (action === "list") {
|
|
2869
|
+
const runtime2 = await createRuntimeService(options);
|
|
2270
2870
|
const prefix = consumeOption(cliArgs, "--prefix");
|
|
2271
2871
|
const vault = consumeOption(cliArgs, "--vault");
|
|
2272
2872
|
const provider = consumeOption(cliArgs, "--provider");
|
|
2273
|
-
const entries =
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2873
|
+
const entries = Array.from(runtime2.graph.entries.values()).filter((entry2) => entry2.namespace === "secret").filter((entry2) => !prefix || entry2.key.startsWith(`secret.${prefix}`) || entry2.key.startsWith(prefix)).filter((entry2) => {
|
|
2874
|
+
const secretRef2 = entry2.winner.metadata?.secretRef;
|
|
2875
|
+
if (vault && secretRef2?.vault !== vault) {
|
|
2876
|
+
return false;
|
|
2877
|
+
}
|
|
2878
|
+
if (provider && secretRef2?.provider !== provider) {
|
|
2879
|
+
return false;
|
|
2880
|
+
}
|
|
2881
|
+
return true;
|
|
2882
|
+
}).map((entry2) => {
|
|
2883
|
+
const secretRef2 = entry2.winner.metadata?.secretRef;
|
|
2884
|
+
return {
|
|
2885
|
+
key: entry2.key,
|
|
2886
|
+
vault: secretRef2?.vault ?? "default",
|
|
2887
|
+
provider: secretRef2?.provider ?? "local"
|
|
2888
|
+
};
|
|
2889
|
+
}).sort((left, right) => left.key.localeCompare(right.key));
|
|
2280
2890
|
if (options.json) {
|
|
2281
2891
|
return printJson(entries);
|
|
2282
2892
|
}
|
|
2283
|
-
return entries.map((entry2) => `${entry2.key}
|
|
2893
|
+
return entries.map((entry2) => `${entry2.key} (vault: ${entry2.vault}, provider: ${entry2.provider})`).join("\n");
|
|
2284
2894
|
}
|
|
2285
2895
|
if (action === "set") {
|
|
2286
2896
|
const secretPath2 = tail[0];
|
|
2287
|
-
const rawValue = tail[1] ?? "";
|
|
2288
2897
|
const local = consumeFlag(cliArgs, "--local");
|
|
2289
2898
|
const remote = consumeFlag(cliArgs, "--remote");
|
|
2290
2899
|
const ref = consumeFlag(cliArgs, "--ref");
|
|
2900
|
+
const stdin = consumeFlag(cliArgs, "--stdin");
|
|
2291
2901
|
const target = consumeOption(cliArgs, "--target") ?? "local";
|
|
2292
2902
|
const provider = consumeOption(cliArgs, "--provider");
|
|
2293
|
-
const passphrase = consumeOption(cliArgs, "--passphrase");
|
|
2294
2903
|
const vault = consumeOption(cliArgs, "--vault") ?? "default";
|
|
2295
2904
|
const mode = local ? "local" : remote ? "remote" : ref ? "ref" : void 0;
|
|
2905
|
+
const rawValue = stdin ? await readStdinValue() : tail[1] ?? "";
|
|
2296
2906
|
const result = await setSecret(secretPath2 ?? "app.token", rawValue, {
|
|
2297
2907
|
...options,
|
|
2298
2908
|
cliArgs,
|
|
2299
2909
|
target,
|
|
2300
2910
|
vault,
|
|
2301
2911
|
...mode ? { mode } : {},
|
|
2302
|
-
...provider ? { provider } : {}
|
|
2303
|
-
...passphrase ? { passphrase } : {}
|
|
2912
|
+
...provider ? { provider } : {}
|
|
2304
2913
|
});
|
|
2305
2914
|
if (options.json) {
|
|
2306
2915
|
return printJson(result);
|
|
2307
2916
|
}
|
|
2308
|
-
return result.provider === "local" ? `set secret.${secretPath2} in vault "${result.vault ?? "default"}" with ref "${result.ref}" and repo pointer ${result.filePath}` : `set secret.${secretPath2} via ${result.provider} in ${result.filePath}`;
|
|
2917
|
+
return result.provider === "local" ? `set secret.${secretPath2} in vault "${result.vault ?? "default"}" with ref "${result.ref}" and repo pointer ${displayPath(result.filePath, root)}` : `set secret.${secretPath2} via ${result.provider} in ${displayPath(result.filePath, root)}`;
|
|
2309
2918
|
}
|
|
2310
2919
|
if (action === "delete") {
|
|
2311
2920
|
const secretPath2 = tail[0];
|
|
@@ -2318,11 +2927,12 @@ async function runSecret(argsOrPath, options = {}) {
|
|
|
2318
2927
|
if (options.json) {
|
|
2319
2928
|
return printJson(result);
|
|
2320
2929
|
}
|
|
2321
|
-
return result.deleted ? `deleted secret.${secretPath2} from ${result.filePath}` : `no secret.${secretPath2} found in ${result.filePath}`;
|
|
2930
|
+
return result.deleted ? `deleted secret.${secretPath2} from ${displayPath(result.filePath, root)}` : `no secret.${secretPath2} found in ${displayPath(result.filePath, root)}`;
|
|
2322
2931
|
}
|
|
2323
2932
|
const runtime = await createRuntimeService(options);
|
|
2324
2933
|
const secretPath = tail[0] ?? "app.token";
|
|
2325
2934
|
const expectedVault = consumeOption(cliArgs, "--vault");
|
|
2935
|
+
const reveal = consumeFlag(cliArgs, "--reveal");
|
|
2326
2936
|
const entry = runtime.graph.entries.get(`secret.${secretPath}`);
|
|
2327
2937
|
const secretRef = entry?.winner.metadata?.secretRef;
|
|
2328
2938
|
const value = runtime.secret(secretPath);
|
|
@@ -2332,36 +2942,24 @@ async function runSecret(argsOrPath, options = {}) {
|
|
|
2332
2942
|
if (expectedVault && secretRef?.vault && secretRef.vault !== expectedVault) {
|
|
2333
2943
|
throw new Error(`Secret ${secretPath} belongs to vault "${secretRef.vault}", not "${expectedVault}"`);
|
|
2334
2944
|
}
|
|
2335
|
-
|
|
2336
|
-
if (value.provider === "local") {
|
|
2337
|
-
const vault = value.vault ?? "default";
|
|
2338
|
-
throw new Error(
|
|
2339
|
-
`Secret ${secretPath} is stored in vault "${vault}" as ref "${value.ref}". Provide the correct vault passphrase to resolve it.`
|
|
2340
|
-
);
|
|
2341
|
-
}
|
|
2342
|
-
if (value.provider === "github-secrets") {
|
|
2343
|
-
throw new Error(
|
|
2344
|
-
`Secret ${secretPath} is backed by GitHub secrets via ref "${value.ref}". Set that env var in the current process or CI job to resolve it.`
|
|
2345
|
-
);
|
|
2346
|
-
}
|
|
2347
|
-
throw new Error(`Secret ${secretPath} is stored as a ${value.provider} reference "${value.ref}" and is not resolved.`);
|
|
2348
|
-
}
|
|
2945
|
+
const valueForOutput = reveal ? value : maskSecretValue(value);
|
|
2349
2946
|
if (options.json) {
|
|
2350
2947
|
return printJson({
|
|
2351
2948
|
key: `secret.${secretPath}`,
|
|
2352
|
-
value
|
|
2949
|
+
value: valueForOutput,
|
|
2950
|
+
vault: secretRef?.vault ?? "default"
|
|
2353
2951
|
});
|
|
2354
2952
|
}
|
|
2355
|
-
return printValue(
|
|
2953
|
+
return printValue(valueForOutput);
|
|
2356
2954
|
}
|
|
2357
2955
|
|
|
2358
2956
|
// src/commands/use.ts
|
|
2359
|
-
import
|
|
2957
|
+
import path18 from "path";
|
|
2360
2958
|
async function runUse(args = [], options = {}) {
|
|
2361
|
-
const root =
|
|
2362
|
-
const action = args[0]
|
|
2959
|
+
const root = path18.resolve(options.root ?? process.cwd());
|
|
2960
|
+
const action = args[0];
|
|
2363
2961
|
const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
|
|
2364
|
-
if (action === "show" || !hasUpdates) {
|
|
2962
|
+
if (action === "show" || !action && !hasUpdates) {
|
|
2365
2963
|
const context = await loadCliContext(root);
|
|
2366
2964
|
if (options.json) {
|
|
2367
2965
|
return printJson(context);
|
|
@@ -2377,7 +2975,7 @@ async function runUse(args = [], options = {}) {
|
|
|
2377
2975
|
if (options.json) {
|
|
2378
2976
|
return printJson(result);
|
|
2379
2977
|
}
|
|
2380
|
-
return `updated CLI context in ${result.filePath}`;
|
|
2978
|
+
return `updated CLI context in ${displayPath(result.filePath, root)}`;
|
|
2381
2979
|
}
|
|
2382
2980
|
|
|
2383
2981
|
// src/commands/validate.ts
|
|
@@ -2395,7 +2993,7 @@ async function runValidate(options = {}) {
|
|
|
2395
2993
|
// package.json
|
|
2396
2994
|
var package_default = {
|
|
2397
2995
|
name: "@kitsy/cnos-cli",
|
|
2398
|
-
version: "1.
|
|
2996
|
+
version: "1.4.0",
|
|
2399
2997
|
description: "CLI entry point and developer tooling for CNOS.",
|
|
2400
2998
|
type: "module",
|
|
2401
2999
|
main: "./dist/index.js",
|
|
@@ -2450,6 +3048,7 @@ function runVersion() {
|
|
|
2450
3048
|
}
|
|
2451
3049
|
|
|
2452
3050
|
// src/commands/value.ts
|
|
3051
|
+
import path19 from "path";
|
|
2453
3052
|
function normalizeValueCommand(args) {
|
|
2454
3053
|
const [actionOrPath, ...tail] = args;
|
|
2455
3054
|
if (!actionOrPath) {
|
|
@@ -2473,6 +3072,7 @@ async function runValue(argsOrPath, options = {}) {
|
|
|
2473
3072
|
const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
|
|
2474
3073
|
const { action, tail } = normalizeValueCommand(args);
|
|
2475
3074
|
const cliArgs = [...options.cliArgs ?? []];
|
|
3075
|
+
const root = path19.resolve(options.root ?? process.cwd());
|
|
2476
3076
|
if (action === "list") {
|
|
2477
3077
|
const prefix = consumeOption(cliArgs, "--prefix");
|
|
2478
3078
|
const entries = await listConfigEntries("value", {
|
|
@@ -2503,7 +3103,7 @@ async function runValue(argsOrPath, options = {}) {
|
|
|
2503
3103
|
value: result.value
|
|
2504
3104
|
});
|
|
2505
3105
|
}
|
|
2506
|
-
return `set value.${valuePath} in ${result.filePath}`;
|
|
3106
|
+
return `set value.${valuePath} in ${displayPath(result.filePath, root)}`;
|
|
2507
3107
|
}
|
|
2508
3108
|
if (action === "delete") {
|
|
2509
3109
|
const valuePath = tail[0] ?? "app.name";
|
|
@@ -2516,7 +3116,7 @@ async function runValue(argsOrPath, options = {}) {
|
|
|
2516
3116
|
if (options.json) {
|
|
2517
3117
|
return printJson(result);
|
|
2518
3118
|
}
|
|
2519
|
-
return result.deleted ? `deleted value.${valuePath} from ${result.filePath}` : `no value.${valuePath} found in ${result.filePath}`;
|
|
3119
|
+
return result.deleted ? `deleted value.${valuePath} from ${displayPath(result.filePath, root)}` : `no value.${valuePath} found in ${displayPath(result.filePath, root)}`;
|
|
2520
3120
|
}
|
|
2521
3121
|
const runtime = await createRuntimeService(options);
|
|
2522
3122
|
const value = runtime.value(tail[0] ?? "app.name");
|
|
@@ -2532,6 +3132,180 @@ async function runValue(argsOrPath, options = {}) {
|
|
|
2532
3132
|
return printValue(value);
|
|
2533
3133
|
}
|
|
2534
3134
|
|
|
3135
|
+
// src/commands/watch.ts
|
|
3136
|
+
import { watch } from "fs";
|
|
3137
|
+
import { spawn as spawn2 } from "child_process";
|
|
3138
|
+
import {
|
|
3139
|
+
CNOS_GRAPH_ENV_VAR as CNOS_GRAPH_ENV_VAR2,
|
|
3140
|
+
CNOS_SECRET_PAYLOAD_ENV_VAR as CNOS_SECRET_PAYLOAD_ENV_VAR2,
|
|
3141
|
+
CNOS_SESSION_KEY_ENV_VAR as CNOS_SESSION_KEY_ENV_VAR2,
|
|
3142
|
+
diffGraphs,
|
|
3143
|
+
serializeRuntimeGraph as serializeRuntimeGraph2,
|
|
3144
|
+
serializeSecretPayload as serializeSecretPayload2,
|
|
3145
|
+
watchFiles
|
|
3146
|
+
} from "@kitsy/cnos/internal";
|
|
3147
|
+
async function buildRunEnvironment(options) {
|
|
3148
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
3149
|
+
const isPublic = consumeFlag(cliArgs, "--public");
|
|
3150
|
+
const isAuthenticated = consumeFlag(cliArgs, "--auth");
|
|
3151
|
+
const framework = consumeOption(cliArgs, "--framework");
|
|
3152
|
+
const prefix = consumeOption(cliArgs, "--prefix");
|
|
3153
|
+
const runtime = await createRuntimeService({
|
|
3154
|
+
...options,
|
|
3155
|
+
cliArgs
|
|
3156
|
+
});
|
|
3157
|
+
const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
|
|
3158
|
+
Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
|
|
3159
|
+
) : void 0;
|
|
3160
|
+
const secretPayload = authenticatedSecrets ? serializeSecretPayload2(authenticatedSecrets) : void 0;
|
|
3161
|
+
return {
|
|
3162
|
+
runtime,
|
|
3163
|
+
env: {
|
|
3164
|
+
...process.env,
|
|
3165
|
+
...isPublic ? runtime.toPublicEnv({
|
|
3166
|
+
...framework ? { framework } : {},
|
|
3167
|
+
...prefix ? { prefix } : {}
|
|
3168
|
+
}) : runtime.toEnv(),
|
|
3169
|
+
[CNOS_GRAPH_ENV_VAR2]: serializeRuntimeGraph2(runtime.graph),
|
|
3170
|
+
...secretPayload ? {
|
|
3171
|
+
[CNOS_SECRET_PAYLOAD_ENV_VAR2]: secretPayload.payload,
|
|
3172
|
+
[CNOS_SESSION_KEY_ENV_VAR2]: secretPayload.sessionKey
|
|
3173
|
+
} : {}
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
function spawnWatchedChild(command, cwd, env) {
|
|
3178
|
+
const executable = command[0];
|
|
3179
|
+
if (!executable) {
|
|
3180
|
+
throw new Error("watch requires a command after -- unless --signal is used");
|
|
3181
|
+
}
|
|
3182
|
+
return spawn2(executable, command.slice(1), {
|
|
3183
|
+
cwd,
|
|
3184
|
+
env,
|
|
3185
|
+
stdio: "inherit",
|
|
3186
|
+
shell: false
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
async function startWatchLoop(options) {
|
|
3190
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
3191
|
+
const isSignal = consumeFlag(cliArgs, "--signal");
|
|
3192
|
+
const debounceMs = Number(consumeOption(cliArgs, "--debounce") ?? "300");
|
|
3193
|
+
const command = options.command ?? [];
|
|
3194
|
+
const root = options.root ?? process.cwd();
|
|
3195
|
+
let current = await buildRunEnvironment({
|
|
3196
|
+
...options,
|
|
3197
|
+
cliArgs
|
|
3198
|
+
});
|
|
3199
|
+
let child = !isSignal ? spawnWatchedChild(command, root, current.env) : void 0;
|
|
3200
|
+
const watcherMap = /* @__PURE__ */ new Map();
|
|
3201
|
+
let timer;
|
|
3202
|
+
let closed = false;
|
|
3203
|
+
const attachWatcher = (targetPath, recursive = false) => {
|
|
3204
|
+
if (watcherMap.has(targetPath)) {
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
try {
|
|
3208
|
+
const watcher = watch(
|
|
3209
|
+
targetPath,
|
|
3210
|
+
recursive ? {
|
|
3211
|
+
recursive: true
|
|
3212
|
+
} : void 0,
|
|
3213
|
+
() => {
|
|
3214
|
+
if (timer) {
|
|
3215
|
+
clearTimeout(timer);
|
|
3216
|
+
}
|
|
3217
|
+
timer = setTimeout(() => {
|
|
3218
|
+
void handleChange();
|
|
3219
|
+
}, debounceMs);
|
|
3220
|
+
}
|
|
3221
|
+
);
|
|
3222
|
+
watcherMap.set(targetPath, watcher);
|
|
3223
|
+
} catch {
|
|
3224
|
+
if (recursive) {
|
|
3225
|
+
attachWatcher(targetPath, false);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
const refreshWatchers = async () => {
|
|
3230
|
+
const nextTargets = await watchFiles(current.runtime, options.root);
|
|
3231
|
+
attachWatcher(nextTargets.manifestPath, false);
|
|
3232
|
+
for (const workspaceRoot of nextTargets.roots) {
|
|
3233
|
+
attachWatcher(workspaceRoot, true);
|
|
3234
|
+
}
|
|
3235
|
+
for (const filePath of nextTargets.files) {
|
|
3236
|
+
attachWatcher(filePath, false);
|
|
3237
|
+
}
|
|
3238
|
+
};
|
|
3239
|
+
const handleChange = async () => {
|
|
3240
|
+
if (closed) {
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
const next = await buildRunEnvironment({
|
|
3244
|
+
...options,
|
|
3245
|
+
cliArgs
|
|
3246
|
+
});
|
|
3247
|
+
const changedKeys = diffGraphs(current.runtime.graph, next.runtime.graph);
|
|
3248
|
+
current = next;
|
|
3249
|
+
await refreshWatchers();
|
|
3250
|
+
if (changedKeys.length === 0) {
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
if (isSignal) {
|
|
3254
|
+
await options.onSignal?.({ changedKeys });
|
|
3255
|
+
process.stdout.write(`${printJson({ changedKeys })}
|
|
3256
|
+
`);
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
if (child && !child.killed) {
|
|
3260
|
+
await new Promise((resolve) => {
|
|
3261
|
+
child?.once("close", () => resolve());
|
|
3262
|
+
child?.kill();
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
child = spawnWatchedChild(command, root, current.env);
|
|
3266
|
+
await options.onRestart?.({ changedKeys });
|
|
3267
|
+
};
|
|
3268
|
+
await refreshWatchers();
|
|
3269
|
+
return {
|
|
3270
|
+
async close() {
|
|
3271
|
+
closed = true;
|
|
3272
|
+
if (timer) {
|
|
3273
|
+
clearTimeout(timer);
|
|
3274
|
+
}
|
|
3275
|
+
for (const watcher of watcherMap.values()) {
|
|
3276
|
+
watcher.close();
|
|
3277
|
+
}
|
|
3278
|
+
watcherMap.clear();
|
|
3279
|
+
if (child && !child.killed) {
|
|
3280
|
+
await new Promise((resolve) => {
|
|
3281
|
+
child?.once("close", () => resolve());
|
|
3282
|
+
child?.kill();
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
async function runWatch(command, options = {}) {
|
|
3289
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
3290
|
+
const isSignal = consumeFlag(cliArgs, "--signal");
|
|
3291
|
+
const debounce = consumeOption(cliArgs, "--debounce");
|
|
3292
|
+
const handle = await startWatchLoop({
|
|
3293
|
+
...options,
|
|
3294
|
+
cliArgs: [
|
|
3295
|
+
...cliArgs,
|
|
3296
|
+
...isSignal ? ["--signal"] : [],
|
|
3297
|
+
...debounce ? ["--debounce", debounce] : []
|
|
3298
|
+
],
|
|
3299
|
+
command
|
|
3300
|
+
});
|
|
3301
|
+
const closeWatcher = () => {
|
|
3302
|
+
void handle.close();
|
|
3303
|
+
};
|
|
3304
|
+
process.once("SIGINT", closeWatcher);
|
|
3305
|
+
process.once("SIGTERM", closeWatcher);
|
|
3306
|
+
return isSignal ? "watching config changes in signal mode" : "watching config changes in restart mode";
|
|
3307
|
+
}
|
|
3308
|
+
|
|
2535
3309
|
// src/index.ts
|
|
2536
3310
|
function resolveHelpTopic(command, args) {
|
|
2537
3311
|
if (command === "help" || command === "help-ai") {
|
|
@@ -2612,6 +3386,14 @@ async function main(argv) {
|
|
|
2612
3386
|
return;
|
|
2613
3387
|
case "onboard":
|
|
2614
3388
|
process.stdout.write(`${await runOnboard(runtimeOptions)}
|
|
3389
|
+
`);
|
|
3390
|
+
return;
|
|
3391
|
+
case "migrate":
|
|
3392
|
+
process.stdout.write(`${await runMigrate(runtimeOptions)}
|
|
3393
|
+
`);
|
|
3394
|
+
return;
|
|
3395
|
+
case "codegen":
|
|
3396
|
+
process.stdout.write(`${await runCodegen(runtimeOptions)}
|
|
2615
3397
|
`);
|
|
2616
3398
|
return;
|
|
2617
3399
|
case "read":
|
|
@@ -2676,8 +3458,16 @@ async function main(argv) {
|
|
|
2676
3458
|
process.exitCode = result.exitCode;
|
|
2677
3459
|
return;
|
|
2678
3460
|
}
|
|
3461
|
+
case "watch":
|
|
3462
|
+
process.stdout.write(`${await runWatch(passthrough.length > 0 ? passthrough : args, runtimeOptions)}
|
|
3463
|
+
`);
|
|
3464
|
+
return;
|
|
2679
3465
|
case "diff":
|
|
2680
3466
|
process.stdout.write(`${await runDiff(args[0] ?? "local", args[1] ?? "stage", runtimeOptions)}
|
|
3467
|
+
`);
|
|
3468
|
+
return;
|
|
3469
|
+
case "drift":
|
|
3470
|
+
process.stdout.write(`${await runDrift(runtimeOptions)}
|
|
2681
3471
|
`);
|
|
2682
3472
|
return;
|
|
2683
3473
|
case "doctor":
|
|
@@ -2685,6 +3475,11 @@ async function main(argv) {
|
|
|
2685
3475
|
`);
|
|
2686
3476
|
return;
|
|
2687
3477
|
default:
|
|
3478
|
+
if (args.length > 0 && /^[a-z][a-z0-9-]*$/i.test(command)) {
|
|
3479
|
+
process.stdout.write(`${await runNamespace(command, args, runtimeOptions)}
|
|
3480
|
+
`);
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
2688
3483
|
process.stderr.write(`Unknown command: ${command}
|
|
2689
3484
|
`);
|
|
2690
3485
|
process.exitCode = 1;
|