@kitsy/cnos-cli 1.2.0 → 1.3.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 +885 -310
- 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,34 @@ 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
|
+
if ((command === "set" || command === "get") && (resource === "value" || resource === "secret")) {
|
|
48
|
+
return [resource, command, ...remaining];
|
|
49
|
+
}
|
|
28
50
|
if ((command === "create" || command === "add") && resource === "profile") {
|
|
29
51
|
return ["profile", "create", ...remaining];
|
|
30
52
|
}
|
|
@@ -37,6 +59,12 @@ function normalizeCommand(argv) {
|
|
|
37
59
|
if ((command === "delete" || command === "remove") && resource === "vault") {
|
|
38
60
|
return ["vault", "remove", ...remaining];
|
|
39
61
|
}
|
|
62
|
+
if (command === "auth" && resource === "vault") {
|
|
63
|
+
return ["vault", "auth", ...remaining];
|
|
64
|
+
}
|
|
65
|
+
if (command === "logout" && resource === "vault") {
|
|
66
|
+
return ["vault", "logout", ...remaining];
|
|
67
|
+
}
|
|
40
68
|
if (command === "list" && resource === "profile") {
|
|
41
69
|
return ["profile", "list", ...remaining];
|
|
42
70
|
}
|
|
@@ -197,166 +225,29 @@ function printJson(value) {
|
|
|
197
225
|
}
|
|
198
226
|
|
|
199
227
|
// src/services/writes.ts
|
|
200
|
-
import { mkdir, readFile
|
|
201
|
-
import
|
|
228
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
229
|
+
import path from "path";
|
|
202
230
|
import {
|
|
203
|
-
|
|
231
|
+
createSecretVaultProvider,
|
|
204
232
|
parseYaml,
|
|
205
233
|
resolveConfigDocumentPath,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
resolveConfiguredVaultPassphrase as resolveConfiguredVaultPassphrase2,
|
|
209
|
-
resolveSecretStoreRoot as resolveSecretStoreRoot2
|
|
234
|
+
resolveVaultAuth,
|
|
235
|
+
stringifyYaml
|
|
210
236
|
} from "@kitsy/cnos/internal";
|
|
211
237
|
|
|
212
238
|
// src/services/runtime.ts
|
|
213
|
-
import { createCnos } from "@kitsy/cnos";
|
|
239
|
+
import { createCnos } from "@kitsy/cnos/configure";
|
|
214
240
|
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
241
|
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
|
-
};
|
|
242
|
+
...options.root ? { root: options.root } : {},
|
|
243
|
+
...options.workspace ? { workspace: options.workspace } : {},
|
|
244
|
+
...options.profile ? { profile: options.profile } : {},
|
|
245
|
+
...options.globalRoot ? { globalRoot: options.globalRoot } : {},
|
|
246
|
+
...options.cliArgs && options.cliArgs.length > 0 ? { cliArgs: options.cliArgs } : {},
|
|
247
|
+
...options.secretResolution ? { secretResolution: options.secretResolution } : {},
|
|
248
|
+
processEnv: options.processEnv ?? process.env
|
|
316
249
|
});
|
|
317
250
|
}
|
|
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
251
|
|
|
361
252
|
// src/services/writes.ts
|
|
362
253
|
function setNestedValue(target, pathSegments, value) {
|
|
@@ -405,7 +296,7 @@ function isSecretReference(value) {
|
|
|
405
296
|
}
|
|
406
297
|
async function readYamlDocument(filePath) {
|
|
407
298
|
try {
|
|
408
|
-
return parseYaml(await
|
|
299
|
+
return parseYaml(await readFile(filePath, "utf8")) ?? {};
|
|
409
300
|
} catch {
|
|
410
301
|
return {};
|
|
411
302
|
}
|
|
@@ -433,7 +324,8 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
|
|
|
433
324
|
filePath: secret.filePath,
|
|
434
325
|
value: {
|
|
435
326
|
provider: secret.provider,
|
|
436
|
-
ref: secret.ref
|
|
327
|
+
ref: secret.ref,
|
|
328
|
+
...secret.vault ? { vault: secret.vault } : {}
|
|
437
329
|
}
|
|
438
330
|
};
|
|
439
331
|
}
|
|
@@ -444,8 +336,8 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
|
|
|
444
336
|
const document = await readYamlDocument(filePath);
|
|
445
337
|
const parsedValue = parseScalarValue(rawValue);
|
|
446
338
|
setNestedValue(document, configPath.split("."), parsedValue);
|
|
447
|
-
await mkdir(
|
|
448
|
-
await
|
|
339
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
340
|
+
await writeFile(filePath, stringifyYaml(document), "utf8");
|
|
449
341
|
return {
|
|
450
342
|
filePath,
|
|
451
343
|
value: parsedValue
|
|
@@ -459,53 +351,36 @@ async function setSecret(configPath, rawValue, options = {}) {
|
|
|
459
351
|
const document = await readYamlDocument(filePath);
|
|
460
352
|
const vault = options.vault?.trim() || "default";
|
|
461
353
|
const vaultDefinition = runtime.manifest.vaults[vault];
|
|
462
|
-
|
|
463
|
-
|
|
354
|
+
if (!vaultDefinition) {
|
|
355
|
+
throw new Error(`Unknown vault "${vault}". Create it first with cnos vault create ${vault}.`);
|
|
356
|
+
}
|
|
357
|
+
const mode = options.mode ?? (vaultDefinition.provider === "local" ? "local" : vaultDefinition.provider === "github-secrets" ? "ref" : "remote");
|
|
464
358
|
let reference;
|
|
465
|
-
let storePath;
|
|
466
359
|
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);
|
|
360
|
+
const auth = await resolveVaultAuth(vault, vaultDefinition, options.processEnv ?? process.env);
|
|
361
|
+
const provider = createSecretVaultProvider(vault, vaultDefinition, options.processEnv ?? process.env);
|
|
362
|
+
await provider.authenticate(auth);
|
|
363
|
+
await provider.set(configPath, rawValue);
|
|
486
364
|
reference = {
|
|
487
365
|
provider: "local",
|
|
488
|
-
ref,
|
|
366
|
+
ref: configPath,
|
|
489
367
|
vault
|
|
490
368
|
};
|
|
491
369
|
} else {
|
|
492
370
|
reference = {
|
|
493
|
-
provider:
|
|
371
|
+
provider: options.provider?.trim() || vaultDefinition.provider,
|
|
494
372
|
ref: rawValue || configPath.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase(),
|
|
495
|
-
|
|
496
|
-
vault
|
|
497
|
-
} : {}
|
|
373
|
+
vault
|
|
498
374
|
};
|
|
499
375
|
}
|
|
500
376
|
setNestedValue(document, configPath.split("."), reference);
|
|
501
|
-
await mkdir(
|
|
502
|
-
await
|
|
377
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
378
|
+
await writeFile(filePath, stringifyYaml(document), "utf8");
|
|
503
379
|
return {
|
|
504
380
|
filePath,
|
|
505
381
|
provider: reference.provider,
|
|
506
382
|
ref: reference.ref,
|
|
507
|
-
...reference.vault ? { vault: reference.vault } : {}
|
|
508
|
-
...storePath ? { storePath } : {}
|
|
383
|
+
...reference.vault ? { vault: reference.vault } : {}
|
|
509
384
|
};
|
|
510
385
|
}
|
|
511
386
|
async function deleteSecret(configPath, options = {}) {
|
|
@@ -522,24 +397,20 @@ async function deleteSecret(configPath, options = {}) {
|
|
|
522
397
|
deleted: false
|
|
523
398
|
};
|
|
524
399
|
}
|
|
525
|
-
await
|
|
526
|
-
let removedStore;
|
|
400
|
+
await writeFile(filePath, stringifyYaml(document), "utf8");
|
|
527
401
|
const secretRef = metadata?.secretRef;
|
|
528
402
|
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;
|
|
403
|
+
const definition = runtime.manifest.vaults[secretRef.vault ?? "default"];
|
|
404
|
+
if (definition) {
|
|
405
|
+
const auth = await resolveVaultAuth(secretRef.vault ?? "default", definition, options.processEnv ?? process.env);
|
|
406
|
+
const provider = createSecretVaultProvider(secretRef.vault ?? "default", definition, options.processEnv ?? process.env);
|
|
407
|
+
await provider.authenticate(auth);
|
|
408
|
+
await provider.delete(secretRef.ref);
|
|
409
|
+
}
|
|
538
410
|
}
|
|
539
411
|
return {
|
|
540
412
|
filePath,
|
|
541
|
-
deleted: true
|
|
542
|
-
...removedStore ? { removedStore } : {}
|
|
413
|
+
deleted: true
|
|
543
414
|
};
|
|
544
415
|
}
|
|
545
416
|
async function deleteValue(namespace, configPath, options = {}) {
|
|
@@ -558,7 +429,7 @@ async function deleteValue(namespace, configPath, options = {}) {
|
|
|
558
429
|
deleted: false
|
|
559
430
|
};
|
|
560
431
|
}
|
|
561
|
-
await
|
|
432
|
+
await writeFile(filePath, stringifyYaml(document), "utf8");
|
|
562
433
|
return {
|
|
563
434
|
filePath,
|
|
564
435
|
deleted: true
|
|
@@ -596,6 +467,17 @@ async function runDefine(namespace, configPath, rawValue, options = {}) {
|
|
|
596
467
|
return `defined ${namespace}.${configPath} in ${result.filePath}`;
|
|
597
468
|
}
|
|
598
469
|
|
|
470
|
+
// src/commands/drift.ts
|
|
471
|
+
import { compareSchemaToGraph, formatDriftReport } from "@kitsy/cnos/internal";
|
|
472
|
+
async function runDrift(options = {}) {
|
|
473
|
+
const runtime = await createRuntimeService(options);
|
|
474
|
+
const report = compareSchemaToGraph(runtime);
|
|
475
|
+
if (options.json) {
|
|
476
|
+
return printJson(report);
|
|
477
|
+
}
|
|
478
|
+
return formatDriftReport(report);
|
|
479
|
+
}
|
|
480
|
+
|
|
599
481
|
// src/commands/diff.ts
|
|
600
482
|
import { flattenObject } from "@kitsy/cnos/internal";
|
|
601
483
|
function flattenRuntime(runtime) {
|
|
@@ -634,8 +516,15 @@ async function runDiff(leftProfile, rightProfile, options = {}) {
|
|
|
634
516
|
}
|
|
635
517
|
|
|
636
518
|
// src/services/doctor.ts
|
|
637
|
-
import { readFile as
|
|
638
|
-
import
|
|
519
|
+
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
520
|
+
import path2 from "path";
|
|
521
|
+
import {
|
|
522
|
+
detectLegacyVaultFormat,
|
|
523
|
+
isSecretReference as isSecretReference2,
|
|
524
|
+
parseYaml as parseYaml2,
|
|
525
|
+
readKeychain,
|
|
526
|
+
resolveSecretStoreRoot
|
|
527
|
+
} from "@kitsy/cnos/internal";
|
|
639
528
|
|
|
640
529
|
// src/services/validation.ts
|
|
641
530
|
import { validateRuntime } from "@kitsy/cnos/internal";
|
|
@@ -650,7 +539,7 @@ async function createValidationSummary(options = {}) {
|
|
|
650
539
|
|
|
651
540
|
// src/services/doctor.ts
|
|
652
541
|
async function checkGitignore(root) {
|
|
653
|
-
const gitignorePath =
|
|
542
|
+
const gitignorePath = path2.join(root, ".gitignore");
|
|
654
543
|
const expected = [
|
|
655
544
|
".cnos/env/.env",
|
|
656
545
|
".cnos/env/.env.*",
|
|
@@ -662,7 +551,7 @@ async function checkGitignore(root) {
|
|
|
662
551
|
"!.cnos/workspaces/*/env/.env.*.example"
|
|
663
552
|
];
|
|
664
553
|
try {
|
|
665
|
-
const content = await
|
|
554
|
+
const content = await readFile2(gitignorePath, "utf8");
|
|
666
555
|
const missing = expected.filter((entry) => !content.includes(entry));
|
|
667
556
|
return {
|
|
668
557
|
name: "gitignore",
|
|
@@ -680,8 +569,72 @@ async function checkGitignore(root) {
|
|
|
680
569
|
function issueSummary(issues) {
|
|
681
570
|
return issues.length === 0 ? "no issues" : issues.map((issue) => issue.message).join("; ");
|
|
682
571
|
}
|
|
572
|
+
async function collectYamlFiles(root) {
|
|
573
|
+
try {
|
|
574
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
575
|
+
const results = [];
|
|
576
|
+
for (const entry of entries) {
|
|
577
|
+
const target = path2.join(root, entry.name);
|
|
578
|
+
if (entry.isDirectory()) {
|
|
579
|
+
results.push(...await collectYamlFiles(target));
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (entry.isFile() && [".yml", ".yaml"].includes(path2.extname(entry.name).toLowerCase())) {
|
|
583
|
+
results.push(target);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return results;
|
|
587
|
+
} catch {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function hasPlaintextSecret(value) {
|
|
592
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
593
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
594
|
+
}
|
|
595
|
+
if (isSecretReference2(value)) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
return Object.values(value).some((entry) => hasPlaintextSecret(entry));
|
|
599
|
+
}
|
|
600
|
+
async function checkSecretSecurity(options, runtime) {
|
|
601
|
+
const storeRoot = resolveSecretStoreRoot(options.processEnv);
|
|
602
|
+
const legacyPaths = await Promise.all(
|
|
603
|
+
Object.entries(runtime.manifest.vaults).filter(([, definition]) => definition.provider === "local").map(async ([vault]) => ({ vault, path: await detectLegacyVaultFormat(storeRoot, vault) }))
|
|
604
|
+
);
|
|
605
|
+
const legacyDetected = legacyPaths.filter((entry) => Boolean(entry.path));
|
|
606
|
+
const secretFiles = await Promise.all(
|
|
607
|
+
runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => collectYamlFiles(path2.join(root.path, "secrets")))
|
|
608
|
+
);
|
|
609
|
+
const plaintextFiles = [];
|
|
610
|
+
for (const file of secretFiles.flat()) {
|
|
611
|
+
try {
|
|
612
|
+
const parsed = parseYaml2(await readFile2(file, "utf8"));
|
|
613
|
+
if (hasPlaintextSecret(parsed)) {
|
|
614
|
+
plaintextFiles.push(file);
|
|
615
|
+
}
|
|
616
|
+
} catch {
|
|
617
|
+
plaintextFiles.push(file);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const keychainWarnings = await Promise.all(
|
|
621
|
+
Object.entries(runtime.manifest.vaults).filter(([, definition]) => definition.provider === "local").flatMap(
|
|
622
|
+
([vault, definition]) => (definition.auth?.passphrase?.from ?? []).filter((source) => source.startsWith("keychain:")).map(async (source) => ({ vault, source, value: await readKeychain(source.slice("keychain:".length)) }))
|
|
623
|
+
)
|
|
624
|
+
);
|
|
625
|
+
const warnings = [
|
|
626
|
+
...legacyDetected.map((entry) => `legacy vault ${entry.vault}: ${entry.path}`),
|
|
627
|
+
...plaintextFiles.map((file) => `plaintext secret file: ${file}`),
|
|
628
|
+
...keychainWarnings.filter((entry) => !entry.value).map((entry) => `no keychain entry for vault ${entry.vault} (${entry.source})`)
|
|
629
|
+
];
|
|
630
|
+
return {
|
|
631
|
+
name: "security",
|
|
632
|
+
ok: warnings.length === 0,
|
|
633
|
+
details: warnings.length === 0 ? "no legacy vaults, plaintext secret files, or missing keychain entries" : warnings.join("; ")
|
|
634
|
+
};
|
|
635
|
+
}
|
|
683
636
|
async function evaluateDoctor(options = {}) {
|
|
684
|
-
const root =
|
|
637
|
+
const root = path2.resolve(options.root ?? process.cwd());
|
|
685
638
|
const { runtime, summary } = await createValidationSummary(options);
|
|
686
639
|
const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
|
|
687
640
|
const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
|
|
@@ -711,6 +664,7 @@ async function evaluateDoctor(options = {}) {
|
|
|
711
664
|
ok: !runtime.manifest.workspaces.global.enabled || Boolean(runtime.graph.workspace.globalRoot),
|
|
712
665
|
details: runtime.manifest.workspaces.global.enabled ? runtime.graph.workspace.globalRoot ? `enabled at ${runtime.graph.workspace.globalRoot}` : "enabled but no global root resolved" : "disabled"
|
|
713
666
|
},
|
|
667
|
+
await checkSecretSecurity(options, runtime),
|
|
714
668
|
await checkGitignore(root)
|
|
715
669
|
];
|
|
716
670
|
}
|
|
@@ -729,7 +683,7 @@ async function runDoctor(options = {}) {
|
|
|
729
683
|
}
|
|
730
684
|
|
|
731
685
|
// src/commands/dump.ts
|
|
732
|
-
import { writeDump } from "@kitsy/cnos";
|
|
686
|
+
import { writeDump } from "@kitsy/cnos/configure";
|
|
733
687
|
async function runDump(options = {}) {
|
|
734
688
|
const cliArgs = [...options.cliArgs ?? []];
|
|
735
689
|
const flatten = consumeFlag(cliArgs, "--flatten");
|
|
@@ -751,9 +705,51 @@ async function runDump(options = {}) {
|
|
|
751
705
|
return `dumped ${result.files.length} files to ${result.root}`;
|
|
752
706
|
}
|
|
753
707
|
|
|
708
|
+
// src/commands/codegen.ts
|
|
709
|
+
import { watchSchema, writeCodegenOutput } from "@kitsy/cnos/internal";
|
|
710
|
+
async function runCodegen(options = {}) {
|
|
711
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
712
|
+
const out = consumeOption(cliArgs, "--out");
|
|
713
|
+
const watch2 = consumeFlag(cliArgs, "--watch");
|
|
714
|
+
if (cliArgs.length > 0) {
|
|
715
|
+
throw new Error(`Unknown codegen options: ${cliArgs.join(" ")}`);
|
|
716
|
+
}
|
|
717
|
+
if (watch2) {
|
|
718
|
+
const watcher = await watchSchema({
|
|
719
|
+
...options.root ? {
|
|
720
|
+
root: options.root
|
|
721
|
+
} : {},
|
|
722
|
+
...out ? {
|
|
723
|
+
out
|
|
724
|
+
} : {},
|
|
725
|
+
onError(error) {
|
|
726
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
727
|
+
process.stderr.write(`${message}
|
|
728
|
+
`);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
const closeWatcher = () => {
|
|
732
|
+
watcher.close();
|
|
733
|
+
};
|
|
734
|
+
process.once("SIGINT", closeWatcher);
|
|
735
|
+
process.once("SIGTERM", closeWatcher);
|
|
736
|
+
return `watching schema changes -> ${out ?? ".cnos/types/cnos.d.ts"}`;
|
|
737
|
+
}
|
|
738
|
+
const result = await writeCodegenOutput({
|
|
739
|
+
...options.root ? {
|
|
740
|
+
root: options.root
|
|
741
|
+
} : {},
|
|
742
|
+
...out ? {
|
|
743
|
+
out
|
|
744
|
+
} : {}
|
|
745
|
+
});
|
|
746
|
+
const summary = result.hasSchema ? `generated types from ${result.schemaEntryCount} schema entr${result.schemaEntryCount === 1 ? "y" : "ies"}` : "generated empty types (no schema section found)";
|
|
747
|
+
return `${summary} -> ${result.typesPath} and ${result.runtimePath}`;
|
|
748
|
+
}
|
|
749
|
+
|
|
754
750
|
// src/commands/exportEnv.ts
|
|
755
|
-
import { mkdir as mkdir2, writeFile as
|
|
756
|
-
import
|
|
751
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
752
|
+
import path3 from "path";
|
|
757
753
|
function formatEnvOutput(env) {
|
|
758
754
|
return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
|
|
759
755
|
}
|
|
@@ -773,9 +769,9 @@ async function runExportEnv(options = {}) {
|
|
|
773
769
|
}) : runtime.toEnv();
|
|
774
770
|
const output = formatEnvOutput(env);
|
|
775
771
|
if (to) {
|
|
776
|
-
const targetPath =
|
|
777
|
-
await mkdir2(
|
|
778
|
-
await
|
|
772
|
+
const targetPath = path3.resolve(options.root ?? process.cwd(), to);
|
|
773
|
+
await mkdir2(path3.dirname(targetPath), { recursive: true });
|
|
774
|
+
await writeFile2(targetPath, output, "utf8");
|
|
779
775
|
if (options.json) {
|
|
780
776
|
return printJson({
|
|
781
777
|
to: targetPath,
|
|
@@ -852,6 +848,23 @@ var COMMANDS = [
|
|
|
852
848
|
],
|
|
853
849
|
examples: ["cnos onboard", "cnos onboard --workspace webapp", "cnos onboard --root ../my-app --workspace app --move"]
|
|
854
850
|
},
|
|
851
|
+
{
|
|
852
|
+
id: "codegen",
|
|
853
|
+
summary: "Generate typed CNOS access wrappers from schema.",
|
|
854
|
+
usage: "cnos codegen [--out <path>] [--watch] [--root <path>]",
|
|
855
|
+
description: "Reads schema from .cnos/cnos.yml and generates typed CNOS declaration output plus a typed createCnos wrapper.",
|
|
856
|
+
options: [
|
|
857
|
+
{
|
|
858
|
+
flag: "--out <path>",
|
|
859
|
+
description: "Custom path for the generated type declaration file. runtime.ts is emitted beside it."
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
flag: "--watch",
|
|
863
|
+
description: "Watch the manifest schema and regenerate output when it changes."
|
|
864
|
+
}
|
|
865
|
+
],
|
|
866
|
+
examples: ["cnos codegen", "cnos codegen --out src/cnos-config.d.ts", "cnos codegen --watch"]
|
|
867
|
+
},
|
|
855
868
|
{
|
|
856
869
|
id: "read",
|
|
857
870
|
summary: "Read any fully-qualified CNOS key.",
|
|
@@ -943,18 +956,19 @@ var COMMANDS = [
|
|
|
943
956
|
flag: "--provider <name>",
|
|
944
957
|
description: "Provider name for --remote or --ref secret writes."
|
|
945
958
|
},
|
|
946
|
-
{
|
|
947
|
-
flag: "--passphrase <value>",
|
|
948
|
-
description: "Passphrase used to encrypt local secret material when --local is selected."
|
|
949
|
-
},
|
|
950
959
|
{
|
|
951
960
|
flag: "--vault <name>",
|
|
952
961
|
description: "Use a manifest-defined vault. Provider behavior is inferred from the vault definition."
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
flag: "--reveal",
|
|
965
|
+
description: "Reveal the resolved secret value for get-style reads. Output is masked by default."
|
|
953
966
|
}
|
|
954
967
|
],
|
|
955
968
|
examples: [
|
|
956
969
|
"cnos secret app.token",
|
|
957
|
-
"cnos vault create local-dev
|
|
970
|
+
"cnos vault create local-dev",
|
|
971
|
+
"cnos vault auth local-dev",
|
|
958
972
|
"cnos secret set app.token super-secret --vault local-dev",
|
|
959
973
|
"cnos vault create github-ci --provider github-secrets --no-passphrase",
|
|
960
974
|
"cnos secret set app.token APP_TOKEN --vault github-ci"
|
|
@@ -970,17 +984,14 @@ var COMMANDS = [
|
|
|
970
984
|
flag: "--provider <local|github-secrets>",
|
|
971
985
|
description: "Vault provider. Defaults to local."
|
|
972
986
|
},
|
|
973
|
-
{
|
|
974
|
-
flag: "--passphrase <value>",
|
|
975
|
-
description: "Required for local vault creation unless already available in the configured passphrase env var."
|
|
976
|
-
},
|
|
977
987
|
{
|
|
978
988
|
flag: "--no-passphrase",
|
|
979
989
|
description: "Allowed for passwordless providers such as github-secrets."
|
|
980
990
|
}
|
|
981
991
|
],
|
|
982
992
|
examples: [
|
|
983
|
-
"cnos vault create local-dev
|
|
993
|
+
"cnos vault create local-dev",
|
|
994
|
+
"cnos vault auth local-dev",
|
|
984
995
|
"cnos vault create github-ci --provider github-secrets --no-passphrase",
|
|
985
996
|
"cnos vault list",
|
|
986
997
|
"cnos vault remove local-dev"
|
|
@@ -989,13 +1000,33 @@ var COMMANDS = [
|
|
|
989
1000
|
{
|
|
990
1001
|
id: "vault create",
|
|
991
1002
|
summary: "Create a manifest-defined vault.",
|
|
992
|
-
usage: "cnos vault create <name> [--provider <local|github-secrets>] [--
|
|
1003
|
+
usage: "cnos vault create <name> [--provider <local|github-secrets>] [--no-passphrase] [global-options]",
|
|
993
1004
|
description: "Creates a vault definition in .cnos/cnos.yml and, for local vaults, initializes the encrypted store under ~/.cnos/secrets.",
|
|
994
1005
|
examples: [
|
|
995
|
-
"cnos vault create local-dev
|
|
1006
|
+
"cnos vault create local-dev",
|
|
996
1007
|
"cnos vault create github-ci --provider github-secrets --no-passphrase"
|
|
997
1008
|
]
|
|
998
1009
|
},
|
|
1010
|
+
{
|
|
1011
|
+
id: "vault auth",
|
|
1012
|
+
summary: "Authenticate a vault for the current shell session.",
|
|
1013
|
+
usage: "cnos vault auth <name> [--store-keychain] [global-options]",
|
|
1014
|
+
description: "Authenticates a local vault using env, keychain, or prompt-based auth and stores a derived session key for later CNOS commands in the same shell.",
|
|
1015
|
+
examples: ["cnos vault auth local-dev", "cnos vault auth local-dev --store-keychain"]
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
id: "vault logout",
|
|
1019
|
+
summary: "Clear vault auth state for the current shell session.",
|
|
1020
|
+
usage: "cnos vault logout <name> [global-options]",
|
|
1021
|
+
description: "Removes active vault session auth for the selected vault or all vaults when used with --all.",
|
|
1022
|
+
options: [
|
|
1023
|
+
{
|
|
1024
|
+
flag: "--all",
|
|
1025
|
+
description: "Clear all active vault auth sessions for the current shell context."
|
|
1026
|
+
}
|
|
1027
|
+
],
|
|
1028
|
+
examples: ["cnos vault logout local-dev", "cnos vault logout --all"]
|
|
1029
|
+
},
|
|
999
1030
|
{
|
|
1000
1031
|
id: "vault list",
|
|
1001
1032
|
summary: "List manifest-defined vaults.",
|
|
@@ -1139,10 +1170,11 @@ var COMMANDS = [
|
|
|
1139
1170
|
{
|
|
1140
1171
|
id: "secret set",
|
|
1141
1172
|
summary: "Write a secret securely.",
|
|
1142
|
-
usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [
|
|
1173
|
+
usage: "cnos secret set <path> <value> [--local|--remote|--ref] [--vault <name>] [--provider <name>] [global-options]",
|
|
1143
1174
|
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
1175
|
examples: [
|
|
1145
|
-
"cnos vault create db
|
|
1176
|
+
"cnos vault create db",
|
|
1177
|
+
"cnos vault auth db",
|
|
1146
1178
|
"cnos secret set app.token super-secret --vault db",
|
|
1147
1179
|
"cnos vault create github-ci --provider github-secrets --no-passphrase",
|
|
1148
1180
|
"cnos secret set app.token APP_TOKEN --vault github-ci"
|
|
@@ -1151,9 +1183,9 @@ var COMMANDS = [
|
|
|
1151
1183
|
{
|
|
1152
1184
|
id: "secret create vault",
|
|
1153
1185
|
summary: "Create a local secret vault.",
|
|
1154
|
-
usage: "cnos secret create vault <name>
|
|
1186
|
+
usage: "cnos secret create vault <name> [global-options]",
|
|
1155
1187
|
description: "Alias for cnos vault create <name>.",
|
|
1156
|
-
examples: ["cnos secret create vault db
|
|
1188
|
+
examples: ["cnos secret create vault db"]
|
|
1157
1189
|
},
|
|
1158
1190
|
{
|
|
1159
1191
|
id: "secret list",
|
|
@@ -1310,6 +1342,60 @@ var COMMANDS = [
|
|
|
1310
1342
|
description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace.",
|
|
1311
1343
|
examples: ["cnos doctor", "cnos doctor --workspace api --json"]
|
|
1312
1344
|
},
|
|
1345
|
+
{
|
|
1346
|
+
id: "drift",
|
|
1347
|
+
summary: "Compare resolved config against schema and report drift.",
|
|
1348
|
+
usage: "cnos drift [--workspace <id>] [--profile <name>] [--json]",
|
|
1349
|
+
description: "Reports missing required keys, undeclared keys, type mismatches, and defaults applied for the selected workspace/profile.",
|
|
1350
|
+
examples: ["cnos drift", "cnos drift --workspace api --profile stage", "cnos drift --json"]
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
id: "watch",
|
|
1354
|
+
summary: "Watch CNOS inputs and either restart a process or emit changed keys.",
|
|
1355
|
+
usage: "cnos watch [--signal] [--debounce <ms>] [global-options] -- <command...>",
|
|
1356
|
+
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.",
|
|
1357
|
+
options: [
|
|
1358
|
+
{
|
|
1359
|
+
flag: "--signal",
|
|
1360
|
+
description: "Emit changed keys as JSON instead of restarting a child process."
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
flag: "--debounce <ms>",
|
|
1364
|
+
description: "Debounce change handling before re-resolving the graph. Defaults to 300ms."
|
|
1365
|
+
}
|
|
1366
|
+
],
|
|
1367
|
+
examples: ["cnos watch -- node server.js", "cnos watch --signal", "cnos watch --debounce 100 -- node server.js"]
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
id: "migrate",
|
|
1371
|
+
summary: "Scan env usage and propose CNOS manifest mappings.",
|
|
1372
|
+
usage: "cnos migrate [--scan <path>] [--dry-run] [--apply] [--rewrite] [global-options]",
|
|
1373
|
+
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.",
|
|
1374
|
+
options: [
|
|
1375
|
+
{
|
|
1376
|
+
flag: "--scan <path>",
|
|
1377
|
+
description: "Directory to scan. Defaults to ./src relative to the repo root."
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
flag: "--dry-run",
|
|
1381
|
+
description: "Preview the proposed mappings without changing the manifest."
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
flag: "--apply",
|
|
1385
|
+
description: "Write proposed env mappings and public promotions into .cnos/cnos.yml."
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
flag: "--rewrite",
|
|
1389
|
+
description: "With --apply, rewrite supported process.env usages in source files and create .bak backups."
|
|
1390
|
+
}
|
|
1391
|
+
],
|
|
1392
|
+
examples: [
|
|
1393
|
+
"cnos migrate",
|
|
1394
|
+
"cnos migrate --scan src --dry-run",
|
|
1395
|
+
"cnos migrate --scan apps/api/src --apply",
|
|
1396
|
+
"cnos migrate --apply --rewrite"
|
|
1397
|
+
]
|
|
1398
|
+
},
|
|
1313
1399
|
{
|
|
1314
1400
|
id: "help",
|
|
1315
1401
|
summary: "Show human-readable CLI help.",
|
|
@@ -1500,11 +1586,11 @@ function runHelpAi(topic, cliArgs = []) {
|
|
|
1500
1586
|
}
|
|
1501
1587
|
|
|
1502
1588
|
// src/commands/init.ts
|
|
1503
|
-
import
|
|
1589
|
+
import path5 from "path";
|
|
1504
1590
|
|
|
1505
1591
|
// src/services/scaffold.ts
|
|
1506
|
-
import { mkdir as mkdir3, readFile as
|
|
1507
|
-
import
|
|
1592
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1593
|
+
import path4 from "path";
|
|
1508
1594
|
function scaffoldManifest(projectName, workspace) {
|
|
1509
1595
|
const lines = [
|
|
1510
1596
|
"version: 1",
|
|
@@ -1535,15 +1621,15 @@ function scaffoldManifest(projectName, workspace) {
|
|
|
1535
1621
|
}
|
|
1536
1622
|
async function ensureFile(filePath, content) {
|
|
1537
1623
|
try {
|
|
1538
|
-
await
|
|
1624
|
+
await readFile3(filePath, "utf8");
|
|
1539
1625
|
return false;
|
|
1540
1626
|
} catch {
|
|
1541
|
-
await
|
|
1627
|
+
await writeFile3(filePath, content, "utf8");
|
|
1542
1628
|
return true;
|
|
1543
1629
|
}
|
|
1544
1630
|
}
|
|
1545
1631
|
async function ensureGitignore(root) {
|
|
1546
|
-
const gitignorePath =
|
|
1632
|
+
const gitignorePath = path4.join(root, ".gitignore");
|
|
1547
1633
|
const requiredEntries = [
|
|
1548
1634
|
".cnos/env/.env",
|
|
1549
1635
|
".cnos/env/.env.*",
|
|
@@ -1556,7 +1642,7 @@ async function ensureGitignore(root) {
|
|
|
1556
1642
|
];
|
|
1557
1643
|
let current = "";
|
|
1558
1644
|
try {
|
|
1559
|
-
current = await
|
|
1645
|
+
current = await readFile3(gitignorePath, "utf8");
|
|
1560
1646
|
} catch {
|
|
1561
1647
|
current = "";
|
|
1562
1648
|
}
|
|
@@ -1566,18 +1652,18 @@ async function ensureGitignore(root) {
|
|
|
1566
1652
|
}
|
|
1567
1653
|
const prefix = current.trim().length > 0 ? `${current.trimEnd()}
|
|
1568
1654
|
` : "";
|
|
1569
|
-
await
|
|
1655
|
+
await writeFile3(gitignorePath, `${prefix}${missingEntries.join("\n")}
|
|
1570
1656
|
`, "utf8");
|
|
1571
1657
|
return true;
|
|
1572
1658
|
}
|
|
1573
1659
|
async function scaffoldWorkspace(root, workspace) {
|
|
1574
|
-
const cnosRoot =
|
|
1575
|
-
const workspaceRoot = workspace ?
|
|
1660
|
+
const cnosRoot = path4.join(root, ".cnos");
|
|
1661
|
+
const workspaceRoot = workspace ? path4.join(cnosRoot, "workspaces", workspace) : cnosRoot;
|
|
1576
1662
|
const createdPaths = [];
|
|
1577
|
-
await mkdir3(
|
|
1578
|
-
await mkdir3(
|
|
1579
|
-
await mkdir3(
|
|
1580
|
-
await mkdir3(
|
|
1663
|
+
await mkdir3(path4.join(workspaceRoot, "profiles"), { recursive: true });
|
|
1664
|
+
await mkdir3(path4.join(workspaceRoot, "values"), { recursive: true });
|
|
1665
|
+
await mkdir3(path4.join(workspaceRoot, "secrets"), { recursive: true });
|
|
1666
|
+
await mkdir3(path4.join(workspaceRoot, "env"), { recursive: true });
|
|
1581
1667
|
const relativePaths = workspace ? [
|
|
1582
1668
|
["workspaces", workspace, "profiles", ".gitkeep"],
|
|
1583
1669
|
["workspaces", workspace, "values", ".gitkeep"],
|
|
@@ -1590,15 +1676,15 @@ async function scaffoldWorkspace(root, workspace) {
|
|
|
1590
1676
|
["env", ".gitkeep"]
|
|
1591
1677
|
];
|
|
1592
1678
|
for (const relativePath of relativePaths) {
|
|
1593
|
-
const filePath =
|
|
1679
|
+
const filePath = path4.join(cnosRoot, ...relativePath);
|
|
1594
1680
|
if (await ensureFile(filePath, "")) {
|
|
1595
|
-
createdPaths.push(
|
|
1681
|
+
createdPaths.push(path4.relative(root, filePath).replace(/\\/g, "/"));
|
|
1596
1682
|
}
|
|
1597
1683
|
}
|
|
1598
|
-
if (await ensureFile(
|
|
1684
|
+
if (await ensureFile(path4.join(cnosRoot, "cnos.yml"), scaffoldManifest(path4.basename(root), workspace))) {
|
|
1599
1685
|
createdPaths.push(".cnos/cnos.yml");
|
|
1600
1686
|
}
|
|
1601
|
-
if (workspace && await ensureFile(
|
|
1687
|
+
if (workspace && await ensureFile(path4.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
|
|
1602
1688
|
globalRoot: ~/.cnos
|
|
1603
1689
|
`)) {
|
|
1604
1690
|
createdPaths.push(".cnos-workspace.yml");
|
|
@@ -1615,7 +1701,7 @@ globalRoot: ~/.cnos
|
|
|
1615
1701
|
|
|
1616
1702
|
// src/commands/init.ts
|
|
1617
1703
|
async function runInit(options = {}) {
|
|
1618
|
-
const root =
|
|
1704
|
+
const root = path5.resolve(options.root ?? process.cwd());
|
|
1619
1705
|
const result = await scaffoldWorkspace(root, options.workspace);
|
|
1620
1706
|
if (options.json) {
|
|
1621
1707
|
return printJson(result);
|
|
@@ -1626,6 +1712,12 @@ async function runInit(options = {}) {
|
|
|
1626
1712
|
return `initialized CNOS project at ${root}`;
|
|
1627
1713
|
}
|
|
1628
1714
|
|
|
1715
|
+
// src/format/maskSecret.ts
|
|
1716
|
+
var MASKED_SECRET_VALUE = "****";
|
|
1717
|
+
function maskSecretValue(value) {
|
|
1718
|
+
return value === void 0 ? "" : MASKED_SECRET_VALUE;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1629
1721
|
// src/format/printInspect.ts
|
|
1630
1722
|
function printInspect(record) {
|
|
1631
1723
|
const lines = [
|
|
@@ -1650,12 +1742,22 @@ function printInspect(record) {
|
|
|
1650
1742
|
|
|
1651
1743
|
// src/commands/inspect.ts
|
|
1652
1744
|
async function runInspect(key, options = {}) {
|
|
1745
|
+
const reveal = options.cliArgs?.includes("--reveal") ?? false;
|
|
1653
1746
|
const runtime = await createRuntimeService(options);
|
|
1654
1747
|
const inspectResult = runtime.inspect(key);
|
|
1748
|
+
const value = key.startsWith("secret.") && !reveal ? maskSecretValue(inspectResult.value) : inspectResult.value;
|
|
1749
|
+
const printable = {
|
|
1750
|
+
...inspectResult,
|
|
1751
|
+
value,
|
|
1752
|
+
overridden: inspectResult.overridden.map((entry) => ({
|
|
1753
|
+
...entry,
|
|
1754
|
+
value: key.startsWith("secret.") && !reveal ? maskSecretValue(entry.value) : entry.value
|
|
1755
|
+
}))
|
|
1756
|
+
};
|
|
1655
1757
|
if (options.json) {
|
|
1656
|
-
return printJson(
|
|
1758
|
+
return printJson(printable);
|
|
1657
1759
|
}
|
|
1658
|
-
return printInspect(
|
|
1760
|
+
return printInspect(printable);
|
|
1659
1761
|
}
|
|
1660
1762
|
|
|
1661
1763
|
// src/format/printValue.ts
|
|
@@ -1784,12 +1886,92 @@ async function runList(args = [], options = {}) {
|
|
|
1784
1886
|
return entries.map((entry) => `${entry.key}=${printValue(entry.value)}`).join("\n");
|
|
1785
1887
|
}
|
|
1786
1888
|
|
|
1889
|
+
// src/commands/migrate.ts
|
|
1890
|
+
import path6 from "path";
|
|
1891
|
+
import {
|
|
1892
|
+
applyManifestMappings,
|
|
1893
|
+
loadManifest,
|
|
1894
|
+
proposeMapping,
|
|
1895
|
+
rewriteSourceFiles,
|
|
1896
|
+
scanEnvUsage
|
|
1897
|
+
} from "@kitsy/cnos/internal";
|
|
1898
|
+
async function runMigrate(options = {}) {
|
|
1899
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
1900
|
+
const scan = consumeOption(cliArgs, "--scan");
|
|
1901
|
+
const apply = consumeFlag(cliArgs, "--apply");
|
|
1902
|
+
const dryRun = consumeFlag(cliArgs, "--dry-run") || !apply;
|
|
1903
|
+
const rewrite = consumeFlag(cliArgs, "--rewrite");
|
|
1904
|
+
if (cliArgs.length > 0) {
|
|
1905
|
+
throw new Error(`Unknown migrate options: ${cliArgs.join(" ")}`);
|
|
1906
|
+
}
|
|
1907
|
+
const manifest = await loadManifest(options.root ? { root: options.root } : {});
|
|
1908
|
+
const scanRoot = path6.resolve(manifest.repoRoot, scan ?? "src");
|
|
1909
|
+
const usages = await scanEnvUsage(scanRoot);
|
|
1910
|
+
const uniqueProposals = new Map(usages.map((usage) => [usage.envVar, proposeMapping(usage.envVar)]));
|
|
1911
|
+
const proposals = Array.from(uniqueProposals.values()).sort((left, right) => left.envVar.localeCompare(right.envVar));
|
|
1912
|
+
let manifestResult;
|
|
1913
|
+
let rewriteResult;
|
|
1914
|
+
if (apply) {
|
|
1915
|
+
manifestResult = await applyManifestMappings(proposals, options.root);
|
|
1916
|
+
if (rewrite) {
|
|
1917
|
+
rewriteResult = await rewriteSourceFiles(usages.filter((usage) => usage.kind === "process-env"), uniqueProposals);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
if (options.json) {
|
|
1921
|
+
return printJson({
|
|
1922
|
+
scanRoot,
|
|
1923
|
+
dryRun,
|
|
1924
|
+
apply,
|
|
1925
|
+
rewrite,
|
|
1926
|
+
usages,
|
|
1927
|
+
proposals,
|
|
1928
|
+
...manifestResult ? { manifest: manifestResult } : {},
|
|
1929
|
+
...rewriteResult ? { rewriteResult } : {}
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
const lines = [
|
|
1933
|
+
`Scanned ${usages.length} env usage${usages.length === 1 ? "" : "s"} in ${scanRoot}`,
|
|
1934
|
+
"",
|
|
1935
|
+
"Proposed mappings:",
|
|
1936
|
+
...proposals.map(
|
|
1937
|
+
(proposal) => ` ${proposal.envVar} -> ${proposal.logicalKey}${proposal.public ? " (promote to public)" : ""}`
|
|
1938
|
+
)
|
|
1939
|
+
];
|
|
1940
|
+
if (proposals.length === 0) {
|
|
1941
|
+
lines.push(" none");
|
|
1942
|
+
}
|
|
1943
|
+
if (dryRun) {
|
|
1944
|
+
lines.push("", "Dry run only. Re-run with --apply to update the manifest.");
|
|
1945
|
+
}
|
|
1946
|
+
if (manifestResult) {
|
|
1947
|
+
lines.push(
|
|
1948
|
+
"",
|
|
1949
|
+
`Updated ${manifestResult.manifestPath} with ${manifestResult.appliedMappings} env mapping${manifestResult.appliedMappings === 1 ? "" : "s"} and ${manifestResult.appliedPromotions} public promotion${manifestResult.appliedPromotions === 1 ? "" : "s"}.`
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
if (rewrite) {
|
|
1953
|
+
if (!apply) {
|
|
1954
|
+
lines.push("", "Source rewrite requested but skipped because --apply was not set.");
|
|
1955
|
+
} else if (rewriteResult) {
|
|
1956
|
+
lines.push(
|
|
1957
|
+
"",
|
|
1958
|
+
`Rewrote ${rewriteResult.rewrittenFiles.length} file${rewriteResult.rewrittenFiles.length === 1 ? "" : "s"} and created ${rewriteResult.backupFiles.length} backup${rewriteResult.backupFiles.length === 1 ? "" : "s"}.`
|
|
1959
|
+
);
|
|
1960
|
+
if (rewriteResult.skippedUsages.length > 0) {
|
|
1961
|
+
lines.push("Skipped usages:");
|
|
1962
|
+
lines.push(...rewriteResult.skippedUsages.map((entry) => ` ${entry}`));
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return lines.join("\n");
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1787
1969
|
// src/commands/onboard.ts
|
|
1788
|
-
import { copyFile, readdir, rm
|
|
1970
|
+
import { copyFile, readdir as readdir2, rm } from "fs/promises";
|
|
1789
1971
|
import path7 from "path";
|
|
1790
1972
|
var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
|
|
1791
1973
|
async function listRootEnvFiles(root) {
|
|
1792
|
-
const entries = await
|
|
1974
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
1793
1975
|
return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
1794
1976
|
}
|
|
1795
1977
|
async function runOnboard(options = {}) {
|
|
@@ -1812,7 +1994,7 @@ async function runOnboard(options = {}) {
|
|
|
1812
1994
|
await copyFile(sourcePath, targetPath);
|
|
1813
1995
|
imported.push(path7.relative(root, targetPath).replace(/\\/g, "/"));
|
|
1814
1996
|
if (move) {
|
|
1815
|
-
await
|
|
1997
|
+
await rm(sourcePath);
|
|
1816
1998
|
}
|
|
1817
1999
|
} catch {
|
|
1818
2000
|
skipped.push(fileName);
|
|
@@ -1838,14 +2020,14 @@ async function runOnboard(options = {}) {
|
|
|
1838
2020
|
import path10 from "path";
|
|
1839
2021
|
|
|
1840
2022
|
// src/services/context.ts
|
|
1841
|
-
import { readFile as
|
|
2023
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
1842
2024
|
import path8 from "path";
|
|
1843
|
-
import { parseYaml as
|
|
2025
|
+
import { parseYaml as parseYaml3, stringifyYaml as stringifyYaml2 } from "@kitsy/cnos/internal";
|
|
1844
2026
|
async function loadCliContext(root = process.cwd()) {
|
|
1845
2027
|
const filePath = path8.join(path8.resolve(root), ".cnos-workspace.yml");
|
|
1846
2028
|
try {
|
|
1847
|
-
const source = await
|
|
1848
|
-
const parsed =
|
|
2029
|
+
const source = await readFile4(filePath, "utf8");
|
|
2030
|
+
const parsed = parseYaml3(source);
|
|
1849
2031
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1850
2032
|
return {};
|
|
1851
2033
|
}
|
|
@@ -1866,7 +2048,7 @@ async function saveCliContext(options = {}) {
|
|
|
1866
2048
|
...options.profile ? { profile: options.profile } : {},
|
|
1867
2049
|
...options.globalRoot ? { globalRoot: options.globalRoot } : {}
|
|
1868
2050
|
};
|
|
1869
|
-
await
|
|
2051
|
+
await writeFile4(filePath, stringifyYaml2(next), "utf8");
|
|
1870
2052
|
return {
|
|
1871
2053
|
filePath,
|
|
1872
2054
|
context: next
|
|
@@ -1874,9 +2056,9 @@ async function saveCliContext(options = {}) {
|
|
|
1874
2056
|
}
|
|
1875
2057
|
|
|
1876
2058
|
// src/services/profiles.ts
|
|
1877
|
-
import { mkdir as mkdir4, readdir as
|
|
2059
|
+
import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
1878
2060
|
import path9 from "path";
|
|
1879
|
-
import { parseYaml as
|
|
2061
|
+
import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
|
|
1880
2062
|
async function createProfileDefinition(root = process.cwd(), profile, inherit) {
|
|
1881
2063
|
const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
|
|
1882
2064
|
await mkdir4(path9.dirname(filePath), { recursive: true });
|
|
@@ -1886,7 +2068,7 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit) {
|
|
|
1886
2068
|
} : {
|
|
1887
2069
|
name: profile
|
|
1888
2070
|
};
|
|
1889
|
-
await
|
|
2071
|
+
await writeFile5(filePath, stringifyYaml3(document), "utf8");
|
|
1890
2072
|
return {
|
|
1891
2073
|
filePath,
|
|
1892
2074
|
profile,
|
|
@@ -1896,7 +2078,7 @@ async function createProfileDefinition(root = process.cwd(), profile, inherit) {
|
|
|
1896
2078
|
async function listProfiles(root = process.cwd()) {
|
|
1897
2079
|
const profilesRoot = path9.join(path9.resolve(root), ".cnos", "profiles");
|
|
1898
2080
|
try {
|
|
1899
|
-
const entries = await
|
|
2081
|
+
const entries = await readdir3(profilesRoot, { withFileTypes: true });
|
|
1900
2082
|
const discovered = /* @__PURE__ */ new Set(["base"]);
|
|
1901
2083
|
for (const entry of entries) {
|
|
1902
2084
|
if (entry.isFile() && entry.name.endsWith(".yml")) {
|
|
@@ -1911,7 +2093,7 @@ async function listProfiles(root = process.cwd()) {
|
|
|
1911
2093
|
async function deleteProfileDefinition(root = process.cwd(), profile) {
|
|
1912
2094
|
const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
|
|
1913
2095
|
try {
|
|
1914
|
-
await
|
|
2096
|
+
await rm2(filePath);
|
|
1915
2097
|
return {
|
|
1916
2098
|
filePath,
|
|
1917
2099
|
deleted: true
|
|
@@ -1931,7 +2113,7 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
|
|
|
1931
2113
|
}
|
|
1932
2114
|
const filePath = path9.join(path9.resolve(root), ".cnos", "profiles", `${profile}.yml`);
|
|
1933
2115
|
try {
|
|
1934
|
-
return
|
|
2116
|
+
return parseYaml4(await readFile5(filePath, "utf8")) ?? void 0;
|
|
1935
2117
|
} catch {
|
|
1936
2118
|
return void 0;
|
|
1937
2119
|
}
|
|
@@ -1997,11 +2179,11 @@ async function runProfile(args, options = {}) {
|
|
|
1997
2179
|
}
|
|
1998
2180
|
|
|
1999
2181
|
// src/commands/promote.ts
|
|
2000
|
-
import { writeFile as
|
|
2182
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
2001
2183
|
import {
|
|
2002
2184
|
ensureProjectionAllowed,
|
|
2003
2185
|
loadManifest as loadManifest2,
|
|
2004
|
-
stringifyYaml as
|
|
2186
|
+
stringifyYaml as stringifyYaml4
|
|
2005
2187
|
} from "@kitsy/cnos/internal";
|
|
2006
2188
|
function normalizeTarget(value) {
|
|
2007
2189
|
if (value === "public" || value === "env") {
|
|
@@ -2051,7 +2233,7 @@ async function runPromote(args = [], options = {}) {
|
|
|
2051
2233
|
})
|
|
2052
2234
|
};
|
|
2053
2235
|
}
|
|
2054
|
-
await
|
|
2236
|
+
await writeFile6(loadedManifest.manifestPath, stringifyYaml4(rawManifest), "utf8");
|
|
2055
2237
|
if (options.json) {
|
|
2056
2238
|
return printJson({
|
|
2057
2239
|
target,
|
|
@@ -2070,16 +2252,21 @@ async function runRead(key, options = {}) {
|
|
|
2070
2252
|
if (value === void 0) {
|
|
2071
2253
|
throw new Error(`Missing CNOS config key: ${key}`);
|
|
2072
2254
|
}
|
|
2255
|
+
const isSecret = key.startsWith("secret.");
|
|
2256
|
+
const valueForOutput = isSecret ? maskSecretValue(value) : value;
|
|
2073
2257
|
if (options.json) {
|
|
2074
|
-
return printJson({ key, value });
|
|
2258
|
+
return printJson({ key, value: valueForOutput });
|
|
2075
2259
|
}
|
|
2076
|
-
return printValue(
|
|
2260
|
+
return printValue(valueForOutput);
|
|
2077
2261
|
}
|
|
2078
2262
|
|
|
2079
2263
|
// src/commands/run.ts
|
|
2080
2264
|
import { spawn } from "child_process";
|
|
2081
2265
|
import {
|
|
2082
2266
|
CNOS_GRAPH_ENV_VAR,
|
|
2267
|
+
CNOS_SECRET_PAYLOAD_ENV_VAR,
|
|
2268
|
+
CNOS_SESSION_KEY_ENV_VAR,
|
|
2269
|
+
serializeSecretPayload,
|
|
2083
2270
|
serializeRuntimeGraph
|
|
2084
2271
|
} from "@kitsy/cnos/internal";
|
|
2085
2272
|
function consumeOptions(args, flag) {
|
|
@@ -2118,6 +2305,7 @@ async function runCommand(command, options = {}) {
|
|
|
2118
2305
|
}
|
|
2119
2306
|
const cliArgs = [...options.cliArgs ?? []];
|
|
2120
2307
|
const isPublic = consumeFlag(cliArgs, "--public");
|
|
2308
|
+
const isAuthenticated = consumeFlag(cliArgs, "--auth");
|
|
2121
2309
|
const framework = consumeOption(cliArgs, "--framework");
|
|
2122
2310
|
const prefix = consumeOption(cliArgs, "--prefix");
|
|
2123
2311
|
const setOverrides = normalizeSetOverrides(consumeOptions(cliArgs, "--set"));
|
|
@@ -2125,13 +2313,21 @@ async function runCommand(command, options = {}) {
|
|
|
2125
2313
|
...options,
|
|
2126
2314
|
cliArgs: [...cliArgs, ...setOverrides]
|
|
2127
2315
|
});
|
|
2316
|
+
const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
|
|
2317
|
+
Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
|
|
2318
|
+
) : void 0;
|
|
2319
|
+
const secretPayload = authenticatedSecrets ? serializeSecretPayload(authenticatedSecrets) : void 0;
|
|
2128
2320
|
const env = {
|
|
2129
2321
|
...process.env,
|
|
2130
2322
|
...isPublic ? runtime.toPublicEnv({
|
|
2131
2323
|
...framework ? { framework } : {},
|
|
2132
2324
|
...prefix ? { prefix } : {}
|
|
2133
2325
|
}) : runtime.toEnv(),
|
|
2134
|
-
[CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph)
|
|
2326
|
+
[CNOS_GRAPH_ENV_VAR]: serializeRuntimeGraph(runtime.graph),
|
|
2327
|
+
...secretPayload ? {
|
|
2328
|
+
[CNOS_SECRET_PAYLOAD_ENV_VAR]: secretPayload.payload,
|
|
2329
|
+
[CNOS_SESSION_KEY_ENV_VAR]: secretPayload.sessionKey
|
|
2330
|
+
} : {}
|
|
2135
2331
|
};
|
|
2136
2332
|
return new Promise((resolve, reject) => {
|
|
2137
2333
|
const executable = command[0];
|
|
@@ -2166,12 +2362,174 @@ async function runCommand(command, options = {}) {
|
|
|
2166
2362
|
});
|
|
2167
2363
|
}
|
|
2168
2364
|
|
|
2365
|
+
// src/services/vaults.ts
|
|
2366
|
+
import { rm as rm3, writeFile as writeFile7 } from "fs/promises";
|
|
2367
|
+
import path11 from "path";
|
|
2368
|
+
import {
|
|
2369
|
+
clearAllVaultSessionKeys,
|
|
2370
|
+
clearVaultSessionKey,
|
|
2371
|
+
createSecretVault,
|
|
2372
|
+
deriveVaultKey,
|
|
2373
|
+
loadManifest as loadManifest3,
|
|
2374
|
+
listSecretVaults,
|
|
2375
|
+
readVaultMetadata,
|
|
2376
|
+
resolveSecretStoreRoot as resolveSecretStoreRoot2,
|
|
2377
|
+
resolveVaultAuth as resolveVaultAuth2,
|
|
2378
|
+
resolveVaultDefinition,
|
|
2379
|
+
stringifyYaml as stringifyYaml5,
|
|
2380
|
+
writeKeychain,
|
|
2381
|
+
writeVaultSessionKey
|
|
2382
|
+
} from "@kitsy/cnos/internal";
|
|
2383
|
+
function sortVaults(vaults) {
|
|
2384
|
+
return Object.fromEntries(Object.entries(vaults).sort(([left], [right]) => left.localeCompare(right)));
|
|
2385
|
+
}
|
|
2386
|
+
function defaultLocalAuthSources(vault) {
|
|
2387
|
+
const token = vault.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
2388
|
+
return [`env:CNOS_SECRET_PASSPHRASE_${token}`, "env:CNOS_SECRET_PASSPHRASE", `keychain:cnos/${vault}`, "prompt"];
|
|
2389
|
+
}
|
|
2390
|
+
async function createVaultDefinition(name, options = {}) {
|
|
2391
|
+
const vault = name.trim() || "default";
|
|
2392
|
+
const provider = options.provider?.trim() || "local";
|
|
2393
|
+
if (provider === "local" && (options.noPassphrase ?? false)) {
|
|
2394
|
+
throw new Error("Local vaults cannot be passwordless.");
|
|
2395
|
+
}
|
|
2396
|
+
const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
|
|
2397
|
+
const rawManifest = {
|
|
2398
|
+
...loadedManifest.rawManifest,
|
|
2399
|
+
vaults: {
|
|
2400
|
+
...loadedManifest.rawManifest.vaults ?? {},
|
|
2401
|
+
[vault]: provider === "local" ? {
|
|
2402
|
+
provider: "local",
|
|
2403
|
+
auth: {
|
|
2404
|
+
passphrase: {
|
|
2405
|
+
from: defaultLocalAuthSources(vault)
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
} : {
|
|
2409
|
+
provider,
|
|
2410
|
+
auth: {
|
|
2411
|
+
method: "environment"
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
|
|
2417
|
+
const definition = resolveVaultDefinition({ [vault]: rawManifest.vaults[vault] }, vault);
|
|
2418
|
+
return {
|
|
2419
|
+
...definition,
|
|
2420
|
+
authMethod: definition.auth?.method ?? (provider === "local" ? "passphrase" : "environment"),
|
|
2421
|
+
localStore: provider === "local",
|
|
2422
|
+
manifestPath: loadedManifest.manifestPath
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
async function listVaultDefinitions(options = {}) {
|
|
2426
|
+
const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
|
|
2427
|
+
const localStoreVaults = await listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
|
|
2428
|
+
return Object.keys(loadedManifest.manifest.vaults).sort((left, right) => left.localeCompare(right)).map((name) => {
|
|
2429
|
+
const definition = resolveVaultDefinition(loadedManifest.manifest.vaults, name);
|
|
2430
|
+
return {
|
|
2431
|
+
...definition,
|
|
2432
|
+
authMethod: definition.auth?.method ?? (definition.provider === "local" ? "passphrase" : "environment"),
|
|
2433
|
+
localStore: localStoreVaults.includes(name)
|
|
2434
|
+
};
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
async function removeVaultDefinition(name, options = {}) {
|
|
2438
|
+
const vault = name.trim() || "default";
|
|
2439
|
+
const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
|
|
2440
|
+
if (!loadedManifest.rawManifest.vaults?.[vault]) {
|
|
2441
|
+
return {
|
|
2442
|
+
name: vault,
|
|
2443
|
+
deleted: false,
|
|
2444
|
+
manifestPath: loadedManifest.manifestPath
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
const nextVaults = { ...loadedManifest.rawManifest.vaults ?? {} };
|
|
2448
|
+
delete nextVaults[vault];
|
|
2449
|
+
const rawManifest = {
|
|
2450
|
+
...loadedManifest.rawManifest,
|
|
2451
|
+
...Object.keys(nextVaults).length > 0 ? { vaults: sortVaults(nextVaults) } : {}
|
|
2452
|
+
};
|
|
2453
|
+
if (Object.keys(nextVaults).length === 0) {
|
|
2454
|
+
delete rawManifest.vaults;
|
|
2455
|
+
}
|
|
2456
|
+
await writeFile7(loadedManifest.manifestPath, stringifyYaml5(rawManifest), "utf8");
|
|
2457
|
+
const vaultRoot = path11.join(resolveSecretStoreRoot2(options.processEnv), "vaults", vault);
|
|
2458
|
+
let removedStore;
|
|
2459
|
+
try {
|
|
2460
|
+
await rm3(vaultRoot, { recursive: true, force: true });
|
|
2461
|
+
removedStore = vaultRoot;
|
|
2462
|
+
} catch {
|
|
2463
|
+
removedStore = void 0;
|
|
2464
|
+
}
|
|
2465
|
+
await clearVaultSessionKey(vault, options.processEnv);
|
|
2466
|
+
return {
|
|
2467
|
+
name: vault,
|
|
2468
|
+
deleted: true,
|
|
2469
|
+
manifestPath: loadedManifest.manifestPath,
|
|
2470
|
+
...removedStore ? { removedStore } : {}
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
async function listLocalStoreVaults(options = {}) {
|
|
2474
|
+
return listSecretVaults(resolveSecretStoreRoot2(options.processEnv));
|
|
2475
|
+
}
|
|
2476
|
+
async function authenticateVault(name, options = {}) {
|
|
2477
|
+
const vault = name.trim() || "default";
|
|
2478
|
+
const loadedManifest = await loadManifest3(options.root ? { root: options.root } : {});
|
|
2479
|
+
const definition = loadedManifest.manifest.vaults[vault];
|
|
2480
|
+
if (!definition) {
|
|
2481
|
+
throw new Error(`Unknown vault "${vault}"`);
|
|
2482
|
+
}
|
|
2483
|
+
const auth = await resolveVaultAuth2(vault, definition, options.processEnv ?? process.env);
|
|
2484
|
+
const storeRoot = resolveSecretStoreRoot2(options.processEnv);
|
|
2485
|
+
if (definition.provider === "local") {
|
|
2486
|
+
if (!auth.passphrase) {
|
|
2487
|
+
throw new Error(`Vault "${vault}" requires passphrase-based authentication.`);
|
|
2488
|
+
}
|
|
2489
|
+
const existing = await readVaultMetadata(storeRoot, vault);
|
|
2490
|
+
if (!existing) {
|
|
2491
|
+
await createSecretVault(storeRoot, vault, auth.passphrase);
|
|
2492
|
+
}
|
|
2493
|
+
const metadata = await readVaultMetadata(storeRoot, vault);
|
|
2494
|
+
if (!metadata) {
|
|
2495
|
+
throw new Error(`Failed to initialize vault "${vault}"`);
|
|
2496
|
+
}
|
|
2497
|
+
const derivedKey = deriveVaultKey(auth.passphrase, Buffer.from(metadata.salt, "base64"), metadata.iterations);
|
|
2498
|
+
const sessionPath2 = await writeVaultSessionKey(vault, derivedKey, options.processEnv);
|
|
2499
|
+
if (options.storeKeychain) {
|
|
2500
|
+
await writeKeychain(`cnos/${vault}`, derivedKey.toString("hex"));
|
|
2501
|
+
}
|
|
2502
|
+
return {
|
|
2503
|
+
name: vault,
|
|
2504
|
+
method: auth.method,
|
|
2505
|
+
storedInKeychain: options.storeKeychain ?? false,
|
|
2506
|
+
sessionPath: sessionPath2
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
const sessionPath = await writeVaultSessionKey(vault, Buffer.from(vault, "utf8"), options.processEnv);
|
|
2510
|
+
return {
|
|
2511
|
+
name: vault,
|
|
2512
|
+
method: auth.method,
|
|
2513
|
+
storedInKeychain: false,
|
|
2514
|
+
sessionPath
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
async function logoutVault(name, options = {}) {
|
|
2518
|
+
if (options.all) {
|
|
2519
|
+
await clearAllVaultSessionKeys(options.processEnv);
|
|
2520
|
+
return { scope: "all" };
|
|
2521
|
+
}
|
|
2522
|
+
const vault = name?.trim() || "default";
|
|
2523
|
+
await clearVaultSessionKey(vault, options.processEnv);
|
|
2524
|
+
return { scope: vault };
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2169
2527
|
// src/commands/vault.ts
|
|
2170
2528
|
function normalizeVaultAction(args) {
|
|
2171
2529
|
const [action = "list", ...tail] = args;
|
|
2172
|
-
if (["create", "add", "list", "delete", "remove"].includes(action)) {
|
|
2530
|
+
if (["create", "add", "list", "delete", "remove", "auth", "logout"].includes(action)) {
|
|
2173
2531
|
return {
|
|
2174
|
-
action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : "list",
|
|
2532
|
+
action: action === "add" || action === "create" ? "create" : action === "delete" || action === "remove" ? "remove" : action === "auth" ? "auth" : action === "logout" ? "logout" : "list",
|
|
2175
2533
|
tail
|
|
2176
2534
|
};
|
|
2177
2535
|
}
|
|
@@ -2183,16 +2541,17 @@ function normalizeVaultAction(args) {
|
|
|
2183
2541
|
async function runVault(args = [], options = {}) {
|
|
2184
2542
|
const { action, tail } = normalizeVaultAction(args);
|
|
2185
2543
|
const cliArgs = [...options.cliArgs ?? []];
|
|
2544
|
+
if (consumeOption(cliArgs, "--passphrase")) {
|
|
2545
|
+
throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
|
|
2546
|
+
}
|
|
2186
2547
|
if (action === "create") {
|
|
2187
2548
|
const name = tail[0] ?? "default";
|
|
2188
2549
|
const provider = consumeOption(cliArgs, "--provider") ?? "local";
|
|
2189
|
-
const passphrase = consumeOption(cliArgs, "--passphrase");
|
|
2190
2550
|
const noPassphrase = consumeFlag(cliArgs, "--no-passphrase");
|
|
2191
2551
|
const result = await createVaultDefinition(name, {
|
|
2192
2552
|
...options,
|
|
2193
2553
|
cliArgs,
|
|
2194
2554
|
provider,
|
|
2195
|
-
...passphrase ? { passphrase } : {},
|
|
2196
2555
|
...noPassphrase ? { noPassphrase: true } : {}
|
|
2197
2556
|
});
|
|
2198
2557
|
if (options.json) {
|
|
@@ -2200,6 +2559,28 @@ async function runVault(args = [], options = {}) {
|
|
|
2200
2559
|
}
|
|
2201
2560
|
return `created vault "${result.name}" with provider "${result.provider}" in ${result.manifestPath}`;
|
|
2202
2561
|
}
|
|
2562
|
+
if (action === "auth") {
|
|
2563
|
+
const result = await authenticateVault(tail[0] ?? "default", {
|
|
2564
|
+
...options,
|
|
2565
|
+
cliArgs,
|
|
2566
|
+
storeKeychain: consumeFlag(cliArgs, "--store-keychain")
|
|
2567
|
+
});
|
|
2568
|
+
if (options.json) {
|
|
2569
|
+
return printJson(result);
|
|
2570
|
+
}
|
|
2571
|
+
return `authenticated vault "${result.name}" via ${result.method}`;
|
|
2572
|
+
}
|
|
2573
|
+
if (action === "logout") {
|
|
2574
|
+
const result = await logoutVault(tail[0], {
|
|
2575
|
+
...options,
|
|
2576
|
+
cliArgs,
|
|
2577
|
+
all: consumeFlag(cliArgs, "--all")
|
|
2578
|
+
});
|
|
2579
|
+
if (options.json) {
|
|
2580
|
+
return printJson(result);
|
|
2581
|
+
}
|
|
2582
|
+
return result.scope === "all" ? "logged out all vault sessions" : `logged out vault "${result.scope}"`;
|
|
2583
|
+
}
|
|
2203
2584
|
if (action === "remove") {
|
|
2204
2585
|
const name = tail[0] ?? "default";
|
|
2205
2586
|
const result = await removeVaultDefinition(name, options);
|
|
@@ -2224,16 +2605,11 @@ async function runVault(args = [], options = {}) {
|
|
|
2224
2605
|
return "";
|
|
2225
2606
|
}
|
|
2226
2607
|
return manifestVaults.map(
|
|
2227
|
-
(vault) => `${vault.name} provider=${vault.provider}
|
|
2608
|
+
(vault) => `${vault.name} provider=${vault.provider} auth=${vault.authMethod}${localStoreVaults.includes(vault.name) ? " local-store=true" : ""}`
|
|
2228
2609
|
).join("\n");
|
|
2229
2610
|
}
|
|
2230
2611
|
|
|
2231
2612
|
// 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
2613
|
function normalizeSecretCommand(args) {
|
|
2238
2614
|
const [actionOrPath, next, ...tail] = args;
|
|
2239
2615
|
if (!actionOrPath) {
|
|
@@ -2259,48 +2635,68 @@ function normalizeSecretCommand(args) {
|
|
|
2259
2635
|
tail: args
|
|
2260
2636
|
};
|
|
2261
2637
|
}
|
|
2638
|
+
async function readStdinValue() {
|
|
2639
|
+
const chunks = [];
|
|
2640
|
+
for await (const chunk of process.stdin) {
|
|
2641
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2642
|
+
}
|
|
2643
|
+
return Buffer.concat(chunks).toString("utf8").trimEnd();
|
|
2644
|
+
}
|
|
2262
2645
|
async function runSecret(argsOrPath, options = {}) {
|
|
2263
2646
|
const args = Array.isArray(argsOrPath) ? argsOrPath : [argsOrPath];
|
|
2264
2647
|
const { action, tail } = normalizeSecretCommand(args);
|
|
2265
2648
|
const cliArgs = [...options.cliArgs ?? []];
|
|
2649
|
+
if (consumeOption(cliArgs, "--passphrase")) {
|
|
2650
|
+
throw new Error("The --passphrase option is not supported in CNOS 1.4. Use env, keychain, or prompt-based auth.");
|
|
2651
|
+
}
|
|
2266
2652
|
if (action === "create-vault") {
|
|
2267
2653
|
return runVault(["create", tail[0] ?? "default"], options);
|
|
2268
2654
|
}
|
|
2269
2655
|
if (action === "list") {
|
|
2656
|
+
const runtime2 = await createRuntimeService(options);
|
|
2270
2657
|
const prefix = consumeOption(cliArgs, "--prefix");
|
|
2271
2658
|
const vault = consumeOption(cliArgs, "--vault");
|
|
2272
2659
|
const provider = consumeOption(cliArgs, "--provider");
|
|
2273
|
-
const entries =
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2660
|
+
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) => {
|
|
2661
|
+
const secretRef2 = entry2.winner.metadata?.secretRef;
|
|
2662
|
+
if (vault && secretRef2?.vault !== vault) {
|
|
2663
|
+
return false;
|
|
2664
|
+
}
|
|
2665
|
+
if (provider && secretRef2?.provider !== provider) {
|
|
2666
|
+
return false;
|
|
2667
|
+
}
|
|
2668
|
+
return true;
|
|
2669
|
+
}).map((entry2) => {
|
|
2670
|
+
const secretRef2 = entry2.winner.metadata?.secretRef;
|
|
2671
|
+
return {
|
|
2672
|
+
key: entry2.key,
|
|
2673
|
+
vault: secretRef2?.vault ?? "default",
|
|
2674
|
+
provider: secretRef2?.provider ?? "local"
|
|
2675
|
+
};
|
|
2676
|
+
}).sort((left, right) => left.key.localeCompare(right.key));
|
|
2280
2677
|
if (options.json) {
|
|
2281
2678
|
return printJson(entries);
|
|
2282
2679
|
}
|
|
2283
|
-
return entries.map((entry2) => `${entry2.key}
|
|
2680
|
+
return entries.map((entry2) => `${entry2.key} (vault: ${entry2.vault}, provider: ${entry2.provider})`).join("\n");
|
|
2284
2681
|
}
|
|
2285
2682
|
if (action === "set") {
|
|
2286
2683
|
const secretPath2 = tail[0];
|
|
2287
|
-
const rawValue = tail[1] ?? "";
|
|
2288
2684
|
const local = consumeFlag(cliArgs, "--local");
|
|
2289
2685
|
const remote = consumeFlag(cliArgs, "--remote");
|
|
2290
2686
|
const ref = consumeFlag(cliArgs, "--ref");
|
|
2687
|
+
const stdin = consumeFlag(cliArgs, "--stdin");
|
|
2291
2688
|
const target = consumeOption(cliArgs, "--target") ?? "local";
|
|
2292
2689
|
const provider = consumeOption(cliArgs, "--provider");
|
|
2293
|
-
const passphrase = consumeOption(cliArgs, "--passphrase");
|
|
2294
2690
|
const vault = consumeOption(cliArgs, "--vault") ?? "default";
|
|
2295
2691
|
const mode = local ? "local" : remote ? "remote" : ref ? "ref" : void 0;
|
|
2692
|
+
const rawValue = stdin ? await readStdinValue() : tail[1] ?? "";
|
|
2296
2693
|
const result = await setSecret(secretPath2 ?? "app.token", rawValue, {
|
|
2297
2694
|
...options,
|
|
2298
2695
|
cliArgs,
|
|
2299
2696
|
target,
|
|
2300
2697
|
vault,
|
|
2301
2698
|
...mode ? { mode } : {},
|
|
2302
|
-
...provider ? { provider } : {}
|
|
2303
|
-
...passphrase ? { passphrase } : {}
|
|
2699
|
+
...provider ? { provider } : {}
|
|
2304
2700
|
});
|
|
2305
2701
|
if (options.json) {
|
|
2306
2702
|
return printJson(result);
|
|
@@ -2323,6 +2719,7 @@ async function runSecret(argsOrPath, options = {}) {
|
|
|
2323
2719
|
const runtime = await createRuntimeService(options);
|
|
2324
2720
|
const secretPath = tail[0] ?? "app.token";
|
|
2325
2721
|
const expectedVault = consumeOption(cliArgs, "--vault");
|
|
2722
|
+
const reveal = consumeFlag(cliArgs, "--reveal");
|
|
2326
2723
|
const entry = runtime.graph.entries.get(`secret.${secretPath}`);
|
|
2327
2724
|
const secretRef = entry?.winner.metadata?.secretRef;
|
|
2328
2725
|
const value = runtime.secret(secretPath);
|
|
@@ -2332,33 +2729,21 @@ async function runSecret(argsOrPath, options = {}) {
|
|
|
2332
2729
|
if (expectedVault && secretRef?.vault && secretRef.vault !== expectedVault) {
|
|
2333
2730
|
throw new Error(`Secret ${secretPath} belongs to vault "${secretRef.vault}", not "${expectedVault}"`);
|
|
2334
2731
|
}
|
|
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
|
-
}
|
|
2732
|
+
const valueForOutput = reveal ? value : maskSecretValue(value);
|
|
2349
2733
|
if (options.json) {
|
|
2350
2734
|
return printJson({
|
|
2351
2735
|
key: `secret.${secretPath}`,
|
|
2352
|
-
value
|
|
2736
|
+
value: valueForOutput,
|
|
2737
|
+
vault: secretRef?.vault ?? "default"
|
|
2353
2738
|
});
|
|
2354
2739
|
}
|
|
2355
|
-
return printValue(
|
|
2740
|
+
return printValue(valueForOutput);
|
|
2356
2741
|
}
|
|
2357
2742
|
|
|
2358
2743
|
// src/commands/use.ts
|
|
2359
|
-
import
|
|
2744
|
+
import path12 from "path";
|
|
2360
2745
|
async function runUse(args = [], options = {}) {
|
|
2361
|
-
const root =
|
|
2746
|
+
const root = path12.resolve(options.root ?? process.cwd());
|
|
2362
2747
|
const action = args[0] ?? "show";
|
|
2363
2748
|
const hasUpdates = Boolean(options.workspace || options.profile || options.globalRoot);
|
|
2364
2749
|
if (action === "show" || !hasUpdates) {
|
|
@@ -2395,7 +2780,7 @@ async function runValidate(options = {}) {
|
|
|
2395
2780
|
// package.json
|
|
2396
2781
|
var package_default = {
|
|
2397
2782
|
name: "@kitsy/cnos-cli",
|
|
2398
|
-
version: "1.
|
|
2783
|
+
version: "1.3.0",
|
|
2399
2784
|
description: "CLI entry point and developer tooling for CNOS.",
|
|
2400
2785
|
type: "module",
|
|
2401
2786
|
main: "./dist/index.js",
|
|
@@ -2532,6 +2917,180 @@ async function runValue(argsOrPath, options = {}) {
|
|
|
2532
2917
|
return printValue(value);
|
|
2533
2918
|
}
|
|
2534
2919
|
|
|
2920
|
+
// src/commands/watch.ts
|
|
2921
|
+
import { watch } from "fs";
|
|
2922
|
+
import { spawn as spawn2 } from "child_process";
|
|
2923
|
+
import {
|
|
2924
|
+
CNOS_GRAPH_ENV_VAR as CNOS_GRAPH_ENV_VAR2,
|
|
2925
|
+
CNOS_SECRET_PAYLOAD_ENV_VAR as CNOS_SECRET_PAYLOAD_ENV_VAR2,
|
|
2926
|
+
CNOS_SESSION_KEY_ENV_VAR as CNOS_SESSION_KEY_ENV_VAR2,
|
|
2927
|
+
diffGraphs,
|
|
2928
|
+
serializeRuntimeGraph as serializeRuntimeGraph2,
|
|
2929
|
+
serializeSecretPayload as serializeSecretPayload2,
|
|
2930
|
+
watchFiles
|
|
2931
|
+
} from "@kitsy/cnos/internal";
|
|
2932
|
+
async function buildRunEnvironment(options) {
|
|
2933
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
2934
|
+
const isPublic = consumeFlag(cliArgs, "--public");
|
|
2935
|
+
const isAuthenticated = consumeFlag(cliArgs, "--auth");
|
|
2936
|
+
const framework = consumeOption(cliArgs, "--framework");
|
|
2937
|
+
const prefix = consumeOption(cliArgs, "--prefix");
|
|
2938
|
+
const runtime = await createRuntimeService({
|
|
2939
|
+
...options,
|
|
2940
|
+
cliArgs
|
|
2941
|
+
});
|
|
2942
|
+
const authenticatedSecrets = isAuthenticated ? Object.fromEntries(
|
|
2943
|
+
Array.from(runtime.graph.entries.values()).filter((entry) => entry.namespace === "secret").map((entry) => [entry.key, runtime.read(entry.key)])
|
|
2944
|
+
) : void 0;
|
|
2945
|
+
const secretPayload = authenticatedSecrets ? serializeSecretPayload2(authenticatedSecrets) : void 0;
|
|
2946
|
+
return {
|
|
2947
|
+
runtime,
|
|
2948
|
+
env: {
|
|
2949
|
+
...process.env,
|
|
2950
|
+
...isPublic ? runtime.toPublicEnv({
|
|
2951
|
+
...framework ? { framework } : {},
|
|
2952
|
+
...prefix ? { prefix } : {}
|
|
2953
|
+
}) : runtime.toEnv(),
|
|
2954
|
+
[CNOS_GRAPH_ENV_VAR2]: serializeRuntimeGraph2(runtime.graph),
|
|
2955
|
+
...secretPayload ? {
|
|
2956
|
+
[CNOS_SECRET_PAYLOAD_ENV_VAR2]: secretPayload.payload,
|
|
2957
|
+
[CNOS_SESSION_KEY_ENV_VAR2]: secretPayload.sessionKey
|
|
2958
|
+
} : {}
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
function spawnWatchedChild(command, cwd, env) {
|
|
2963
|
+
const executable = command[0];
|
|
2964
|
+
if (!executable) {
|
|
2965
|
+
throw new Error("watch requires a command after -- unless --signal is used");
|
|
2966
|
+
}
|
|
2967
|
+
return spawn2(executable, command.slice(1), {
|
|
2968
|
+
cwd,
|
|
2969
|
+
env,
|
|
2970
|
+
stdio: "inherit",
|
|
2971
|
+
shell: false
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
async function startWatchLoop(options) {
|
|
2975
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
2976
|
+
const isSignal = consumeFlag(cliArgs, "--signal");
|
|
2977
|
+
const debounceMs = Number(consumeOption(cliArgs, "--debounce") ?? "300");
|
|
2978
|
+
const command = options.command ?? [];
|
|
2979
|
+
const root = options.root ?? process.cwd();
|
|
2980
|
+
let current = await buildRunEnvironment({
|
|
2981
|
+
...options,
|
|
2982
|
+
cliArgs
|
|
2983
|
+
});
|
|
2984
|
+
let child = !isSignal ? spawnWatchedChild(command, root, current.env) : void 0;
|
|
2985
|
+
const watcherMap = /* @__PURE__ */ new Map();
|
|
2986
|
+
let timer;
|
|
2987
|
+
let closed = false;
|
|
2988
|
+
const attachWatcher = (targetPath, recursive = false) => {
|
|
2989
|
+
if (watcherMap.has(targetPath)) {
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
try {
|
|
2993
|
+
const watcher = watch(
|
|
2994
|
+
targetPath,
|
|
2995
|
+
recursive ? {
|
|
2996
|
+
recursive: true
|
|
2997
|
+
} : void 0,
|
|
2998
|
+
() => {
|
|
2999
|
+
if (timer) {
|
|
3000
|
+
clearTimeout(timer);
|
|
3001
|
+
}
|
|
3002
|
+
timer = setTimeout(() => {
|
|
3003
|
+
void handleChange();
|
|
3004
|
+
}, debounceMs);
|
|
3005
|
+
}
|
|
3006
|
+
);
|
|
3007
|
+
watcherMap.set(targetPath, watcher);
|
|
3008
|
+
} catch {
|
|
3009
|
+
if (recursive) {
|
|
3010
|
+
attachWatcher(targetPath, false);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
};
|
|
3014
|
+
const refreshWatchers = async () => {
|
|
3015
|
+
const nextTargets = await watchFiles(current.runtime, options.root);
|
|
3016
|
+
attachWatcher(nextTargets.manifestPath, false);
|
|
3017
|
+
for (const workspaceRoot of nextTargets.roots) {
|
|
3018
|
+
attachWatcher(workspaceRoot, true);
|
|
3019
|
+
}
|
|
3020
|
+
for (const filePath of nextTargets.files) {
|
|
3021
|
+
attachWatcher(filePath, false);
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
const handleChange = async () => {
|
|
3025
|
+
if (closed) {
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
const next = await buildRunEnvironment({
|
|
3029
|
+
...options,
|
|
3030
|
+
cliArgs
|
|
3031
|
+
});
|
|
3032
|
+
const changedKeys = diffGraphs(current.runtime.graph, next.runtime.graph);
|
|
3033
|
+
current = next;
|
|
3034
|
+
await refreshWatchers();
|
|
3035
|
+
if (changedKeys.length === 0) {
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
if (isSignal) {
|
|
3039
|
+
await options.onSignal?.({ changedKeys });
|
|
3040
|
+
process.stdout.write(`${printJson({ changedKeys })}
|
|
3041
|
+
`);
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
if (child && !child.killed) {
|
|
3045
|
+
await new Promise((resolve) => {
|
|
3046
|
+
child?.once("close", () => resolve());
|
|
3047
|
+
child?.kill();
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
child = spawnWatchedChild(command, root, current.env);
|
|
3051
|
+
await options.onRestart?.({ changedKeys });
|
|
3052
|
+
};
|
|
3053
|
+
await refreshWatchers();
|
|
3054
|
+
return {
|
|
3055
|
+
async close() {
|
|
3056
|
+
closed = true;
|
|
3057
|
+
if (timer) {
|
|
3058
|
+
clearTimeout(timer);
|
|
3059
|
+
}
|
|
3060
|
+
for (const watcher of watcherMap.values()) {
|
|
3061
|
+
watcher.close();
|
|
3062
|
+
}
|
|
3063
|
+
watcherMap.clear();
|
|
3064
|
+
if (child && !child.killed) {
|
|
3065
|
+
await new Promise((resolve) => {
|
|
3066
|
+
child?.once("close", () => resolve());
|
|
3067
|
+
child?.kill();
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
async function runWatch(command, options = {}) {
|
|
3074
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
3075
|
+
const isSignal = consumeFlag(cliArgs, "--signal");
|
|
3076
|
+
const debounce = consumeOption(cliArgs, "--debounce");
|
|
3077
|
+
const handle = await startWatchLoop({
|
|
3078
|
+
...options,
|
|
3079
|
+
cliArgs: [
|
|
3080
|
+
...cliArgs,
|
|
3081
|
+
...isSignal ? ["--signal"] : [],
|
|
3082
|
+
...debounce ? ["--debounce", debounce] : []
|
|
3083
|
+
],
|
|
3084
|
+
command
|
|
3085
|
+
});
|
|
3086
|
+
const closeWatcher = () => {
|
|
3087
|
+
void handle.close();
|
|
3088
|
+
};
|
|
3089
|
+
process.once("SIGINT", closeWatcher);
|
|
3090
|
+
process.once("SIGTERM", closeWatcher);
|
|
3091
|
+
return isSignal ? "watching config changes in signal mode" : "watching config changes in restart mode";
|
|
3092
|
+
}
|
|
3093
|
+
|
|
2535
3094
|
// src/index.ts
|
|
2536
3095
|
function resolveHelpTopic(command, args) {
|
|
2537
3096
|
if (command === "help" || command === "help-ai") {
|
|
@@ -2612,6 +3171,14 @@ async function main(argv) {
|
|
|
2612
3171
|
return;
|
|
2613
3172
|
case "onboard":
|
|
2614
3173
|
process.stdout.write(`${await runOnboard(runtimeOptions)}
|
|
3174
|
+
`);
|
|
3175
|
+
return;
|
|
3176
|
+
case "migrate":
|
|
3177
|
+
process.stdout.write(`${await runMigrate(runtimeOptions)}
|
|
3178
|
+
`);
|
|
3179
|
+
return;
|
|
3180
|
+
case "codegen":
|
|
3181
|
+
process.stdout.write(`${await runCodegen(runtimeOptions)}
|
|
2615
3182
|
`);
|
|
2616
3183
|
return;
|
|
2617
3184
|
case "read":
|
|
@@ -2676,8 +3243,16 @@ async function main(argv) {
|
|
|
2676
3243
|
process.exitCode = result.exitCode;
|
|
2677
3244
|
return;
|
|
2678
3245
|
}
|
|
3246
|
+
case "watch":
|
|
3247
|
+
process.stdout.write(`${await runWatch(passthrough.length > 0 ? passthrough : args, runtimeOptions)}
|
|
3248
|
+
`);
|
|
3249
|
+
return;
|
|
2679
3250
|
case "diff":
|
|
2680
3251
|
process.stdout.write(`${await runDiff(args[0] ?? "local", args[1] ?? "stage", runtimeOptions)}
|
|
3252
|
+
`);
|
|
3253
|
+
return;
|
|
3254
|
+
case "drift":
|
|
3255
|
+
process.stdout.write(`${await runDrift(runtimeOptions)}
|
|
2681
3256
|
`);
|
|
2682
3257
|
return;
|
|
2683
3258
|
case "doctor":
|