@kitsy/cnos-cli 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +758 -114
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
|
12
12
|
"--format",
|
|
13
13
|
"--framework",
|
|
14
14
|
"--prefix",
|
|
15
|
+
"--mode",
|
|
15
16
|
"--scan",
|
|
16
17
|
"--target",
|
|
17
18
|
"--to",
|
|
@@ -23,7 +24,13 @@ var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
|
23
24
|
"--as",
|
|
24
25
|
"--set",
|
|
25
26
|
"--debounce",
|
|
26
|
-
"--expr"
|
|
27
|
+
"--expr",
|
|
28
|
+
"--extends",
|
|
29
|
+
"--workspaces",
|
|
30
|
+
"--env",
|
|
31
|
+
"--yaml",
|
|
32
|
+
"--toml",
|
|
33
|
+
"--config"
|
|
27
34
|
]);
|
|
28
35
|
var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
|
|
29
36
|
"--flatten",
|
|
@@ -42,7 +49,9 @@ var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set([
|
|
|
42
49
|
"--apply",
|
|
43
50
|
"--rewrite",
|
|
44
51
|
"--signal",
|
|
45
|
-
"--derive"
|
|
52
|
+
"--derive",
|
|
53
|
+
"--materialize",
|
|
54
|
+
"--source-only"
|
|
46
55
|
]);
|
|
47
56
|
function normalizeCommand(argv) {
|
|
48
57
|
const [command = "doctor", ...rest] = argv;
|
|
@@ -158,6 +167,14 @@ function parseArgs(argv) {
|
|
|
158
167
|
passthrough.push(token);
|
|
159
168
|
continue;
|
|
160
169
|
}
|
|
170
|
+
if (command === "onboard" && token === "--json") {
|
|
171
|
+
const nextValue = rest[index + 1];
|
|
172
|
+
if (nextValue && !nextValue.startsWith("--")) {
|
|
173
|
+
cliArgs.push(token, nextValue);
|
|
174
|
+
index += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
161
178
|
if (token === "--json") {
|
|
162
179
|
options.json = true;
|
|
163
180
|
continue;
|
|
@@ -771,6 +788,8 @@ async function defineValue(namespace, configPath, rawValue, options = {}) {
|
|
|
771
788
|
const derivedValue = normalizeDerivedValue(options.deriveExpression, options.deriveExprMode ?? false);
|
|
772
789
|
validateParsedDerivation(runtime.manifest, parseDerivation(derivedValue));
|
|
773
790
|
parsedValue = derivedValue;
|
|
791
|
+
} else if (Object.hasOwn(options, "parsedValue")) {
|
|
792
|
+
parsedValue = options.parsedValue;
|
|
774
793
|
} else {
|
|
775
794
|
parsedValue = parseScalarValue(rawValue);
|
|
776
795
|
}
|
|
@@ -1576,23 +1595,65 @@ var COMMANDS = [
|
|
|
1576
1595
|
},
|
|
1577
1596
|
{
|
|
1578
1597
|
id: "init",
|
|
1579
|
-
summary: "Scaffold a
|
|
1580
|
-
usage: "cnos init [--workspace <
|
|
1581
|
-
description: "Creates .cnos/cnos.yml, .cnosrc.yml,
|
|
1582
|
-
examples: [
|
|
1598
|
+
summary: "Scaffold a CNOS project in regular mode or workspace mode.",
|
|
1599
|
+
usage: "cnos init [--mode <regular|workspace>] [--workspaces <csv>] [--root <path>] [--json]",
|
|
1600
|
+
description: "Creates .cnos/cnos.yml, .cnosrc.yml, config folders, and .gitignore entries without overwriting existing files. Regular mode is the default; workspace mode creates base plus optional child workspaces.",
|
|
1601
|
+
examples: [
|
|
1602
|
+
"cnos init",
|
|
1603
|
+
"cnos init --mode workspace",
|
|
1604
|
+
"cnos init --mode workspace --workspaces api,web,agents",
|
|
1605
|
+
"cnos init --root ./apps/api --json"
|
|
1606
|
+
]
|
|
1583
1607
|
},
|
|
1584
1608
|
{
|
|
1585
1609
|
id: "onboard",
|
|
1586
|
-
summary: "
|
|
1587
|
-
usage: "cnos onboard [--workspace <id>] [--
|
|
1588
|
-
description: "
|
|
1610
|
+
summary: "Import existing env or config sources into CNOS and propose value.* mappings.",
|
|
1611
|
+
usage: "cnos onboard [--workspace <id>] [--env <path>|--yaml <path>|--json <path>|--toml <path>|--config <path>] [--materialize|--source-only] [--prefix <path>] [--move] [--root <path>] [--json]",
|
|
1612
|
+
description: "Auto-discovers root .env* files by default, copies them into CNOS source storage, prints proposed value.* mappings, and can materialize those mappings into CNOS values. In workspace mode, imports land in the selected workspace; otherwise they land in the implicit base layer.",
|
|
1589
1613
|
options: [
|
|
1614
|
+
{
|
|
1615
|
+
flag: "--env <path>",
|
|
1616
|
+
description: "Import one dotenv file instead of auto-discovering root .env* files."
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
flag: "--yaml <path>",
|
|
1620
|
+
description: "Import one YAML config file and flatten it into value.* keys."
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
flag: "--json <path>",
|
|
1624
|
+
description: "Import one JSON config file and flatten it into value.* keys."
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
flag: "--toml <path>",
|
|
1628
|
+
description: "Import one TOML config file and flatten it into value.* keys."
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
flag: "--config <path>",
|
|
1632
|
+
description: "Import one config file using extension-based format detection."
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
flag: "--materialize",
|
|
1636
|
+
description: "Write the proposed value.* mappings without prompting."
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
flag: "--source-only",
|
|
1640
|
+
description: "Copy the source file(s) into CNOS storage but skip value materialization."
|
|
1641
|
+
},
|
|
1642
|
+
{
|
|
1643
|
+
flag: "--prefix <path>",
|
|
1644
|
+
description: "Scope imported keys under value.<prefix>.*."
|
|
1645
|
+
},
|
|
1590
1646
|
{
|
|
1591
1647
|
flag: "--move",
|
|
1592
|
-
description: "Move the
|
|
1648
|
+
description: "Move the source files into CNOS instead of leaving the originals in place."
|
|
1593
1649
|
}
|
|
1594
1650
|
],
|
|
1595
|
-
examples: [
|
|
1651
|
+
examples: [
|
|
1652
|
+
"cnos onboard",
|
|
1653
|
+
"cnos onboard --env .env.production --materialize",
|
|
1654
|
+
"cnos onboard --yaml config/app.yml --prefix app",
|
|
1655
|
+
"cnos onboard --workspace api --config config/api.toml"
|
|
1656
|
+
]
|
|
1596
1657
|
},
|
|
1597
1658
|
{
|
|
1598
1659
|
id: "codegen",
|
|
@@ -2214,10 +2275,55 @@ var COMMANDS = [
|
|
|
2214
2275
|
},
|
|
2215
2276
|
{
|
|
2216
2277
|
id: "workspace",
|
|
2217
|
-
summary: "
|
|
2218
|
-
usage: "cnos workspace <attach|detach> [options] [global-options]",
|
|
2219
|
-
description: "
|
|
2220
|
-
examples: [
|
|
2278
|
+
summary: "Manage workspace creation, listing, migration, and attach/detach flows.",
|
|
2279
|
+
usage: "cnos workspace <enable|add|list|remove|scaffold|attach|detach> [options] [global-options]",
|
|
2280
|
+
description: "Enables workspace mode for flat CNOS projects, adds and removes manifest workspaces, scaffolds package anchors, and handles detach/attach flows for independent child packages.",
|
|
2281
|
+
examples: [
|
|
2282
|
+
"cnos workspace list",
|
|
2283
|
+
"cnos workspace enable",
|
|
2284
|
+
"cnos workspace add travel --package-root apps/travel --extends base",
|
|
2285
|
+
"cnos workspace remove gallery",
|
|
2286
|
+
"cnos workspace detach --package-root apps/travel"
|
|
2287
|
+
]
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
id: "workspace enable",
|
|
2291
|
+
summary: "Convert a flat regular-mode CNOS root into workspace mode with base.",
|
|
2292
|
+
usage: "cnos workspace enable [global-options]",
|
|
2293
|
+
description: "Moves .cnos/values, .cnos/secrets, .cnos/env, and .cnos/profiles into .cnos/workspaces/base, adds a workspaces block to cnos.yml, and updates the root anchor to workspace: base.",
|
|
2294
|
+
examples: ["cnos workspace enable"]
|
|
2295
|
+
},
|
|
2296
|
+
{
|
|
2297
|
+
id: "workspace add",
|
|
2298
|
+
summary: "Add a child workspace to the manifest and scaffold its on-disk layout.",
|
|
2299
|
+
usage: "cnos workspace add <id> [--package-root <path>] [--extends <workspace|none>] [--force] [global-options]",
|
|
2300
|
+
description: "Creates .cnos/workspaces/<id>, updates cnos.yml, and writes a .cnosrc.yml anchor at the selected package root. When a base workspace exists, CNOS defaults new child workspaces to extends: [base] unless --extends or --extends none is provided.",
|
|
2301
|
+
examples: [
|
|
2302
|
+
"cnos workspace add travel --package-root apps/travel --extends base",
|
|
2303
|
+
"cnos workspace add insights --package-root apps/insights",
|
|
2304
|
+
"cnos workspace add api --extends none"
|
|
2305
|
+
]
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
id: "workspace scaffold",
|
|
2309
|
+
summary: "Scaffold a workspace and anchor without changing other runtime flows.",
|
|
2310
|
+
usage: "cnos workspace scaffold <id> [--package-root <path>] [--extends <workspace>] [--force] [global-options]",
|
|
2311
|
+
description: "Creates the workspace manifest entry, workspace folders, and package anchor for a new app or package. This is an alias-oriented workflow for teams that prefer scaffold wording over add.",
|
|
2312
|
+
examples: ["cnos workspace scaffold gallery --package-root apps/gallery --extends base"]
|
|
2313
|
+
},
|
|
2314
|
+
{
|
|
2315
|
+
id: "workspace list",
|
|
2316
|
+
summary: "List declared workspaces and their inheritance.",
|
|
2317
|
+
usage: "cnos workspace list [global-options]",
|
|
2318
|
+
description: "Shows the declared workspace ids, default workspace, and extends relationships from cnos.yml.",
|
|
2319
|
+
examples: ["cnos workspace list", "cnos workspace list --json"]
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
id: "workspace remove",
|
|
2323
|
+
summary: "Remove a workspace from the manifest and delete its local workspace tree.",
|
|
2324
|
+
usage: "cnos workspace remove <id> [global-options]",
|
|
2325
|
+
description: "Deletes .cnos/workspaces/<id> and removes the workspace entry from cnos.yml. CNOS refuses to remove the current default workspace until you change workspaces.default.",
|
|
2326
|
+
examples: ["cnos workspace remove gallery", "cnos workspace remove insights --json"]
|
|
2221
2327
|
},
|
|
2222
2328
|
{
|
|
2223
2329
|
id: "workspace detach",
|
|
@@ -2511,32 +2617,41 @@ import path10 from "path";
|
|
|
2511
2617
|
// src/services/scaffold.ts
|
|
2512
2618
|
import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
2513
2619
|
import path9 from "path";
|
|
2514
|
-
function scaffoldManifest(projectName,
|
|
2620
|
+
function scaffoldManifest(projectName, options = {}) {
|
|
2621
|
+
const mode = options.mode ?? "regular";
|
|
2622
|
+
const baseWorkspace = options.workspace ?? "base";
|
|
2623
|
+
const workspaceIds = mode === "workspace" ? [baseWorkspace, ...(options.workspaces ?? []).filter((id) => id !== baseWorkspace)] : [];
|
|
2515
2624
|
const lines = [
|
|
2516
2625
|
"version: 1",
|
|
2517
2626
|
"project:",
|
|
2518
|
-
` name: ${projectName}
|
|
2519
|
-
"profiles:",
|
|
2520
|
-
" default: base",
|
|
2521
|
-
"envMapping:",
|
|
2522
|
-
" convention: SCREAMING_SNAKE",
|
|
2523
|
-
"public:",
|
|
2524
|
-
" promote: []",
|
|
2525
|
-
""
|
|
2627
|
+
` name: ${projectName}`
|
|
2526
2628
|
];
|
|
2527
|
-
if (workspace) {
|
|
2528
|
-
lines.
|
|
2529
|
-
4,
|
|
2530
|
-
0,
|
|
2629
|
+
if (mode === "workspace") {
|
|
2630
|
+
lines.push(
|
|
2531
2631
|
"workspaces:",
|
|
2532
|
-
` default: ${
|
|
2632
|
+
` default: ${baseWorkspace}`,
|
|
2533
2633
|
" global:",
|
|
2534
2634
|
" enabled: false",
|
|
2535
2635
|
" allowWrite: false",
|
|
2536
2636
|
" items:",
|
|
2537
|
-
` ${
|
|
2637
|
+
` ${baseWorkspace}: {}`
|
|
2538
2638
|
);
|
|
2639
|
+
for (const workspaceId of workspaceIds) {
|
|
2640
|
+
if (workspaceId === baseWorkspace) {
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
lines.push(` ${workspaceId}:`, " extends: [base]");
|
|
2644
|
+
}
|
|
2539
2645
|
}
|
|
2646
|
+
lines.push(
|
|
2647
|
+
"profiles:",
|
|
2648
|
+
" default: local",
|
|
2649
|
+
"envMapping:",
|
|
2650
|
+
" convention: SCREAMING_SNAKE",
|
|
2651
|
+
"public:",
|
|
2652
|
+
" promote: []",
|
|
2653
|
+
""
|
|
2654
|
+
);
|
|
2540
2655
|
return lines.join("\n");
|
|
2541
2656
|
}
|
|
2542
2657
|
async function ensureFile(filePath, content) {
|
|
@@ -2576,8 +2691,7 @@ async function ensureGitignore(root) {
|
|
|
2576
2691
|
`, "utf8");
|
|
2577
2692
|
return true;
|
|
2578
2693
|
}
|
|
2579
|
-
async function
|
|
2580
|
-
const cnosRoot = path9.join(root, ".cnos");
|
|
2694
|
+
async function ensureWorkspaceLayout(cnosRoot, workspace) {
|
|
2581
2695
|
const workspaceRoot = workspace ? path9.join(cnosRoot, "workspaces", workspace) : cnosRoot;
|
|
2582
2696
|
const createdPaths = [];
|
|
2583
2697
|
await mkdir4(path9.join(workspaceRoot, "profiles"), { recursive: true });
|
|
@@ -2598,21 +2712,44 @@ async function scaffoldWorkspace(root, workspace) {
|
|
|
2598
2712
|
for (const relativePath of relativePaths) {
|
|
2599
2713
|
const filePath = path9.join(cnosRoot, ...relativePath);
|
|
2600
2714
|
if (await ensureFile(filePath, "")) {
|
|
2601
|
-
createdPaths.push(path9.relative(
|
|
2715
|
+
createdPaths.push(path9.relative(path9.dirname(cnosRoot), filePath).replace(/\\/g, "/"));
|
|
2602
2716
|
}
|
|
2603
2717
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2718
|
+
return createdPaths;
|
|
2719
|
+
}
|
|
2720
|
+
async function ensureCnosrc(root, workspace) {
|
|
2721
|
+
return ensureFile(
|
|
2608
2722
|
path9.join(root, ".cnosrc.yml"),
|
|
2609
2723
|
workspace ? `root: ./.cnos
|
|
2610
2724
|
workspace: ${workspace}
|
|
2611
2725
|
` : "root: ./.cnos\n"
|
|
2612
|
-
)
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
async function scaffoldProject(root, options = {}) {
|
|
2729
|
+
const mode = options.mode ?? "regular";
|
|
2730
|
+
const baseWorkspace = options.workspace ?? "base";
|
|
2731
|
+
const childWorkspaces = mode === "workspace" ? (options.workspaces ?? []).filter((workspaceId) => workspaceId !== baseWorkspace) : [];
|
|
2732
|
+
const cnosRoot = path9.join(root, ".cnos");
|
|
2733
|
+
const createdPaths = [];
|
|
2734
|
+
if (mode === "workspace") {
|
|
2735
|
+
createdPaths.push(
|
|
2736
|
+
...(await ensureWorkspaceLayout(cnosRoot, baseWorkspace)).map((entry) => entry.replace(/^\.cnos\//, ".cnos/"))
|
|
2737
|
+
);
|
|
2738
|
+
for (const workspaceId of childWorkspaces) {
|
|
2739
|
+
createdPaths.push(
|
|
2740
|
+
...(await ensureWorkspaceLayout(cnosRoot, workspaceId)).map((entry) => entry.replace(/^\.cnos\//, ".cnos/"))
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
} else {
|
|
2744
|
+
createdPaths.push(...(await ensureWorkspaceLayout(cnosRoot)).map((entry) => entry.replace(/^\.cnos\//, ".cnos/")));
|
|
2745
|
+
}
|
|
2746
|
+
if (await ensureFile(path9.join(cnosRoot, "cnos.yml"), scaffoldManifest(path9.basename(root), options))) {
|
|
2747
|
+
createdPaths.push(".cnos/cnos.yml");
|
|
2748
|
+
}
|
|
2749
|
+
if (await ensureCnosrc(root, mode === "workspace" ? baseWorkspace : void 0)) {
|
|
2613
2750
|
createdPaths.push(".cnosrc.yml");
|
|
2614
2751
|
}
|
|
2615
|
-
if (workspace && await ensureFile(path9.join(root, ".cnos-workspace.yml"), `workspace: ${
|
|
2752
|
+
if (mode === "workspace" && await ensureFile(path9.join(root, ".cnos-workspace.yml"), `workspace: ${baseWorkspace}
|
|
2616
2753
|
globalRoot: ~/.cnos
|
|
2617
2754
|
`)) {
|
|
2618
2755
|
createdPaths.push(".cnos-workspace.yml");
|
|
@@ -2622,20 +2759,42 @@ globalRoot: ~/.cnos
|
|
|
2622
2759
|
}
|
|
2623
2760
|
return {
|
|
2624
2761
|
root,
|
|
2625
|
-
|
|
2762
|
+
mode,
|
|
2763
|
+
...mode === "workspace" ? { workspace: baseWorkspace, workspaces: [baseWorkspace, ...childWorkspaces] } : {},
|
|
2626
2764
|
created: createdPaths
|
|
2627
2765
|
};
|
|
2628
2766
|
}
|
|
2629
2767
|
|
|
2630
2768
|
// src/commands/init.ts
|
|
2769
|
+
function parseWorkspaceList(value) {
|
|
2770
|
+
if (!value) {
|
|
2771
|
+
return [];
|
|
2772
|
+
}
|
|
2773
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
2774
|
+
}
|
|
2631
2775
|
async function runInit(options = {}) {
|
|
2632
2776
|
const root = path10.resolve(options.root ?? process.cwd());
|
|
2633
|
-
const
|
|
2777
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
2778
|
+
const modeOption = consumeOption(cliArgs, "--mode");
|
|
2779
|
+
const workspacesOption = consumeOption(cliArgs, "--workspaces");
|
|
2780
|
+
if (cliArgs.length > 0) {
|
|
2781
|
+
throw new Error(`Unsupported init arguments: ${cliArgs.join(" ")}`);
|
|
2782
|
+
}
|
|
2783
|
+
const mode = modeOption === void 0 ? options.workspace ? "workspace" : "regular" : modeOption === "workspace" || modeOption === "regular" ? modeOption : void 0;
|
|
2784
|
+
if (!mode) {
|
|
2785
|
+
throw new Error(`Invalid value for --mode: ${modeOption}. Use "regular" or "workspace".`);
|
|
2786
|
+
}
|
|
2787
|
+
const workspaces = parseWorkspaceList(workspacesOption);
|
|
2788
|
+
const result = await scaffoldProject(root, {
|
|
2789
|
+
mode,
|
|
2790
|
+
...mode === "workspace" ? { workspace: options.workspace ?? "base", workspaces } : {}
|
|
2791
|
+
});
|
|
2634
2792
|
if (options.json) {
|
|
2635
2793
|
return printJson(result);
|
|
2636
2794
|
}
|
|
2637
|
-
if (result.workspace) {
|
|
2638
|
-
|
|
2795
|
+
if (result.mode === "workspace" && result.workspace) {
|
|
2796
|
+
const suffix = result.workspaces && result.workspaces.length > 1 ? ` (${result.workspaces.slice(1).join(", ")} extends ${result.workspace})` : "";
|
|
2797
|
+
return `initialized CNOS workspace project at ${root} with base workspace ${result.workspace}${suffix}`;
|
|
2639
2798
|
}
|
|
2640
2799
|
return `initialized CNOS project at ${root}`;
|
|
2641
2800
|
}
|
|
@@ -3034,67 +3193,289 @@ async function runNamespace(namespace, args = [], options = {}) {
|
|
|
3034
3193
|
}
|
|
3035
3194
|
|
|
3036
3195
|
// src/commands/onboard.ts
|
|
3037
|
-
import { copyFile, readdir as readdir3, rm as rm2 } from "fs/promises";
|
|
3196
|
+
import { copyFile, mkdir as mkdir5, readdir as readdir3, rm as rm2, stat as stat2, readFile as readFile4 } from "fs/promises";
|
|
3038
3197
|
import path13 from "path";
|
|
3198
|
+
import readline from "readline/promises";
|
|
3199
|
+
import { loadManifest as loadManifest5, parseYaml as parseYaml3 } from "@kitsy/cnos/internal";
|
|
3200
|
+
import { parse as parseToml } from "smol-toml";
|
|
3039
3201
|
var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
|
|
3202
|
+
var SECRET_LIKE_PATTERN = /(secret|token|password|passwd|private|api[_-]?key|client[_-]?secret|dsn)/i;
|
|
3203
|
+
async function exists(targetPath) {
|
|
3204
|
+
try {
|
|
3205
|
+
await stat2(targetPath);
|
|
3206
|
+
return true;
|
|
3207
|
+
} catch {
|
|
3208
|
+
return false;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3040
3211
|
async function listRootEnvFiles(root) {
|
|
3041
3212
|
const entries = await readdir3(root, { withFileTypes: true });
|
|
3042
3213
|
return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
3043
3214
|
}
|
|
3215
|
+
function normalizePathSegment(value) {
|
|
3216
|
+
return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
3217
|
+
}
|
|
3218
|
+
function toLogicalPath(sourceKey, prefixSegments) {
|
|
3219
|
+
const derivedSegments = sourceKey.split(/[._-]+/).map((segment) => normalizePathSegment(segment)).filter(Boolean);
|
|
3220
|
+
return [...prefixSegments, ...derivedSegments].join(".");
|
|
3221
|
+
}
|
|
3222
|
+
function toWarning(sourceKey) {
|
|
3223
|
+
return SECRET_LIKE_PATTERN.test(sourceKey) ? "looks like a secret" : void 0;
|
|
3224
|
+
}
|
|
3225
|
+
function createProposedMapping(source, pathKey, value, warning) {
|
|
3226
|
+
return {
|
|
3227
|
+
source,
|
|
3228
|
+
key: `value.${pathKey}`,
|
|
3229
|
+
path: pathKey,
|
|
3230
|
+
value,
|
|
3231
|
+
...warning ? { warning } : {}
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
function parseEnv(content) {
|
|
3235
|
+
const values = {};
|
|
3236
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
3237
|
+
const line = rawLine.trim();
|
|
3238
|
+
if (!line || line.startsWith("#")) {
|
|
3239
|
+
continue;
|
|
3240
|
+
}
|
|
3241
|
+
const separator = line.indexOf("=");
|
|
3242
|
+
if (separator <= 0) {
|
|
3243
|
+
continue;
|
|
3244
|
+
}
|
|
3245
|
+
const key = line.slice(0, separator).trim();
|
|
3246
|
+
let value = line.slice(separator + 1).trim();
|
|
3247
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3248
|
+
value = value.slice(1, -1);
|
|
3249
|
+
}
|
|
3250
|
+
values[key] = value;
|
|
3251
|
+
}
|
|
3252
|
+
return values;
|
|
3253
|
+
}
|
|
3254
|
+
function flattenStructured(value, prefixSegments, currentKey = []) {
|
|
3255
|
+
if (Array.isArray(value) || value === null || typeof value !== "object") {
|
|
3256
|
+
const pathKey = [...prefixSegments, ...currentKey.map((segment) => normalizePathSegment(segment)).filter(Boolean)].join(".");
|
|
3257
|
+
const sourceKey = currentKey.join(".");
|
|
3258
|
+
return pathKey ? [
|
|
3259
|
+
createProposedMapping(sourceKey, pathKey, value, sourceKey ? toWarning(sourceKey) : void 0)
|
|
3260
|
+
] : [];
|
|
3261
|
+
}
|
|
3262
|
+
return Object.entries(value).flatMap(
|
|
3263
|
+
([key, nested]) => flattenStructured(nested, prefixSegments, [...currentKey, key])
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3266
|
+
async function parseSource(input, prefixSegments) {
|
|
3267
|
+
const content = await readFile4(input.filePath, "utf8");
|
|
3268
|
+
switch (input.kind) {
|
|
3269
|
+
case "env":
|
|
3270
|
+
return Object.entries(parseEnv(content)).map(([sourceKey, value]) => {
|
|
3271
|
+
const logicalPath = toLogicalPath(sourceKey, prefixSegments);
|
|
3272
|
+
return createProposedMapping(sourceKey, logicalPath, value, toWarning(sourceKey));
|
|
3273
|
+
});
|
|
3274
|
+
case "yaml":
|
|
3275
|
+
return flattenStructured(parseYaml3(content), prefixSegments);
|
|
3276
|
+
case "json":
|
|
3277
|
+
return flattenStructured(JSON.parse(content), prefixSegments);
|
|
3278
|
+
case "toml":
|
|
3279
|
+
return flattenStructured(parseToml(content), prefixSegments);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
function detectKindFromPath(filePath) {
|
|
3283
|
+
const ext = path13.extname(filePath).toLowerCase();
|
|
3284
|
+
switch (ext) {
|
|
3285
|
+
case ".env":
|
|
3286
|
+
return "env";
|
|
3287
|
+
case ".yaml":
|
|
3288
|
+
case ".yml":
|
|
3289
|
+
return "yaml";
|
|
3290
|
+
case ".json":
|
|
3291
|
+
return "json";
|
|
3292
|
+
case ".toml":
|
|
3293
|
+
return "toml";
|
|
3294
|
+
default:
|
|
3295
|
+
throw new Error(`Unsupported config format for ${filePath}. Use --env, --yaml, --json, --toml, or --config with a supported extension.`);
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
function buildPrefixSegments(prefix) {
|
|
3299
|
+
if (!prefix) {
|
|
3300
|
+
return [];
|
|
3301
|
+
}
|
|
3302
|
+
return prefix.split(".").map((segment) => normalizePathSegment(segment)).filter(Boolean);
|
|
3303
|
+
}
|
|
3304
|
+
function formatProposals(proposed) {
|
|
3305
|
+
if (proposed.length === 0) {
|
|
3306
|
+
return ["No value mappings were discovered."];
|
|
3307
|
+
}
|
|
3308
|
+
return proposed.map((entry) => {
|
|
3309
|
+
const renderedValue = typeof entry.value === "string" ? JSON.stringify(entry.value) : JSON.stringify(entry.value);
|
|
3310
|
+
return ` ${entry.source || entry.path} -> ${entry.key} = ${renderedValue}${entry.warning ? ` [${entry.warning}]` : ""}`;
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
async function promptForMaterialize() {
|
|
3314
|
+
const rl = readline.createInterface({
|
|
3315
|
+
input: process.stdin,
|
|
3316
|
+
output: process.stdout
|
|
3317
|
+
});
|
|
3318
|
+
try {
|
|
3319
|
+
const answer = (await rl.question("Materialize these values into value.*? [Y/n] ")).trim().toLowerCase();
|
|
3320
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
3321
|
+
} finally {
|
|
3322
|
+
rl.close();
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
function isInteractive(options) {
|
|
3326
|
+
const processEnv = options.processEnv ?? process.env;
|
|
3327
|
+
return !processEnv.CI && process.stdin.isTTY && process.stdout.isTTY;
|
|
3328
|
+
}
|
|
3329
|
+
function resolveSourceInputs(root, cliArgs) {
|
|
3330
|
+
const envFile = consumeOption(cliArgs, "--env");
|
|
3331
|
+
const yamlFile = consumeOption(cliArgs, "--yaml");
|
|
3332
|
+
const jsonFile = consumeOption(cliArgs, "--json");
|
|
3333
|
+
const tomlFile = consumeOption(cliArgs, "--toml");
|
|
3334
|
+
const configFile = consumeOption(cliArgs, "--config");
|
|
3335
|
+
const explicit = [
|
|
3336
|
+
envFile ? { kind: "env", filePath: envFile } : void 0,
|
|
3337
|
+
yamlFile ? { kind: "yaml", filePath: yamlFile } : void 0,
|
|
3338
|
+
jsonFile ? { kind: "json", filePath: jsonFile } : void 0,
|
|
3339
|
+
tomlFile ? { kind: "toml", filePath: tomlFile } : void 0,
|
|
3340
|
+
configFile ? { kind: detectKindFromPath(configFile), filePath: configFile } : void 0
|
|
3341
|
+
].filter(Boolean);
|
|
3342
|
+
if (explicit.length > 1) {
|
|
3343
|
+
throw new Error("Use only one explicit source flag per onboard invocation.");
|
|
3344
|
+
}
|
|
3345
|
+
if (explicit.length === 1) {
|
|
3346
|
+
const source = explicit[0];
|
|
3347
|
+
if (!source) {
|
|
3348
|
+
return [];
|
|
3349
|
+
}
|
|
3350
|
+
const resolvedPath = path13.resolve(root, source.filePath);
|
|
3351
|
+
return [
|
|
3352
|
+
{
|
|
3353
|
+
kind: source.kind,
|
|
3354
|
+
filePath: resolvedPath,
|
|
3355
|
+
displayName: path13.basename(resolvedPath)
|
|
3356
|
+
}
|
|
3357
|
+
];
|
|
3358
|
+
}
|
|
3359
|
+
return [];
|
|
3360
|
+
}
|
|
3044
3361
|
async function runOnboard(options = {}) {
|
|
3045
3362
|
const root = path13.resolve(options.root ?? process.cwd());
|
|
3046
|
-
const workspace = options.workspace ?? path13.basename(root);
|
|
3047
3363
|
const cliArgs = [...options.cliArgs ?? []];
|
|
3048
3364
|
const move = consumeFlag(cliArgs, "--move");
|
|
3365
|
+
const materialize = consumeFlag(cliArgs, "--materialize");
|
|
3366
|
+
const sourceOnly = consumeFlag(cliArgs, "--source-only");
|
|
3367
|
+
const prefix = consumeOption(cliArgs, "--prefix");
|
|
3368
|
+
if (materialize && sourceOnly) {
|
|
3369
|
+
throw new Error("Use either --materialize or --source-only, not both.");
|
|
3370
|
+
}
|
|
3371
|
+
const explicitSources = resolveSourceInputs(root, cliArgs);
|
|
3049
3372
|
if (cliArgs.length > 0) {
|
|
3050
3373
|
throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
|
|
3051
3374
|
}
|
|
3052
|
-
|
|
3053
|
-
const
|
|
3054
|
-
|
|
3375
|
+
let scaffolded = [];
|
|
3376
|
+
const manifestPath = path13.join(root, ".cnos", "cnos.yml");
|
|
3377
|
+
if (!await exists(manifestPath)) {
|
|
3378
|
+
const scaffold = await scaffoldProject(root, {
|
|
3379
|
+
mode: options.workspace && options.workspace !== "base" ? "workspace" : "regular",
|
|
3380
|
+
...options.workspace && options.workspace !== "base" ? { workspaces: [options.workspace] } : {}
|
|
3381
|
+
});
|
|
3382
|
+
scaffolded = scaffold.created;
|
|
3383
|
+
}
|
|
3384
|
+
const loaded = await loadManifest5({
|
|
3385
|
+
root,
|
|
3386
|
+
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
3387
|
+
});
|
|
3388
|
+
const isWorkspaceMode = Object.keys(loaded.manifest.workspaces.items).length > 0;
|
|
3389
|
+
const selectedWorkspace = isWorkspaceMode ? options.workspace ?? (loaded.manifest.workspaces.items.base ? "base" : loaded.manifest.workspaces.default ?? "base") : "base";
|
|
3390
|
+
if (isWorkspaceMode && !loaded.manifest.workspaces.items[selectedWorkspace]) {
|
|
3391
|
+
throw new Error(`Workspace "${selectedWorkspace}" does not exist in this CNOS root.`);
|
|
3392
|
+
}
|
|
3393
|
+
if (!isWorkspaceMode && options.workspace && options.workspace !== "base") {
|
|
3394
|
+
throw new Error("This repo is still in regular mode. Run `cnos workspace enable` before onboarding into a child workspace.");
|
|
3395
|
+
}
|
|
3396
|
+
const envRoot = isWorkspaceMode ? path13.join(root, ".cnos", "workspaces", selectedWorkspace, "env") : path13.join(root, ".cnos", "env");
|
|
3397
|
+
await mkdir5(envRoot, { recursive: true });
|
|
3398
|
+
const rootFiles = explicitSources.length > 0 ? explicitSources : (await listRootEnvFiles(root)).map((fileName) => ({
|
|
3399
|
+
kind: "env",
|
|
3400
|
+
filePath: path13.join(root, fileName),
|
|
3401
|
+
displayName: fileName
|
|
3402
|
+
}));
|
|
3055
3403
|
const imported = [];
|
|
3056
3404
|
const skipped = [];
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3405
|
+
const prefixSegments = buildPrefixSegments(prefix);
|
|
3406
|
+
const proposed = [];
|
|
3407
|
+
for (const source of rootFiles) {
|
|
3408
|
+
const targetPath = path13.join(envRoot, source.displayName);
|
|
3060
3409
|
try {
|
|
3061
|
-
await copyFile(
|
|
3410
|
+
await copyFile(source.filePath, targetPath);
|
|
3062
3411
|
imported.push(path13.relative(root, targetPath).replace(/\\/g, "/"));
|
|
3063
3412
|
if (move) {
|
|
3064
|
-
await rm2(
|
|
3413
|
+
await rm2(source.filePath);
|
|
3065
3414
|
}
|
|
3066
3415
|
} catch {
|
|
3067
|
-
skipped.push(
|
|
3416
|
+
skipped.push(source.displayName);
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
proposed.push(...await parseSource(source, prefixSegments));
|
|
3420
|
+
}
|
|
3421
|
+
const shouldMaterialize = materialize || (!sourceOnly && isInteractive(options) && proposed.length > 0 ? await promptForMaterialize() : false);
|
|
3422
|
+
const materialized = [];
|
|
3423
|
+
if (shouldMaterialize) {
|
|
3424
|
+
for (const entry of proposed) {
|
|
3425
|
+
await defineValue("value", entry.path, String(entry.value ?? ""), {
|
|
3426
|
+
root,
|
|
3427
|
+
...isWorkspaceMode ? { workspace: selectedWorkspace } : {},
|
|
3428
|
+
parsedValue: entry.value
|
|
3429
|
+
});
|
|
3430
|
+
materialized.push(entry.key);
|
|
3068
3431
|
}
|
|
3069
3432
|
}
|
|
3070
3433
|
const result = {
|
|
3071
3434
|
root,
|
|
3072
|
-
workspace,
|
|
3073
|
-
|
|
3435
|
+
workspace: selectedWorkspace,
|
|
3436
|
+
mode: move ? "move" : "copy",
|
|
3437
|
+
storageMode: isWorkspaceMode ? "workspace" : "regular",
|
|
3438
|
+
scaffolded,
|
|
3074
3439
|
imported,
|
|
3075
3440
|
skipped,
|
|
3076
|
-
|
|
3441
|
+
proposed,
|
|
3442
|
+
materialized
|
|
3077
3443
|
};
|
|
3078
3444
|
if (options.json) {
|
|
3079
3445
|
return printJson(result);
|
|
3080
3446
|
}
|
|
3081
|
-
const
|
|
3082
|
-
|
|
3083
|
-
|
|
3447
|
+
const lines = [
|
|
3448
|
+
`onboarded ${selectedWorkspace} at ${root}`,
|
|
3449
|
+
`Imported ${imported.length} source file(s) into ${path13.relative(root, envRoot).replace(/\\/g, "/") || ".cnos/env"} using ${result.mode}.`,
|
|
3450
|
+
"",
|
|
3451
|
+
`Discovered ${proposed.length} proposed value mapping(s):`,
|
|
3452
|
+
...formatProposals(proposed)
|
|
3453
|
+
];
|
|
3454
|
+
if (!shouldMaterialize && !sourceOnly && !isInteractive(options) && proposed.length > 0) {
|
|
3455
|
+
lines.push("", "Non-interactive mode detected; defaulted to source-only. Re-run with --materialize to write value.* keys.");
|
|
3456
|
+
} else if (shouldMaterialize) {
|
|
3457
|
+
lines.push("", `Materialized ${materialized.length} value key(s).`);
|
|
3458
|
+
} else if (sourceOnly) {
|
|
3459
|
+
lines.push("", "Skipped value materialization because --source-only was set.");
|
|
3460
|
+
}
|
|
3461
|
+
if (skipped.length > 0) {
|
|
3462
|
+
lines.push("", `Skipped ${skipped.length} source file(s): ${skipped.join(", ")}`);
|
|
3463
|
+
}
|
|
3464
|
+
return lines.join("\n");
|
|
3084
3465
|
}
|
|
3085
3466
|
|
|
3086
3467
|
// src/commands/profile.ts
|
|
3087
3468
|
import path16 from "path";
|
|
3088
3469
|
|
|
3089
3470
|
// src/services/context.ts
|
|
3090
|
-
import { readFile as
|
|
3471
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
3091
3472
|
import path14 from "path";
|
|
3092
|
-
import { parseYaml as
|
|
3473
|
+
import { parseYaml as parseYaml4, stringifyYaml as stringifyYaml3 } from "@kitsy/cnos/internal";
|
|
3093
3474
|
async function loadCliContext(root = process.cwd()) {
|
|
3094
3475
|
const filePath = path14.join(path14.resolve(root), ".cnos-workspace.yml");
|
|
3095
3476
|
try {
|
|
3096
|
-
const source = await
|
|
3097
|
-
const parsed =
|
|
3477
|
+
const source = await readFile5(filePath, "utf8");
|
|
3478
|
+
const parsed = parseYaml4(source);
|
|
3098
3479
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3099
3480
|
return {};
|
|
3100
3481
|
}
|
|
@@ -3123,21 +3504,21 @@ async function saveCliContext(options = {}) {
|
|
|
3123
3504
|
}
|
|
3124
3505
|
|
|
3125
3506
|
// src/services/profiles.ts
|
|
3126
|
-
import { mkdir as
|
|
3507
|
+
import { mkdir as mkdir6, readdir as readdir4, readFile as readFile6, rm as rm3, writeFile as writeFile6 } from "fs/promises";
|
|
3127
3508
|
import path15 from "path";
|
|
3128
|
-
import { loadManifest as
|
|
3509
|
+
import { loadManifest as loadManifest6, parseYaml as parseYaml5, stringifyYaml as stringifyYaml4 } from "@kitsy/cnos/internal";
|
|
3129
3510
|
async function resolveProfilesRoot(root = process.cwd()) {
|
|
3130
3511
|
try {
|
|
3131
|
-
const loadedManifest = await
|
|
3512
|
+
const loadedManifest = await loadManifest6({ root });
|
|
3132
3513
|
return path15.join(loadedManifest.manifestRoot, "profiles");
|
|
3133
3514
|
} catch {
|
|
3134
|
-
const loadedManifest = await
|
|
3515
|
+
const loadedManifest = await loadManifest6({ cwd: root });
|
|
3135
3516
|
return path15.join(loadedManifest.manifestRoot, "profiles");
|
|
3136
3517
|
}
|
|
3137
3518
|
}
|
|
3138
3519
|
async function createProfileDefinition(root = process.cwd(), profile, inherit, options = {}) {
|
|
3139
3520
|
const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
|
|
3140
|
-
await
|
|
3521
|
+
await mkdir6(path15.dirname(filePath), { recursive: true });
|
|
3141
3522
|
const document = options.noInherit ? {
|
|
3142
3523
|
name: profile,
|
|
3143
3524
|
activate: {
|
|
@@ -3197,7 +3578,7 @@ async function readProfileDefinition(root = process.cwd(), profile = "base") {
|
|
|
3197
3578
|
}
|
|
3198
3579
|
const filePath = path15.join(await resolveProfilesRoot(root), `${profile}.yml`);
|
|
3199
3580
|
try {
|
|
3200
|
-
return
|
|
3581
|
+
return parseYaml5(await readFile6(filePath, "utf8")) ?? void 0;
|
|
3201
3582
|
} catch {
|
|
3202
3583
|
return void 0;
|
|
3203
3584
|
}
|
|
@@ -3277,7 +3658,7 @@ import path17 from "path";
|
|
|
3277
3658
|
import { writeFile as writeFile7 } from "fs/promises";
|
|
3278
3659
|
import {
|
|
3279
3660
|
ensureProjectionAllowed,
|
|
3280
|
-
loadManifest as
|
|
3661
|
+
loadManifest as loadManifest7,
|
|
3281
3662
|
stringifyYaml as stringifyYaml5
|
|
3282
3663
|
} from "@kitsy/cnos/internal";
|
|
3283
3664
|
function normalizeTarget(value) {
|
|
@@ -3306,7 +3687,7 @@ async function runPromote(args = [], options = {}) {
|
|
|
3306
3687
|
throw new Error("promote --to env requires --as <ENV_VAR>");
|
|
3307
3688
|
}
|
|
3308
3689
|
}
|
|
3309
|
-
const loadedManifest = await
|
|
3690
|
+
const loadedManifest = await loadManifest7({
|
|
3310
3691
|
...options.root ? { root: options.root } : {},
|
|
3311
3692
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
3312
3693
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -3473,7 +3854,7 @@ import {
|
|
|
3473
3854
|
createSecretVault,
|
|
3474
3855
|
deriveVaultKey,
|
|
3475
3856
|
listLocalSecrets,
|
|
3476
|
-
loadManifest as
|
|
3857
|
+
loadManifest as loadManifest8,
|
|
3477
3858
|
listSecretVaults,
|
|
3478
3859
|
readVaultMetadata,
|
|
3479
3860
|
resolveSecretStoreRoot as resolveSecretStoreRoot2,
|
|
@@ -3512,7 +3893,7 @@ async function createVaultDefinition(name, options = {}) {
|
|
|
3512
3893
|
if (provider === "local" && (options.noPassphrase ?? false)) {
|
|
3513
3894
|
throw new Error("Local vaults cannot be passwordless.");
|
|
3514
3895
|
}
|
|
3515
|
-
const loadedManifest = await
|
|
3896
|
+
const loadedManifest = await loadManifest8({
|
|
3516
3897
|
...options.root ? { root: options.root } : {},
|
|
3517
3898
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
3518
3899
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -3549,7 +3930,7 @@ async function createVaultDefinition(name, options = {}) {
|
|
|
3549
3930
|
};
|
|
3550
3931
|
}
|
|
3551
3932
|
async function listVaultDefinitions(options = {}) {
|
|
3552
|
-
const loadedManifest = await
|
|
3933
|
+
const loadedManifest = await loadManifest8({
|
|
3553
3934
|
...options.root ? { root: options.root } : {},
|
|
3554
3935
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
3555
3936
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -3570,7 +3951,7 @@ async function listVaultDefinitions(options = {}) {
|
|
|
3570
3951
|
async function removeVaultDefinition(name, options = {}) {
|
|
3571
3952
|
await assertWritableConfigRoot(`remove vault ${name}`, options);
|
|
3572
3953
|
const vault = name.trim() || "default";
|
|
3573
|
-
const loadedManifest = await
|
|
3954
|
+
const loadedManifest = await loadManifest8({
|
|
3574
3955
|
...options.root ? { root: options.root } : {},
|
|
3575
3956
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
3576
3957
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -3616,7 +3997,7 @@ async function listLocalStoreVaults(options = {}) {
|
|
|
3616
3997
|
}
|
|
3617
3998
|
async function authenticateVault(name, options = {}) {
|
|
3618
3999
|
const vault = name.trim() || "default";
|
|
3619
|
-
const loadedManifest = await
|
|
4000
|
+
const loadedManifest = await loadManifest8({
|
|
3620
4001
|
...options.root ? { root: options.root } : {},
|
|
3621
4002
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
3622
4003
|
...options.processEnv ? { processEnv: options.processEnv } : {},
|
|
@@ -3937,7 +4318,7 @@ async function runValidate(options = {}) {
|
|
|
3937
4318
|
// package.json
|
|
3938
4319
|
var package_default = {
|
|
3939
4320
|
name: "@kitsy/cnos-cli",
|
|
3940
|
-
version: "1.
|
|
4321
|
+
version: "1.8.1",
|
|
3941
4322
|
description: "CLI entry point and developer tooling for CNOS.",
|
|
3942
4323
|
type: "module",
|
|
3943
4324
|
main: "./dist/index.js",
|
|
@@ -3973,7 +4354,8 @@ var package_default = {
|
|
|
3973
4354
|
access: "public"
|
|
3974
4355
|
},
|
|
3975
4356
|
dependencies: {
|
|
3976
|
-
"@kitsy/cnos": "workspace:*"
|
|
4357
|
+
"@kitsy/cnos": "workspace:*",
|
|
4358
|
+
"smol-toml": "^1.4.2"
|
|
3977
4359
|
},
|
|
3978
4360
|
scripts: {
|
|
3979
4361
|
build: "tsup src/index.ts --format esm --dts",
|
|
@@ -4212,40 +4594,52 @@ async function runWatch(command, options = {}) {
|
|
|
4212
4594
|
}
|
|
4213
4595
|
|
|
4214
4596
|
// src/commands/workspace.ts
|
|
4215
|
-
import { cp, mkdir as
|
|
4597
|
+
import { cp, mkdir as mkdir7, readdir as readdir5, readFile as readFile7, rename, rm as rm5, stat as stat3, writeFile as writeFile9 } from "fs/promises";
|
|
4216
4598
|
import path23 from "path";
|
|
4217
|
-
import { loadManifest as
|
|
4218
|
-
async function
|
|
4599
|
+
import { loadManifest as loadManifest9, parseYaml as parseYaml6, stringifyYaml as stringifyYaml7 } from "@kitsy/cnos/internal";
|
|
4600
|
+
async function exists2(targetPath) {
|
|
4219
4601
|
try {
|
|
4220
|
-
await
|
|
4602
|
+
await stat3(targetPath);
|
|
4221
4603
|
return true;
|
|
4222
4604
|
} catch {
|
|
4223
4605
|
return false;
|
|
4224
4606
|
}
|
|
4225
4607
|
}
|
|
4226
4608
|
async function copyIfExists(source, target) {
|
|
4227
|
-
if (!await
|
|
4609
|
+
if (!await exists2(source)) {
|
|
4228
4610
|
return;
|
|
4229
4611
|
}
|
|
4230
|
-
await
|
|
4612
|
+
await mkdir7(path23.dirname(target), { recursive: true });
|
|
4231
4613
|
await cp(source, target, { recursive: true, force: true });
|
|
4232
4614
|
}
|
|
4615
|
+
async function moveIfExists(source, target, force = false) {
|
|
4616
|
+
if (!await exists2(source)) {
|
|
4617
|
+
return false;
|
|
4618
|
+
}
|
|
4619
|
+
if (force) {
|
|
4620
|
+
await rm5(target, { recursive: true, force: true });
|
|
4621
|
+
} else if (await exists2(target)) {
|
|
4622
|
+
throw new Error(`Refusing to overwrite existing path ${target}. Use --force to replace it.`);
|
|
4623
|
+
}
|
|
4624
|
+
await mkdir7(path23.dirname(target), { recursive: true });
|
|
4625
|
+
await rename(source, target);
|
|
4626
|
+
return true;
|
|
4627
|
+
}
|
|
4233
4628
|
async function mergeWorkspaceRootsIntoStandalone(targetCnosRoot, sourceRoots) {
|
|
4234
4629
|
for (const sourceRoot of sourceRoots) {
|
|
4235
4630
|
for (const folderName of ["values", "secrets", "env", "profiles"]) {
|
|
4236
|
-
await copyIfExists(
|
|
4237
|
-
path23.join(sourceRoot, folderName),
|
|
4238
|
-
path23.join(targetCnosRoot, folderName)
|
|
4239
|
-
);
|
|
4631
|
+
await copyIfExists(path23.join(sourceRoot, folderName), path23.join(targetCnosRoot, folderName));
|
|
4240
4632
|
}
|
|
4241
4633
|
}
|
|
4242
4634
|
}
|
|
4243
|
-
async function
|
|
4635
|
+
async function writeAnchor(packageRoot, manifestRoot, workspace) {
|
|
4636
|
+
const relativeRoot = path23.relative(packageRoot, manifestRoot).replace(/\\/g, "/");
|
|
4637
|
+
const rootValue = relativeRoot.length === 0 ? "./.cnos" : relativeRoot.startsWith(".") ? relativeRoot : `./${relativeRoot}`;
|
|
4244
4638
|
await writeFile9(
|
|
4245
4639
|
path23.join(packageRoot, ".cnosrc.yml"),
|
|
4246
4640
|
stringifyYaml7({
|
|
4247
|
-
root:
|
|
4248
|
-
...
|
|
4641
|
+
root: rootValue,
|
|
4642
|
+
...workspace ? { workspace } : {}
|
|
4249
4643
|
}),
|
|
4250
4644
|
"utf8"
|
|
4251
4645
|
);
|
|
@@ -4257,14 +4651,72 @@ function createDetachedManifest(rawManifest) {
|
|
|
4257
4651
|
}
|
|
4258
4652
|
return next;
|
|
4259
4653
|
}
|
|
4654
|
+
function normalizeWorkspaceId(value) {
|
|
4655
|
+
const workspaceId = value?.trim();
|
|
4656
|
+
if (!workspaceId) {
|
|
4657
|
+
throw new Error("Workspace id is required");
|
|
4658
|
+
}
|
|
4659
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(workspaceId)) {
|
|
4660
|
+
throw new Error(`Invalid workspace id "${workspaceId}". Use letters, numbers, dot, underscore, or dash.`);
|
|
4661
|
+
}
|
|
4662
|
+
return workspaceId;
|
|
4663
|
+
}
|
|
4664
|
+
function splitExtends(value) {
|
|
4665
|
+
if (!value) {
|
|
4666
|
+
return void 0;
|
|
4667
|
+
}
|
|
4668
|
+
if (value.trim() === "none") {
|
|
4669
|
+
return [];
|
|
4670
|
+
}
|
|
4671
|
+
const items = value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
4672
|
+
return items.length > 0 ? items : void 0;
|
|
4673
|
+
}
|
|
4674
|
+
async function hasDirectConfigData(cnosRoot) {
|
|
4675
|
+
for (const folderName of ["values", "secrets", "env", "profiles"]) {
|
|
4676
|
+
const folder = path23.join(cnosRoot, folderName);
|
|
4677
|
+
if (!await exists2(folder)) {
|
|
4678
|
+
continue;
|
|
4679
|
+
}
|
|
4680
|
+
const entries = await readdir5(folder, { withFileTypes: true });
|
|
4681
|
+
if (entries.some((entry) => entry.name !== ".gitkeep")) {
|
|
4682
|
+
return true;
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
return false;
|
|
4686
|
+
}
|
|
4687
|
+
async function updateRootAnchorToWorkspace(packageRoot, workspaceId) {
|
|
4688
|
+
const anchorPath = path23.join(packageRoot, ".cnosrc.yml");
|
|
4689
|
+
const current = await exists2(anchorPath) ? parseYaml6(await readFile7(anchorPath, "utf8")) : void 0;
|
|
4690
|
+
await writeFile9(
|
|
4691
|
+
anchorPath,
|
|
4692
|
+
stringifyYaml7({
|
|
4693
|
+
root: typeof current?.root === "string" ? current.root : "./.cnos",
|
|
4694
|
+
workspace: workspaceId
|
|
4695
|
+
}),
|
|
4696
|
+
"utf8"
|
|
4697
|
+
);
|
|
4698
|
+
}
|
|
4699
|
+
async function updateWorkspaceContext(packageRoot, workspaceId) {
|
|
4700
|
+
const workspacePath = path23.join(packageRoot, ".cnos-workspace.yml");
|
|
4701
|
+
const current = await exists2(workspacePath) ? parseYaml6(await readFile7(workspacePath, "utf8")) : void 0;
|
|
4702
|
+
await writeFile9(
|
|
4703
|
+
workspacePath,
|
|
4704
|
+
stringifyYaml7({
|
|
4705
|
+
workspace: workspaceId,
|
|
4706
|
+
...typeof current?.profile === "string" ? { profile: current.profile } : {},
|
|
4707
|
+
...typeof current?.globalRoot === "string" ? { globalRoot: current.globalRoot } : { globalRoot: "~/.cnos" }
|
|
4708
|
+
}),
|
|
4709
|
+
"utf8"
|
|
4710
|
+
);
|
|
4711
|
+
}
|
|
4260
4712
|
async function runDetach(packageRoot, options = {}) {
|
|
4261
|
-
const loaded = await
|
|
4713
|
+
const loaded = await loadManifest9({ cwd: packageRoot });
|
|
4262
4714
|
if (!loaded.anchorPath || !loaded.anchoredWorkspace) {
|
|
4263
4715
|
throw new Error("workspace detach requires a package-local .cnosrc.yml with a workspace binding");
|
|
4264
4716
|
}
|
|
4265
4717
|
const targetCnosRoot = path23.join(packageRoot, ".cnos");
|
|
4266
4718
|
const force = consumeFlag([...options.cliArgs ?? []], "--force");
|
|
4267
|
-
if (await
|
|
4719
|
+
if (await exists2(targetCnosRoot) && !force) {
|
|
4268
4720
|
throw new Error(`Refusing to detach because ${displayPath(targetCnosRoot, packageRoot)} already exists. Use --force to overwrite.`);
|
|
4269
4721
|
}
|
|
4270
4722
|
if (force) {
|
|
@@ -4276,7 +4728,7 @@ async function runDetach(packageRoot, options = {}) {
|
|
|
4276
4728
|
workspace: loaded.anchoredWorkspace
|
|
4277
4729
|
});
|
|
4278
4730
|
const localRoots = runtime.graph.workspace.workspaceRoots.filter((root) => root.scope === "local").map((root) => root.path);
|
|
4279
|
-
await
|
|
4731
|
+
await mkdir7(targetCnosRoot, { recursive: true });
|
|
4280
4732
|
await mergeWorkspaceRootsIntoStandalone(targetCnosRoot, localRoots);
|
|
4281
4733
|
await writeFile9(
|
|
4282
4734
|
path23.join(targetCnosRoot, "cnos.yml"),
|
|
@@ -4294,7 +4746,7 @@ async function runDetach(packageRoot, options = {}) {
|
|
|
4294
4746
|
}
|
|
4295
4747
|
};
|
|
4296
4748
|
await writeFile9(path23.join(targetCnosRoot, ".detached"), stringifyYaml7(marker), "utf8");
|
|
4297
|
-
await
|
|
4749
|
+
await writeFile9(path23.join(packageRoot, ".cnosrc.yml"), stringifyYaml7({ root: "./.cnos" }), "utf8");
|
|
4298
4750
|
if (options.json) {
|
|
4299
4751
|
return printJson({
|
|
4300
4752
|
packageRoot,
|
|
@@ -4309,15 +4761,15 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
4309
4761
|
const force = consumeFlag(cliArgs, "--force");
|
|
4310
4762
|
const childCnosRoot = path23.join(packageRoot, ".cnos");
|
|
4311
4763
|
const markerPath = path23.join(childCnosRoot, ".detached");
|
|
4312
|
-
if (!await
|
|
4764
|
+
if (!await exists2(markerPath)) {
|
|
4313
4765
|
throw new Error("workspace attach requires a detached package with .cnos/.detached");
|
|
4314
4766
|
}
|
|
4315
|
-
const marker =
|
|
4767
|
+
const marker = parseYaml6(await readFile7(markerPath, "utf8"));
|
|
4316
4768
|
if (!marker?.originalCnosrc?.root || !marker.detachedWorkspace) {
|
|
4317
4769
|
throw new Error("Invalid .detached marker");
|
|
4318
4770
|
}
|
|
4319
4771
|
const parentManifestRoot = path23.resolve(packageRoot, marker.originalCnosrc.root);
|
|
4320
|
-
const parentLoaded = await
|
|
4772
|
+
const parentLoaded = await loadManifest9({ root: parentManifestRoot });
|
|
4321
4773
|
if (parentLoaded.rootResolution.readOnly) {
|
|
4322
4774
|
throw new Error(
|
|
4323
4775
|
`Cannot attach workspace because the parent CNOS root is remote and read-only (${parentLoaded.rootResolution.rootUri}).`
|
|
@@ -4325,17 +4777,17 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
4325
4777
|
}
|
|
4326
4778
|
const workspaceId = marker.originalCnosrc.workspace ?? marker.detachedWorkspace;
|
|
4327
4779
|
const parentWorkspaceRoot = path23.join(parentLoaded.manifestRoot, "workspaces", workspaceId);
|
|
4328
|
-
if (await
|
|
4780
|
+
if (await exists2(parentWorkspaceRoot) && !force) {
|
|
4329
4781
|
throw new Error(`workspace "${workspaceId}" already exists in parent root. Use --force to overwrite.`);
|
|
4330
4782
|
}
|
|
4331
4783
|
if (force) {
|
|
4332
4784
|
await rm5(parentWorkspaceRoot, { recursive: true, force: true });
|
|
4333
4785
|
}
|
|
4334
|
-
await
|
|
4786
|
+
await mkdir7(parentWorkspaceRoot, { recursive: true });
|
|
4335
4787
|
for (const folderName of ["values", "secrets", "env", "profiles"]) {
|
|
4336
4788
|
await copyIfExists(path23.join(childCnosRoot, folderName), path23.join(parentWorkspaceRoot, folderName));
|
|
4337
4789
|
}
|
|
4338
|
-
const rawManifest = parentLoaded.rawManifest;
|
|
4790
|
+
const rawManifest = structuredClone(parentLoaded.rawManifest);
|
|
4339
4791
|
const workspaces = rawManifest.workspaces ?? {};
|
|
4340
4792
|
const items = workspaces.items ?? {};
|
|
4341
4793
|
items[workspaceId] = items[workspaceId] ?? {};
|
|
@@ -4345,10 +4797,7 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
4345
4797
|
const archivePath = path23.join(packageRoot, ".cnos.detached.bak");
|
|
4346
4798
|
await rm5(archivePath, { recursive: true, force: true });
|
|
4347
4799
|
await rename(childCnosRoot, archivePath);
|
|
4348
|
-
await
|
|
4349
|
-
root: marker.originalCnosrc.root,
|
|
4350
|
-
...workspaceId ? { workspace: workspaceId } : {}
|
|
4351
|
-
});
|
|
4800
|
+
await writeAnchor(packageRoot, parentLoaded.manifestRoot, workspaceId);
|
|
4352
4801
|
if (options.json) {
|
|
4353
4802
|
return printJson({
|
|
4354
4803
|
packageRoot,
|
|
@@ -4359,15 +4808,210 @@ async function runAttach(packageRoot, options = {}) {
|
|
|
4359
4808
|
}
|
|
4360
4809
|
return `attached workspace ${workspaceId} to ${displayPath(parentLoaded.manifestRoot, packageRoot)}`;
|
|
4361
4810
|
}
|
|
4362
|
-
async function
|
|
4363
|
-
const
|
|
4811
|
+
async function runList2(manifestCwd, options = {}) {
|
|
4812
|
+
const loaded = await loadManifest9({
|
|
4813
|
+
...options.root ? { root: options.root } : {},
|
|
4814
|
+
cwd: manifestCwd,
|
|
4815
|
+
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
4816
|
+
});
|
|
4817
|
+
const entries = Object.entries(loaded.manifest.workspaces.items).map(([id, config]) => ({
|
|
4818
|
+
id,
|
|
4819
|
+
extends: config.extends,
|
|
4820
|
+
default: loaded.manifest.workspaces.default === id,
|
|
4821
|
+
path: path23.join(loaded.manifestRoot, "workspaces", id)
|
|
4822
|
+
})).sort((left, right) => left.id.localeCompare(right.id));
|
|
4823
|
+
if (options.json) {
|
|
4824
|
+
return printJson({
|
|
4825
|
+
default: loaded.manifest.workspaces.default,
|
|
4826
|
+
workspaces: entries
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
if (entries.length === 0) {
|
|
4830
|
+
return "no workspaces declared";
|
|
4831
|
+
}
|
|
4832
|
+
return entries.map((entry) => {
|
|
4833
|
+
const tags = [
|
|
4834
|
+
entry.default ? "default" : void 0,
|
|
4835
|
+
entry.extends.length > 0 ? `extends=${entry.extends.join(",")}` : void 0
|
|
4836
|
+
].filter(Boolean);
|
|
4837
|
+
return `${entry.id}${tags.length > 0 ? ` (${tags.join(", ")})` : ""}`;
|
|
4838
|
+
}).join("\n");
|
|
4839
|
+
}
|
|
4840
|
+
async function runEnable(manifestCwd, packageRoot, options = {}) {
|
|
4841
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
4842
|
+
if (cliArgs.length > 0) {
|
|
4843
|
+
throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
|
|
4844
|
+
}
|
|
4845
|
+
const loaded = await loadManifest9({
|
|
4846
|
+
...options.root ? { root: options.root } : {},
|
|
4847
|
+
cwd: manifestCwd,
|
|
4848
|
+
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
4849
|
+
});
|
|
4850
|
+
if (loaded.rootResolution.readOnly) {
|
|
4851
|
+
throw new Error(
|
|
4852
|
+
`Cannot enable workspace mode because the active CNOS root is remote and read-only (${loaded.rootResolution.rootUri}). Clone the config repo and edit it directly.`
|
|
4853
|
+
);
|
|
4854
|
+
}
|
|
4855
|
+
const rawManifest = structuredClone(loaded.rawManifest);
|
|
4856
|
+
const rawWorkspaces = rawManifest.workspaces ?? {};
|
|
4857
|
+
const rawItems = rawWorkspaces.items ?? {};
|
|
4858
|
+
if (Object.keys(rawItems).length > 0) {
|
|
4859
|
+
throw new Error("This CNOS root is already in workspace mode.");
|
|
4860
|
+
}
|
|
4861
|
+
const cnosRoot = loaded.manifestRoot;
|
|
4862
|
+
const baseWorkspaceRoot = path23.join(cnosRoot, "workspaces", "base");
|
|
4863
|
+
if (await exists2(baseWorkspaceRoot)) {
|
|
4864
|
+
throw new Error("Cannot enable workspace mode because .cnos/workspaces/base already exists.");
|
|
4865
|
+
}
|
|
4866
|
+
const moved = [];
|
|
4867
|
+
for (const folderName of ["values", "secrets", "env", "profiles"]) {
|
|
4868
|
+
if (await moveIfExists(path23.join(cnosRoot, folderName), path23.join(baseWorkspaceRoot, folderName))) {
|
|
4869
|
+
moved.push(folderName);
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
await ensureWorkspaceLayout(cnosRoot, "base");
|
|
4873
|
+
rawWorkspaces.default = "base";
|
|
4874
|
+
rawWorkspaces.items = {
|
|
4875
|
+
base: {}
|
|
4876
|
+
};
|
|
4877
|
+
rawManifest.workspaces = rawWorkspaces;
|
|
4878
|
+
await writeFile9(path23.join(cnosRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
|
|
4879
|
+
await updateRootAnchorToWorkspace(packageRoot, "base");
|
|
4880
|
+
await updateWorkspaceContext(packageRoot, "base");
|
|
4881
|
+
await ensureGitignore(path23.dirname(cnosRoot));
|
|
4882
|
+
if (options.json) {
|
|
4883
|
+
return printJson({
|
|
4884
|
+
root: path23.dirname(cnosRoot),
|
|
4885
|
+
workspace: "base",
|
|
4886
|
+
moved
|
|
4887
|
+
});
|
|
4888
|
+
}
|
|
4889
|
+
const movedSummary = moved.length > 0 ? `; moved ${moved.join(", ")} into .cnos/workspaces/base` : "";
|
|
4890
|
+
return `enabled workspace mode at ${displayPath(path23.dirname(cnosRoot), packageRoot)} with base workspace${movedSummary}`;
|
|
4891
|
+
}
|
|
4892
|
+
async function runAddOrScaffold(action, workspaceId, manifestCwd, packageRoot, options = {}) {
|
|
4893
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
4894
|
+
const extendsOption = splitExtends(consumeOption(cliArgs, "--extends"));
|
|
4895
|
+
const force = consumeFlag(cliArgs, "--force");
|
|
4896
|
+
if (cliArgs.length > 0) {
|
|
4897
|
+
throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
|
|
4898
|
+
}
|
|
4899
|
+
const loaded = await loadManifest9({
|
|
4900
|
+
...options.root ? { root: options.root } : {},
|
|
4901
|
+
cwd: manifestCwd,
|
|
4902
|
+
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
4903
|
+
});
|
|
4904
|
+
if (loaded.rootResolution.readOnly) {
|
|
4905
|
+
throw new Error(
|
|
4906
|
+
`Cannot ${action} workspace because the active CNOS root is remote and read-only (${loaded.rootResolution.rootUri}). Clone the config repo and edit it directly.`
|
|
4907
|
+
);
|
|
4908
|
+
}
|
|
4909
|
+
const manifestRoot = loaded.manifestRoot;
|
|
4910
|
+
const cnosRoot = manifestRoot;
|
|
4911
|
+
const rawManifest = structuredClone(loaded.rawManifest);
|
|
4912
|
+
const rawWorkspaces = rawManifest.workspaces ?? {};
|
|
4913
|
+
const rawItems = rawWorkspaces.items ?? {};
|
|
4914
|
+
const isWorkspaceMode = Object.keys(rawItems).length > 0;
|
|
4915
|
+
const directConfigPresent = await hasDirectConfigData(cnosRoot);
|
|
4916
|
+
if (!isWorkspaceMode || directConfigPresent) {
|
|
4917
|
+
throw new Error(
|
|
4918
|
+
"This CNOS root is not ready for child workspaces yet. Run `cnos workspace enable` first to convert the flat project into workspace mode."
|
|
4919
|
+
);
|
|
4920
|
+
}
|
|
4921
|
+
if (rawItems[workspaceId] && !force) {
|
|
4922
|
+
throw new Error(`workspace "${workspaceId}" already exists. Use --force to update its manifest entry and anchor.`);
|
|
4923
|
+
}
|
|
4924
|
+
const defaultExtends = extendsOption ?? (!["base", "root"].includes(workspaceId) && rawItems.base ? ["base"] : void 0);
|
|
4925
|
+
rawItems[workspaceId] = defaultExtends && defaultExtends.length > 0 ? { extends: defaultExtends } : {};
|
|
4926
|
+
rawWorkspaces.items = rawItems;
|
|
4927
|
+
rawWorkspaces.default = rawWorkspaces.default ?? workspaceId;
|
|
4928
|
+
rawManifest.workspaces = rawWorkspaces;
|
|
4929
|
+
const workspaceRoot = path23.join(cnosRoot, "workspaces", workspaceId);
|
|
4930
|
+
const created = await ensureWorkspaceLayout(cnosRoot, workspaceId);
|
|
4931
|
+
await writeFile9(path23.join(cnosRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
|
|
4932
|
+
await ensureGitignore(path23.dirname(cnosRoot));
|
|
4933
|
+
await writeAnchor(packageRoot, cnosRoot, workspaceId);
|
|
4934
|
+
await updateWorkspaceContext(packageRoot, workspaceId);
|
|
4935
|
+
const result = {
|
|
4936
|
+
workspace: workspaceId,
|
|
4937
|
+
root: path23.dirname(cnosRoot),
|
|
4938
|
+
packageRoot,
|
|
4939
|
+
created
|
|
4940
|
+
};
|
|
4941
|
+
if (options.json) {
|
|
4942
|
+
return printJson(result);
|
|
4943
|
+
}
|
|
4944
|
+
const verb = action === "add" ? "added" : "scaffolded";
|
|
4945
|
+
return `${verb} workspace ${workspaceId} at ${displayPath(workspaceRoot, packageRoot)}`;
|
|
4946
|
+
}
|
|
4947
|
+
async function runRemove(workspaceId, manifestCwd, options = {}) {
|
|
4364
4948
|
const cliArgs = [...options.cliArgs ?? []];
|
|
4365
|
-
|
|
4949
|
+
consumeFlag(cliArgs, "--force");
|
|
4950
|
+
if (cliArgs.length > 0) {
|
|
4951
|
+
throw new Error(`Unsupported workspace arguments: ${cliArgs.join(" ")}`);
|
|
4952
|
+
}
|
|
4953
|
+
const loaded = await loadManifest9({
|
|
4954
|
+
...options.root ? { root: options.root } : {},
|
|
4955
|
+
cwd: manifestCwd,
|
|
4956
|
+
...options.processEnv ? { processEnv: options.processEnv } : {}
|
|
4957
|
+
});
|
|
4958
|
+
if (loaded.rootResolution.readOnly) {
|
|
4959
|
+
throw new Error(
|
|
4960
|
+
`Cannot remove workspace because the active CNOS root is remote and read-only (${loaded.rootResolution.rootUri}). Clone the config repo and edit it directly.`
|
|
4961
|
+
);
|
|
4962
|
+
}
|
|
4963
|
+
const rawManifest = structuredClone(loaded.rawManifest);
|
|
4964
|
+
const rawWorkspaces = rawManifest.workspaces ?? {};
|
|
4965
|
+
const rawItems = rawWorkspaces.items ?? {};
|
|
4966
|
+
if (!rawItems[workspaceId]) {
|
|
4967
|
+
throw new Error(`workspace "${workspaceId}" does not exist`);
|
|
4968
|
+
}
|
|
4969
|
+
if (rawWorkspaces.default === workspaceId) {
|
|
4970
|
+
throw new Error(`Cannot remove workspace "${workspaceId}" because it is the default workspace. Change workspaces.default first.`);
|
|
4971
|
+
}
|
|
4972
|
+
delete rawItems[workspaceId];
|
|
4973
|
+
rawWorkspaces.items = rawItems;
|
|
4974
|
+
rawManifest.workspaces = rawWorkspaces;
|
|
4975
|
+
await writeFile9(path23.join(loaded.manifestRoot, "cnos.yml"), stringifyYaml7(rawManifest), "utf8");
|
|
4976
|
+
await rm5(path23.join(loaded.manifestRoot, "workspaces", workspaceId), { recursive: true, force: true });
|
|
4977
|
+
if (options.json) {
|
|
4978
|
+
return printJson({
|
|
4979
|
+
workspace: workspaceId,
|
|
4980
|
+
removedFrom: loaded.manifestRoot
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
return `removed workspace ${workspaceId}`;
|
|
4984
|
+
}
|
|
4985
|
+
async function runWorkspace(args = [], options = {}) {
|
|
4986
|
+
const [action, workspaceArg] = args;
|
|
4987
|
+
const baseCliArgs = [...options.cliArgs ?? []];
|
|
4988
|
+
const manifestCwd = path23.resolve(options.root ?? process.cwd());
|
|
4989
|
+
const packageRoot = path23.resolve(consumeOption(baseCliArgs, "--package-root") ?? options.root ?? process.cwd());
|
|
4366
4990
|
switch (action) {
|
|
4367
|
-
case "detach":
|
|
4368
|
-
return runDetach(packageRoot, { ...options, cliArgs });
|
|
4369
4991
|
case "attach":
|
|
4370
|
-
return runAttach(packageRoot, { ...options, cliArgs });
|
|
4992
|
+
return runAttach(packageRoot, { ...options, cliArgs: baseCliArgs });
|
|
4993
|
+
case "detach":
|
|
4994
|
+
return runDetach(packageRoot, { ...options, cliArgs: baseCliArgs });
|
|
4995
|
+
case "enable":
|
|
4996
|
+
return runEnable(manifestCwd, packageRoot, { ...options, cliArgs: baseCliArgs });
|
|
4997
|
+
case "list":
|
|
4998
|
+
return runList2(manifestCwd, options);
|
|
4999
|
+
case "add":
|
|
5000
|
+
return runAddOrScaffold("add", normalizeWorkspaceId(workspaceArg), manifestCwd, packageRoot, {
|
|
5001
|
+
...options,
|
|
5002
|
+
cliArgs: baseCliArgs
|
|
5003
|
+
});
|
|
5004
|
+
case "scaffold":
|
|
5005
|
+
return runAddOrScaffold("scaffold", normalizeWorkspaceId(workspaceArg), manifestCwd, packageRoot, {
|
|
5006
|
+
...options,
|
|
5007
|
+
cliArgs: baseCliArgs
|
|
5008
|
+
});
|
|
5009
|
+
case "remove":
|
|
5010
|
+
case "delete":
|
|
5011
|
+
return runRemove(normalizeWorkspaceId(workspaceArg), manifestCwd, {
|
|
5012
|
+
...options,
|
|
5013
|
+
cliArgs: baseCliArgs
|
|
5014
|
+
});
|
|
4371
5015
|
default:
|
|
4372
5016
|
throw new Error(`Unsupported workspace action: ${action ?? "(missing)"}`);
|
|
4373
5017
|
}
|
|
@@ -4393,8 +5037,8 @@ function resolveHelpTopic(command, args) {
|
|
|
4393
5037
|
if (command === "dev" && args[0] === "env") {
|
|
4394
5038
|
return normalizeHelpTopic([command, args[0]]);
|
|
4395
5039
|
}
|
|
4396
|
-
if (command === "workspace" && args[0] && ["attach", "detach"].includes(args[0])) {
|
|
4397
|
-
return normalizeHelpTopic([command, args[0]]);
|
|
5040
|
+
if (command === "workspace" && args[0] && ["attach", "detach", "add", "list", "remove", "delete", "scaffold", "enable"].includes(args[0])) {
|
|
5041
|
+
return normalizeHelpTopic([command, args[0] === "delete" ? "remove" : args[0]]);
|
|
4398
5042
|
}
|
|
4399
5043
|
if (command === "vault" && args[0] && ["create", "add", "list", "delete", "remove"].includes(args[0])) {
|
|
4400
5044
|
return normalizeHelpTopic([command, args[0] === "delete" ? "remove" : args[0] === "add" ? "create" : args[0]]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitsy/cnos-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "CLI entry point and developer tooling for CNOS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"
|
|
39
|
+
"smol-toml": "^1.4.2",
|
|
40
|
+
"@kitsy/cnos": "1.8.1"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"build": "tsup src/index.ts --format esm --dts",
|