@kitsy/cnos-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1209 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kitsy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,1209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/parseArgs.ts
|
|
4
|
+
var OPTION_KEYS = {
|
|
5
|
+
"--root": "root",
|
|
6
|
+
"--workspace": "workspace",
|
|
7
|
+
"--profile": "profile",
|
|
8
|
+
"--global-root": "globalRoot"
|
|
9
|
+
};
|
|
10
|
+
var COMMAND_OPTION_KEYS_WITH_VALUE = /* @__PURE__ */ new Set(["--format", "--framework", "--prefix", "--target", "--to"]);
|
|
11
|
+
var COMMAND_FLAG_KEYS = /* @__PURE__ */ new Set(["--flatten", "--public"]);
|
|
12
|
+
function setOption(options, key, value) {
|
|
13
|
+
options[key] = value;
|
|
14
|
+
}
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
17
|
+
return {
|
|
18
|
+
command: "help",
|
|
19
|
+
args: [],
|
|
20
|
+
options: {
|
|
21
|
+
cliArgs: []
|
|
22
|
+
},
|
|
23
|
+
passthrough: []
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const [command = "doctor", ...rest] = argv;
|
|
27
|
+
const options = {};
|
|
28
|
+
const args = [];
|
|
29
|
+
const cliArgs = [];
|
|
30
|
+
const passthrough = [];
|
|
31
|
+
let passthroughMode = false;
|
|
32
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
33
|
+
const token = rest[index];
|
|
34
|
+
if (!token) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (token === "--") {
|
|
38
|
+
passthroughMode = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (passthroughMode) {
|
|
42
|
+
passthrough.push(token);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (token === "--json") {
|
|
46
|
+
options.json = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (token === "--help" || token === "-h") {
|
|
50
|
+
options.help = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const optionKey = Object.keys(OPTION_KEYS).find(
|
|
54
|
+
(candidate) => token === candidate || token.startsWith(`${candidate}=`)
|
|
55
|
+
);
|
|
56
|
+
if (optionKey) {
|
|
57
|
+
const inlineValue = token.includes("=") ? token.slice(token.indexOf("=") + 1) : void 0;
|
|
58
|
+
const nextValue = inlineValue ?? rest[index + 1];
|
|
59
|
+
if (!nextValue) {
|
|
60
|
+
throw new Error(`Missing value for ${optionKey}`);
|
|
61
|
+
}
|
|
62
|
+
setOption(options, OPTION_KEYS[optionKey], nextValue);
|
|
63
|
+
if (!inlineValue) {
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (token.startsWith("--")) {
|
|
69
|
+
cliArgs.push(token);
|
|
70
|
+
const rawKey = token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
|
|
71
|
+
if (!token.includes("=") && COMMAND_OPTION_KEYS_WITH_VALUE.has(rawKey)) {
|
|
72
|
+
const nextValue = rest[index + 1];
|
|
73
|
+
if (!nextValue || nextValue.startsWith("--")) {
|
|
74
|
+
throw new Error(`Missing value for ${rawKey}`);
|
|
75
|
+
}
|
|
76
|
+
cliArgs.push(nextValue);
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (COMMAND_FLAG_KEYS.has(rawKey)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
args.push(token);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
command,
|
|
89
|
+
args,
|
|
90
|
+
options: {
|
|
91
|
+
...options,
|
|
92
|
+
cliArgs
|
|
93
|
+
},
|
|
94
|
+
passthrough
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/cli/commandOptions.ts
|
|
99
|
+
function consumeFlag(args, flag) {
|
|
100
|
+
const index = args.indexOf(flag);
|
|
101
|
+
if (index >= 0) {
|
|
102
|
+
args.splice(index, 1);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
function consumeOption(args, flag) {
|
|
108
|
+
const inline = args.find((entry) => entry.startsWith(`${flag}=`));
|
|
109
|
+
if (inline) {
|
|
110
|
+
args.splice(args.indexOf(inline), 1);
|
|
111
|
+
return inline.slice(flag.length + 1);
|
|
112
|
+
}
|
|
113
|
+
const index = args.indexOf(flag);
|
|
114
|
+
if (index >= 0) {
|
|
115
|
+
const value = args[index + 1];
|
|
116
|
+
if (!value) {
|
|
117
|
+
throw new Error(`Missing value for ${flag}`);
|
|
118
|
+
}
|
|
119
|
+
args.splice(index, 2);
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/format/printJson.ts
|
|
126
|
+
function printJson(value) {
|
|
127
|
+
return JSON.stringify(value, null, 2);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/services/writes.ts
|
|
131
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
132
|
+
import path from "path";
|
|
133
|
+
import { parseYaml, resolveWorkspaceScopedPath, stringifyYaml } from "@kitsy/cnos-core";
|
|
134
|
+
|
|
135
|
+
// src/services/runtime.ts
|
|
136
|
+
import { createCnos } from "@kitsy/cnos";
|
|
137
|
+
async function createRuntimeService(options = {}) {
|
|
138
|
+
const createOptions = {
|
|
139
|
+
root: options.root ?? process.cwd(),
|
|
140
|
+
...options.workspace ? {
|
|
141
|
+
workspace: options.workspace
|
|
142
|
+
} : {},
|
|
143
|
+
...options.profile ? {
|
|
144
|
+
profile: options.profile
|
|
145
|
+
} : {},
|
|
146
|
+
...options.globalRoot ? {
|
|
147
|
+
globalRoot: options.globalRoot
|
|
148
|
+
} : {},
|
|
149
|
+
...options.cliArgs && options.cliArgs.length > 0 ? {
|
|
150
|
+
cliArgs: options.cliArgs
|
|
151
|
+
} : {},
|
|
152
|
+
...options.processEnv ? {
|
|
153
|
+
processEnv: options.processEnv
|
|
154
|
+
} : {
|
|
155
|
+
processEnv: process.env
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
return createCnos({
|
|
159
|
+
...createOptions
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/services/writes.ts
|
|
164
|
+
function setNestedValue(target, pathSegments, value) {
|
|
165
|
+
const [head, ...tail] = pathSegments;
|
|
166
|
+
if (!head) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (tail.length === 0) {
|
|
170
|
+
target[head] = value;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const nextTarget = target[head] && typeof target[head] === "object" && !Array.isArray(target[head]) ? target[head] : {};
|
|
174
|
+
target[head] = nextTarget;
|
|
175
|
+
setNestedValue(nextTarget, tail, value);
|
|
176
|
+
}
|
|
177
|
+
function parseScalarValue(rawValue) {
|
|
178
|
+
try {
|
|
179
|
+
return parseYaml(rawValue);
|
|
180
|
+
} catch {
|
|
181
|
+
return rawValue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function defineValue(namespace, configPath, rawValue, options = {}) {
|
|
185
|
+
const runtime = await createRuntimeService(options);
|
|
186
|
+
const target = options.target ?? "local";
|
|
187
|
+
const workspaceRoot = runtime.graph.workspace.workspaceRoots.find(
|
|
188
|
+
(entry) => entry.scope === target && entry.workspaceId === runtime.graph.workspace.workspaceId
|
|
189
|
+
);
|
|
190
|
+
if (!workspaceRoot) {
|
|
191
|
+
throw new Error(`No ${target} workspace root is available for ${runtime.graph.workspace.workspaceId}`);
|
|
192
|
+
}
|
|
193
|
+
if (target === "global" && !runtime.manifest.workspaces.global.allowWrite) {
|
|
194
|
+
throw new Error("Global writes require workspaces.global.allowWrite: true");
|
|
195
|
+
}
|
|
196
|
+
const profile = options.profile ?? runtime.manifest.writePolicy.define.defaultProfile;
|
|
197
|
+
const template = runtime.manifest.writePolicy.define.targets[namespace];
|
|
198
|
+
const filePath = resolveWorkspaceScopedPath(workspaceRoot.path, template, {
|
|
199
|
+
workspace: runtime.graph.workspace.workspaceId,
|
|
200
|
+
profile
|
|
201
|
+
});
|
|
202
|
+
let document = {};
|
|
203
|
+
try {
|
|
204
|
+
document = parseYaml(await readFile(filePath, "utf8")) ?? {};
|
|
205
|
+
} catch {
|
|
206
|
+
document = {};
|
|
207
|
+
}
|
|
208
|
+
const parsedValue = parseScalarValue(rawValue);
|
|
209
|
+
setNestedValue(document, configPath.split("."), parsedValue);
|
|
210
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
211
|
+
await writeFile(filePath, stringifyYaml(document), "utf8");
|
|
212
|
+
return {
|
|
213
|
+
filePath,
|
|
214
|
+
value: parsedValue
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/commands/define.ts
|
|
219
|
+
async function runDefine(namespace, configPath, rawValue, options = {}) {
|
|
220
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
221
|
+
const target = consumeOption(cliArgs, "--target") ?? "local";
|
|
222
|
+
const result = await defineValue(namespace, configPath, rawValue, {
|
|
223
|
+
...options,
|
|
224
|
+
cliArgs,
|
|
225
|
+
target
|
|
226
|
+
});
|
|
227
|
+
if (options.json) {
|
|
228
|
+
return printJson({
|
|
229
|
+
namespace,
|
|
230
|
+
path: configPath,
|
|
231
|
+
target,
|
|
232
|
+
filePath: result.filePath,
|
|
233
|
+
value: result.value
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return `defined ${namespace}.${configPath} in ${result.filePath}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/commands/diff.ts
|
|
240
|
+
import { flattenObject } from "@kitsy/cnos-core";
|
|
241
|
+
function flattenRuntime(runtime) {
|
|
242
|
+
return {
|
|
243
|
+
...Object.fromEntries(
|
|
244
|
+
Object.entries(flattenObject(runtime.toNamespace("value"))).map(([key, value]) => [`value.${key}`, value])
|
|
245
|
+
),
|
|
246
|
+
...Object.fromEntries(
|
|
247
|
+
Object.entries(flattenObject(runtime.toNamespace("secret"))).map(([key, value]) => [`secret.${key}`, value])
|
|
248
|
+
)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function runDiff(leftProfile, rightProfile, options = {}) {
|
|
252
|
+
const leftRuntime = await createRuntimeService({
|
|
253
|
+
...options,
|
|
254
|
+
profile: leftProfile
|
|
255
|
+
});
|
|
256
|
+
const rightRuntime = await createRuntimeService({
|
|
257
|
+
...options,
|
|
258
|
+
profile: rightProfile
|
|
259
|
+
});
|
|
260
|
+
const left = flattenRuntime(leftRuntime);
|
|
261
|
+
const right = flattenRuntime(rightRuntime);
|
|
262
|
+
const keys = Array.from(/* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])).sort(
|
|
263
|
+
(a, b) => a.localeCompare(b)
|
|
264
|
+
);
|
|
265
|
+
const rows = keys.filter((key) => JSON.stringify(left[key]) !== JSON.stringify(right[key])).map((key) => ({
|
|
266
|
+
key,
|
|
267
|
+
left: left[key] ?? null,
|
|
268
|
+
right: right[key] ?? null
|
|
269
|
+
}));
|
|
270
|
+
if (options.json) {
|
|
271
|
+
return printJson(rows);
|
|
272
|
+
}
|
|
273
|
+
return rows.map((row) => `${row.key}: ${JSON.stringify(row.left)} -> ${JSON.stringify(row.right)}`).join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/services/doctor.ts
|
|
277
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
278
|
+
import path2 from "path";
|
|
279
|
+
|
|
280
|
+
// src/services/validation.ts
|
|
281
|
+
import { validateRuntime } from "@kitsy/cnos-core";
|
|
282
|
+
async function createValidationSummary(options = {}) {
|
|
283
|
+
const runtime = await createRuntimeService(options);
|
|
284
|
+
const summary = await validateRuntime(runtime);
|
|
285
|
+
return {
|
|
286
|
+
summary,
|
|
287
|
+
runtime
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/services/doctor.ts
|
|
292
|
+
async function checkGitignore(root) {
|
|
293
|
+
const gitignorePath = path2.join(root, ".gitignore");
|
|
294
|
+
const expected = [
|
|
295
|
+
"cnos/workspaces/*/secrets/",
|
|
296
|
+
"cnos/workspaces/*/env/.env",
|
|
297
|
+
"cnos/workspaces/*/env/.env.*",
|
|
298
|
+
"!cnos/workspaces/*/env/.env.example",
|
|
299
|
+
"!cnos/workspaces/*/env/.env.*.example"
|
|
300
|
+
];
|
|
301
|
+
try {
|
|
302
|
+
const content = await readFile2(gitignorePath, "utf8");
|
|
303
|
+
const missing = expected.filter((entry) => !content.includes(entry));
|
|
304
|
+
return {
|
|
305
|
+
name: "gitignore",
|
|
306
|
+
ok: missing.length === 0,
|
|
307
|
+
details: missing.length === 0 ? "workspace secrets and live env files are ignored while example env files stay trackable" : `missing: ${missing.join(", ")}`
|
|
308
|
+
};
|
|
309
|
+
} catch {
|
|
310
|
+
return {
|
|
311
|
+
name: "gitignore",
|
|
312
|
+
ok: false,
|
|
313
|
+
details: "missing .gitignore"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function issueSummary(issues) {
|
|
318
|
+
return issues.length === 0 ? "no issues" : issues.map((issue) => issue.message).join("; ");
|
|
319
|
+
}
|
|
320
|
+
async function evaluateDoctor(options = {}) {
|
|
321
|
+
const root = path2.resolve(options.root ?? process.cwd());
|
|
322
|
+
const { runtime, summary } = await createValidationSummary(options);
|
|
323
|
+
const localRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "local");
|
|
324
|
+
const globalRoot = runtime.graph.workspace.workspaceRoots.find((entry) => entry.scope === "global");
|
|
325
|
+
return [
|
|
326
|
+
{
|
|
327
|
+
name: "manifest",
|
|
328
|
+
ok: true,
|
|
329
|
+
details: `project=${runtime.manifest.project.name}`
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "workspace",
|
|
333
|
+
ok: true,
|
|
334
|
+
details: `${runtime.graph.workspace.workspaceId} via ${runtime.graph.workspace.workspaceSource}`
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "source-roots",
|
|
338
|
+
ok: Boolean(localRoot),
|
|
339
|
+
details: [localRoot?.path, globalRoot?.path].filter(Boolean).join(" | ")
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "validation",
|
|
343
|
+
ok: summary.valid,
|
|
344
|
+
details: issueSummary(summary.issues)
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "global-policy",
|
|
348
|
+
ok: !runtime.manifest.workspaces.global.enabled || Boolean(runtime.graph.workspace.globalRoot),
|
|
349
|
+
details: runtime.manifest.workspaces.global.enabled ? runtime.graph.workspace.globalRoot ? `enabled at ${runtime.graph.workspace.globalRoot}` : "enabled but no global root resolved" : "disabled"
|
|
350
|
+
},
|
|
351
|
+
await checkGitignore(root)
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/commands/doctor.ts
|
|
356
|
+
async function runDoctor(options = {}) {
|
|
357
|
+
const checks = await evaluateDoctor(options);
|
|
358
|
+
const hasFailures = checks.some((check) => !check.ok);
|
|
359
|
+
if (hasFailures) {
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
}
|
|
362
|
+
if (options.json) {
|
|
363
|
+
return printJson(checks);
|
|
364
|
+
}
|
|
365
|
+
return checks.map((check) => `${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.details}`).join("\n");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/commands/dump.ts
|
|
369
|
+
import { writeDump } from "@kitsy/cnos";
|
|
370
|
+
async function runDump(options = {}) {
|
|
371
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
372
|
+
const flatten = consumeFlag(cliArgs, "--flatten");
|
|
373
|
+
const to = consumeOption(cliArgs, "--to");
|
|
374
|
+
if (!to) {
|
|
375
|
+
throw new Error("dump requires --to <path>");
|
|
376
|
+
}
|
|
377
|
+
const runtime = await createRuntimeService({
|
|
378
|
+
...options,
|
|
379
|
+
cliArgs
|
|
380
|
+
});
|
|
381
|
+
const result = await writeDump(runtime.graph, {
|
|
382
|
+
to,
|
|
383
|
+
flatten
|
|
384
|
+
});
|
|
385
|
+
if (options.json) {
|
|
386
|
+
return printJson(result);
|
|
387
|
+
}
|
|
388
|
+
return `dumped ${result.files.length} files to ${result.root}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/commands/exportEnv.ts
|
|
392
|
+
async function runExportEnv(options = {}) {
|
|
393
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
394
|
+
const isPublic = consumeFlag(cliArgs, "--public");
|
|
395
|
+
const framework = consumeOption(cliArgs, "--framework");
|
|
396
|
+
const prefix = consumeOption(cliArgs, "--prefix");
|
|
397
|
+
const runtime = await createRuntimeService({
|
|
398
|
+
...options,
|
|
399
|
+
cliArgs
|
|
400
|
+
});
|
|
401
|
+
const env = isPublic ? runtime.toPublicEnv({
|
|
402
|
+
...framework ? { framework } : {},
|
|
403
|
+
...prefix ? { prefix } : {}
|
|
404
|
+
}) : runtime.toEnv();
|
|
405
|
+
if (options.json) {
|
|
406
|
+
return printJson(env);
|
|
407
|
+
}
|
|
408
|
+
return Object.entries(env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/commands/export.ts
|
|
412
|
+
async function runExport(subcommand, options = {}) {
|
|
413
|
+
if ((subcommand ?? "env") !== "env") {
|
|
414
|
+
throw new Error(`Unsupported export target: ${subcommand}`);
|
|
415
|
+
}
|
|
416
|
+
return runExportEnv(options);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/cli/helpRegistry.ts
|
|
420
|
+
var GLOBAL_OPTIONS = [
|
|
421
|
+
{
|
|
422
|
+
flag: "--root <path>",
|
|
423
|
+
description: "Resolve the CNOS project from a specific filesystem root."
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
flag: "--workspace <id>",
|
|
427
|
+
description: "Select the active workspace for this invocation."
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
flag: "--profile <name>",
|
|
431
|
+
description: "Override the active profile for reads, export, diff, and run."
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
flag: "--global-root <path>",
|
|
435
|
+
description: "Override the configured global CNOS root used for workspace layering."
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
flag: "--json",
|
|
439
|
+
description: "Emit JSON output for commands that support structured responses."
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
flag: "--help, -h",
|
|
443
|
+
description: "Show command help."
|
|
444
|
+
}
|
|
445
|
+
];
|
|
446
|
+
var COMMANDS = [
|
|
447
|
+
{
|
|
448
|
+
id: "init",
|
|
449
|
+
summary: "Scaffold a workspace-aware CNOS tree in the current project.",
|
|
450
|
+
usage: "cnos init [--workspace <id>] [--root <path>] [--json]",
|
|
451
|
+
description: "Creates cnos/cnos.yml, .cnos-workspace.yml, workspace folders, and .gitignore entries without overwriting existing files.",
|
|
452
|
+
examples: ["cnos init", "cnos init --workspace api", "cnos init --root ./apps/api --workspace api --json"]
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
id: "onboard",
|
|
456
|
+
summary: "Onboard an existing repo into CNOS and import root dotenv files.",
|
|
457
|
+
usage: "cnos onboard [--workspace <id>] [--root <path>] [--move] [--json]",
|
|
458
|
+
description: "Scaffolds the CNOS workspace tree and imports root-level .env, .env.<profile>, and .env.*.example files into cnos/workspaces/<workspace>/env.",
|
|
459
|
+
options: [
|
|
460
|
+
{
|
|
461
|
+
flag: "--move",
|
|
462
|
+
description: "Move the root env files into CNOS instead of leaving the originals in place."
|
|
463
|
+
}
|
|
464
|
+
],
|
|
465
|
+
examples: ["cnos onboard", "cnos onboard --workspace webapp", "cnos onboard --root ../my-app --workspace app --move"]
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: "read",
|
|
469
|
+
summary: "Read any fully-qualified CNOS key.",
|
|
470
|
+
usage: "cnos read <key> [global-options]",
|
|
471
|
+
description: "Reads a fully-qualified key such as value.app.name or secret.app.token.",
|
|
472
|
+
arguments: [
|
|
473
|
+
{
|
|
474
|
+
name: "key",
|
|
475
|
+
description: "Fully-qualified key to read.",
|
|
476
|
+
required: true
|
|
477
|
+
}
|
|
478
|
+
],
|
|
479
|
+
examples: ["cnos read value.app.name", "cnos read secret.app.token --workspace api"]
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "value",
|
|
483
|
+
summary: "Read a value namespace key without the value. prefix.",
|
|
484
|
+
usage: "cnos value <path> [global-options]",
|
|
485
|
+
description: "Reads value.<path> from the selected workspace and profile.",
|
|
486
|
+
arguments: [
|
|
487
|
+
{
|
|
488
|
+
name: "path",
|
|
489
|
+
description: "Value path without the value. namespace prefix.",
|
|
490
|
+
required: true
|
|
491
|
+
}
|
|
492
|
+
],
|
|
493
|
+
examples: ["cnos value app.name", "cnos value server.port --profile stage --workspace api"]
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: "secret",
|
|
497
|
+
summary: "Read a secret namespace key without the secret. prefix.",
|
|
498
|
+
usage: "cnos secret <path> [global-options]",
|
|
499
|
+
description: "Reads secret.<path> from the selected workspace and profile.",
|
|
500
|
+
arguments: [
|
|
501
|
+
{
|
|
502
|
+
name: "path",
|
|
503
|
+
description: "Secret path without the secret. namespace prefix.",
|
|
504
|
+
required: true
|
|
505
|
+
}
|
|
506
|
+
],
|
|
507
|
+
examples: ["cnos secret app.token", "cnos secret service.apiKey --workspace agents"]
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: "define",
|
|
511
|
+
summary: "Write a value or secret into the selected workspace.",
|
|
512
|
+
usage: "cnos define <value|secret> <path> <rawValue> [--target <local|global>] [global-options]",
|
|
513
|
+
description: "Writes deterministic YAML into the selected workspace. Global writes require allowWrite and an explicit --target global flag.",
|
|
514
|
+
arguments: [
|
|
515
|
+
{
|
|
516
|
+
name: "namespace",
|
|
517
|
+
description: "Either value or secret.",
|
|
518
|
+
required: true
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: "path",
|
|
522
|
+
description: "Path without the namespace prefix.",
|
|
523
|
+
required: true
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: "rawValue",
|
|
527
|
+
description: "Literal value to write.",
|
|
528
|
+
required: true
|
|
529
|
+
}
|
|
530
|
+
],
|
|
531
|
+
options: [
|
|
532
|
+
{
|
|
533
|
+
flag: "--target <local|global>",
|
|
534
|
+
description: "Choose whether the write lands in the local project workspace or the configured global root."
|
|
535
|
+
}
|
|
536
|
+
],
|
|
537
|
+
examples: [
|
|
538
|
+
"cnos define value server.port 3000 --workspace api",
|
|
539
|
+
"cnos define secret app.token super-secret --workspace api --target global"
|
|
540
|
+
]
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
id: "inspect",
|
|
544
|
+
summary: "Inspect the winning value and provenance for a key.",
|
|
545
|
+
usage: "cnos inspect <key> [global-options]",
|
|
546
|
+
description: "Shows the resolved value, namespace, active profile, workspace context, and the loader/origin that won precedence.",
|
|
547
|
+
arguments: [
|
|
548
|
+
{
|
|
549
|
+
name: "key",
|
|
550
|
+
description: "Fully-qualified key to inspect.",
|
|
551
|
+
required: true
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
examples: ["cnos inspect value.server.port", "cnos inspect secret.app.token --workspace api --json"]
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
id: "validate",
|
|
558
|
+
summary: "Validate schema, public promotion, and workspace safety rules.",
|
|
559
|
+
usage: "cnos validate [global-options]",
|
|
560
|
+
description: "Runs the CNOS validation pipeline and exits non-zero when validation issues are found.",
|
|
561
|
+
examples: ["cnos validate", "cnos validate --workspace api --profile stage --json"]
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: "export",
|
|
565
|
+
summary: "Export data from the selected workspace.",
|
|
566
|
+
usage: "cnos export <subcommand> [options] [global-options]",
|
|
567
|
+
description: "Currently supports env export for runtime and public environment projections.",
|
|
568
|
+
arguments: [
|
|
569
|
+
{
|
|
570
|
+
name: "subcommand",
|
|
571
|
+
description: "Supported value: env.",
|
|
572
|
+
required: true
|
|
573
|
+
}
|
|
574
|
+
],
|
|
575
|
+
examples: ["cnos export env", "cnos export env --public --framework vite --workspace api"]
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
id: "export env",
|
|
579
|
+
summary: "Render environment variables for the selected workspace.",
|
|
580
|
+
usage: "cnos export env [--public] [--framework <name>] [--prefix <prefix>] [global-options]",
|
|
581
|
+
description: "Exports the effective environment as KEY=VALUE lines, or only promoted public values when --public is set.",
|
|
582
|
+
options: [
|
|
583
|
+
{
|
|
584
|
+
flag: "--public",
|
|
585
|
+
description: "Export only public values based on manifest promotion rules."
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
flag: "--framework <name>",
|
|
589
|
+
description: "Apply framework-specific public env conventions such as vite or nextjs."
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
flag: "--prefix <prefix>",
|
|
593
|
+
description: "Override the generated public env prefix."
|
|
594
|
+
}
|
|
595
|
+
],
|
|
596
|
+
examples: ["cnos export env", "cnos export env --public --framework vite --workspace api"]
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
id: "dump",
|
|
600
|
+
summary: "Materialize the selected workspace into files.",
|
|
601
|
+
usage: "cnos dump --to <path> [--flatten] [global-options]",
|
|
602
|
+
description: "Writes the effective workspace snapshot to disk. Use --flatten to emit a standalone values/secrets tree instead of preserving workspace layout.",
|
|
603
|
+
options: [
|
|
604
|
+
{
|
|
605
|
+
flag: "--to <path>",
|
|
606
|
+
description: "Destination directory for the materialized snapshot."
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
flag: "--flatten",
|
|
610
|
+
description: "Write a flattened values/secrets tree instead of workspace-preserving output."
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
examples: ["cnos dump --to ./out", "cnos dump --to ./snapshot --flatten --workspace api"]
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
id: "run",
|
|
617
|
+
summary: "Run a child process with CNOS env injected.",
|
|
618
|
+
usage: "cnos run [global-options] -- <command...>",
|
|
619
|
+
description: "Resolves the active workspace and profile, injects runtime env variables, and executes the command after --.",
|
|
620
|
+
examples: ["cnos run -- node server.js", "cnos run --workspace api -- pnpm dev"]
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
id: "diff",
|
|
624
|
+
summary: "Diff two profiles for the same workspace.",
|
|
625
|
+
usage: "cnos diff <leftProfile> <rightProfile> [global-options]",
|
|
626
|
+
description: "Compares effective value and secret graphs between two profiles in the selected workspace.",
|
|
627
|
+
arguments: [
|
|
628
|
+
{
|
|
629
|
+
name: "leftProfile",
|
|
630
|
+
description: "Baseline profile name.",
|
|
631
|
+
required: true
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: "rightProfile",
|
|
635
|
+
description: "Comparison profile name.",
|
|
636
|
+
required: true
|
|
637
|
+
}
|
|
638
|
+
],
|
|
639
|
+
examples: ["cnos diff local stage --workspace api", "cnos diff stage prod --workspace api --json"]
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: "doctor",
|
|
643
|
+
summary: "Run repository and workspace diagnostics.",
|
|
644
|
+
usage: "cnos doctor [global-options]",
|
|
645
|
+
description: "Checks manifest/workspace setup, gitignore coverage, and related diagnostics for the selected workspace.",
|
|
646
|
+
examples: ["cnos doctor", "cnos doctor --workspace api --json"]
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: "help",
|
|
650
|
+
summary: "Show human-readable CLI help.",
|
|
651
|
+
usage: "cnos help [command]",
|
|
652
|
+
description: "Prints either the root command list or detailed help for a specific command.",
|
|
653
|
+
arguments: [
|
|
654
|
+
{
|
|
655
|
+
name: "command",
|
|
656
|
+
description: "Optional command or subcommand, for example export env."
|
|
657
|
+
}
|
|
658
|
+
],
|
|
659
|
+
examples: ["cnos help", "cnos help define", "cnos help export env"]
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
id: "help-ai",
|
|
663
|
+
summary: "Show machine-readable CLI help for agents.",
|
|
664
|
+
usage: "cnos help-ai [command] [--format <json|text>]",
|
|
665
|
+
description: "Prints structured CLI help intended for automation and agents. JSON is the default format.",
|
|
666
|
+
arguments: [
|
|
667
|
+
{
|
|
668
|
+
name: "command",
|
|
669
|
+
description: "Optional command or subcommand, for example export env."
|
|
670
|
+
}
|
|
671
|
+
],
|
|
672
|
+
options: [
|
|
673
|
+
{
|
|
674
|
+
flag: "--format <json|text>",
|
|
675
|
+
description: "Select output format. Defaults to json."
|
|
676
|
+
}
|
|
677
|
+
],
|
|
678
|
+
examples: ["cnos help-ai --format json", "cnos help-ai export env --format json"]
|
|
679
|
+
}
|
|
680
|
+
];
|
|
681
|
+
var HELP_DOCUMENT = {
|
|
682
|
+
name: "cnos",
|
|
683
|
+
summary: "Workspace-aware configuration runtime and CLI for local, global, and promoted environment data.",
|
|
684
|
+
usage: "cnos <command> [args] [options]",
|
|
685
|
+
description: "CNOS resolves one active workspace per invocation, layers local and optional global config roots, and exposes read, write, export, dump, validation, and diagnostics commands.",
|
|
686
|
+
globalOptions: GLOBAL_OPTIONS,
|
|
687
|
+
commands: COMMANDS,
|
|
688
|
+
examples: ["cnos doctor --workspace api", "cnos export env --public --framework vite", "cnos help-ai --format json"]
|
|
689
|
+
};
|
|
690
|
+
function normalizeHelpTopic(parts) {
|
|
691
|
+
const cleaned = parts.filter((part) => part.length > 0);
|
|
692
|
+
if (cleaned.length === 0) {
|
|
693
|
+
return void 0;
|
|
694
|
+
}
|
|
695
|
+
const candidate = cleaned.join(" ");
|
|
696
|
+
if (COMMANDS.some((command) => command.id === candidate)) {
|
|
697
|
+
return candidate;
|
|
698
|
+
}
|
|
699
|
+
throw new Error(`Unknown help topic: ${candidate}`);
|
|
700
|
+
}
|
|
701
|
+
function findHelpCommand(topic) {
|
|
702
|
+
if (!topic) {
|
|
703
|
+
return void 0;
|
|
704
|
+
}
|
|
705
|
+
return COMMANDS.find((command) => command.id === topic);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/commands/help.ts
|
|
709
|
+
function formatOptions(title, options) {
|
|
710
|
+
if (!options || options.length === 0) {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
return [title, ...options.map((option) => ` ${option.flag.padEnd(24, " ")} ${option.description}`)];
|
|
714
|
+
}
|
|
715
|
+
function formatCommandHelp(command) {
|
|
716
|
+
const lines = [`Usage: ${command.usage}`, "", command.summary, "", command.description];
|
|
717
|
+
if (command.arguments && command.arguments.length > 0) {
|
|
718
|
+
lines.push("", "Arguments");
|
|
719
|
+
lines.push(
|
|
720
|
+
...command.arguments.map((argument) => {
|
|
721
|
+
const suffix = argument.required ? " (required)" : "";
|
|
722
|
+
return ` ${argument.name}${suffix}: ${argument.description}`;
|
|
723
|
+
})
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
lines.push(...command.options && command.options.length > 0 ? ["", ...formatOptions("Options", command.options)] : []);
|
|
727
|
+
lines.push("", ...formatOptions("Global options", HELP_DOCUMENT.globalOptions));
|
|
728
|
+
if (command.examples && command.examples.length > 0) {
|
|
729
|
+
lines.push("", "Examples", ...command.examples.map((example) => ` ${example}`));
|
|
730
|
+
}
|
|
731
|
+
return lines.join("\n");
|
|
732
|
+
}
|
|
733
|
+
function formatRootHelp() {
|
|
734
|
+
const lines = [
|
|
735
|
+
HELP_DOCUMENT.summary,
|
|
736
|
+
"",
|
|
737
|
+
`Usage: ${HELP_DOCUMENT.usage}`,
|
|
738
|
+
"",
|
|
739
|
+
"Commands",
|
|
740
|
+
...HELP_DOCUMENT.commands.filter((command) => !command.id.includes(" ")).map((command) => ` ${command.id.padEnd(12, " ")} ${command.summary}`),
|
|
741
|
+
"",
|
|
742
|
+
...formatOptions("Global options", HELP_DOCUMENT.globalOptions),
|
|
743
|
+
"",
|
|
744
|
+
"Examples",
|
|
745
|
+
...HELP_DOCUMENT.examples.map((example) => ` ${example}`)
|
|
746
|
+
];
|
|
747
|
+
return lines.join("\n");
|
|
748
|
+
}
|
|
749
|
+
function runHelp(topic) {
|
|
750
|
+
const command = findHelpCommand(topic);
|
|
751
|
+
if (!command) {
|
|
752
|
+
return formatRootHelp();
|
|
753
|
+
}
|
|
754
|
+
return formatCommandHelp(command);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/commands/helpAi.ts
|
|
758
|
+
function runHelpAi(topic, cliArgs = []) {
|
|
759
|
+
const helpArgs = [...cliArgs];
|
|
760
|
+
const format = consumeOption(helpArgs, "--format") ?? "json";
|
|
761
|
+
const command = topic ? findHelpCommand(topic) : void 0;
|
|
762
|
+
if (helpArgs.length > 0) {
|
|
763
|
+
throw new Error(`Unsupported help-ai arguments: ${helpArgs.join(" ")}`);
|
|
764
|
+
}
|
|
765
|
+
const payload = topic ? {
|
|
766
|
+
cli: HELP_DOCUMENT.name,
|
|
767
|
+
summary: HELP_DOCUMENT.summary,
|
|
768
|
+
globalOptions: HELP_DOCUMENT.globalOptions,
|
|
769
|
+
command
|
|
770
|
+
} : {
|
|
771
|
+
cli: HELP_DOCUMENT.name,
|
|
772
|
+
summary: HELP_DOCUMENT.summary,
|
|
773
|
+
usage: HELP_DOCUMENT.usage,
|
|
774
|
+
description: HELP_DOCUMENT.description,
|
|
775
|
+
globalOptions: HELP_DOCUMENT.globalOptions,
|
|
776
|
+
commands: HELP_DOCUMENT.commands
|
|
777
|
+
};
|
|
778
|
+
if (topic && !command) {
|
|
779
|
+
throw new Error(`Unknown help topic: ${topic}`);
|
|
780
|
+
}
|
|
781
|
+
if (format === "text") {
|
|
782
|
+
return topic ? `cnos help-ai ${topic} emits JSON by default. Re-run with --format json for structured output.` : "cnos help-ai emits JSON by default. Re-run with --format json for structured output.";
|
|
783
|
+
}
|
|
784
|
+
if (format !== "json") {
|
|
785
|
+
throw new Error(`Unsupported help-ai format: ${format}`);
|
|
786
|
+
}
|
|
787
|
+
return printJson(payload);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/commands/init.ts
|
|
791
|
+
import path4 from "path";
|
|
792
|
+
|
|
793
|
+
// src/services/scaffold.ts
|
|
794
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
795
|
+
import path3 from "path";
|
|
796
|
+
function scaffoldManifest(projectName, workspace) {
|
|
797
|
+
return [
|
|
798
|
+
"version: 1",
|
|
799
|
+
"project:",
|
|
800
|
+
` name: ${projectName}`,
|
|
801
|
+
"workspaces:",
|
|
802
|
+
` default: ${workspace}`,
|
|
803
|
+
" global:",
|
|
804
|
+
" enabled: false",
|
|
805
|
+
" allowWrite: false",
|
|
806
|
+
" items:",
|
|
807
|
+
` ${workspace}: {}`,
|
|
808
|
+
"profiles:",
|
|
809
|
+
" default: local",
|
|
810
|
+
"envMapping:",
|
|
811
|
+
" convention: SCREAMING_SNAKE",
|
|
812
|
+
"public:",
|
|
813
|
+
" promote: []",
|
|
814
|
+
""
|
|
815
|
+
].join("\n");
|
|
816
|
+
}
|
|
817
|
+
async function ensureFile(filePath, content) {
|
|
818
|
+
try {
|
|
819
|
+
await readFile3(filePath, "utf8");
|
|
820
|
+
return false;
|
|
821
|
+
} catch {
|
|
822
|
+
await writeFile2(filePath, content, "utf8");
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
async function ensureGitignore(root) {
|
|
827
|
+
const gitignorePath = path3.join(root, ".gitignore");
|
|
828
|
+
const requiredEntries = [
|
|
829
|
+
"cnos/workspaces/*/secrets/",
|
|
830
|
+
"cnos/workspaces/*/env/.env",
|
|
831
|
+
"cnos/workspaces/*/env/.env.*",
|
|
832
|
+
"!cnos/workspaces/*/env/.env.example",
|
|
833
|
+
"!cnos/workspaces/*/env/.env.*.example"
|
|
834
|
+
];
|
|
835
|
+
let current = "";
|
|
836
|
+
try {
|
|
837
|
+
current = await readFile3(gitignorePath, "utf8");
|
|
838
|
+
} catch {
|
|
839
|
+
current = "";
|
|
840
|
+
}
|
|
841
|
+
const missingEntries = requiredEntries.filter((entry) => !current.includes(entry));
|
|
842
|
+
if (missingEntries.length === 0) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
const prefix = current.trim().length > 0 ? `${current.trimEnd()}
|
|
846
|
+
` : "";
|
|
847
|
+
await writeFile2(gitignorePath, `${prefix}${missingEntries.join("\n")}
|
|
848
|
+
`, "utf8");
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
851
|
+
async function scaffoldWorkspace(root, workspace) {
|
|
852
|
+
const cnosRoot = path3.join(root, "cnos");
|
|
853
|
+
const workspaceRoot = path3.join(cnosRoot, "workspaces", workspace);
|
|
854
|
+
const createdPaths = [];
|
|
855
|
+
await mkdir2(path3.join(workspaceRoot, "profiles"), { recursive: true });
|
|
856
|
+
await mkdir2(path3.join(workspaceRoot, "values", "local"), { recursive: true });
|
|
857
|
+
await mkdir2(path3.join(workspaceRoot, "secrets", "local"), { recursive: true });
|
|
858
|
+
await mkdir2(path3.join(workspaceRoot, "env"), { recursive: true });
|
|
859
|
+
for (const relativePath of [
|
|
860
|
+
["workspaces", workspace, "profiles", ".gitkeep"],
|
|
861
|
+
["workspaces", workspace, "values", "local", ".gitkeep"],
|
|
862
|
+
["workspaces", workspace, "secrets", "local", ".gitkeep"],
|
|
863
|
+
["workspaces", workspace, "env", ".gitkeep"]
|
|
864
|
+
]) {
|
|
865
|
+
const filePath = path3.join(cnosRoot, ...relativePath);
|
|
866
|
+
if (await ensureFile(filePath, "")) {
|
|
867
|
+
createdPaths.push(path3.relative(root, filePath).replace(/\\/g, "/"));
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (await ensureFile(path3.join(cnosRoot, "cnos.yml"), scaffoldManifest(path3.basename(root), workspace))) {
|
|
871
|
+
createdPaths.push("cnos/cnos.yml");
|
|
872
|
+
}
|
|
873
|
+
if (await ensureFile(path3.join(root, ".cnos-workspace.yml"), `workspace: ${workspace}
|
|
874
|
+
globalRoot: ~/.cnos
|
|
875
|
+
`)) {
|
|
876
|
+
createdPaths.push(".cnos-workspace.yml");
|
|
877
|
+
}
|
|
878
|
+
if (await ensureGitignore(root)) {
|
|
879
|
+
createdPaths.push(".gitignore");
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
root,
|
|
883
|
+
workspace,
|
|
884
|
+
created: createdPaths
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/commands/init.ts
|
|
889
|
+
async function runInit(options = {}) {
|
|
890
|
+
const root = path4.resolve(options.root ?? process.cwd());
|
|
891
|
+
const workspace = options.workspace ?? path4.basename(root);
|
|
892
|
+
const result = await scaffoldWorkspace(root, workspace);
|
|
893
|
+
if (options.json) {
|
|
894
|
+
return printJson(result);
|
|
895
|
+
}
|
|
896
|
+
return `initialized CNOS workspace ${workspace} at ${root}`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/format/printInspect.ts
|
|
900
|
+
function printInspect(record) {
|
|
901
|
+
const lines = [
|
|
902
|
+
`key: ${record.key}`,
|
|
903
|
+
`value: ${String(record.value)}`,
|
|
904
|
+
`namespace: ${record.namespace}`,
|
|
905
|
+
`profile: ${record.profile} (${record.profileSource})`,
|
|
906
|
+
`workspace: ${record.workspace.id} (${record.workspace.source})`,
|
|
907
|
+
`workspaceChain: ${record.workspace.chain.join(" -> ")}`,
|
|
908
|
+
`winner: ${record.winner.sourceId} via ${record.winner.pluginId} @ ${record.winner.workspaceId}`
|
|
909
|
+
];
|
|
910
|
+
if (record.winner.origin?.file) {
|
|
911
|
+
lines.push(`winnerOrigin: ${record.winner.origin.file}`);
|
|
912
|
+
}
|
|
913
|
+
if (record.overridden.length > 0) {
|
|
914
|
+
lines.push(
|
|
915
|
+
`overridden: ${record.overridden.map((entry) => `${entry.sourceId}@${entry.workspaceId}=${String(entry.value)}`).join(", ")}`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
return lines.join("\n");
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/commands/inspect.ts
|
|
922
|
+
async function runInspect(key, options = {}) {
|
|
923
|
+
const runtime = await createRuntimeService(options);
|
|
924
|
+
const inspectResult = runtime.inspect(key);
|
|
925
|
+
if (options.json) {
|
|
926
|
+
return printJson(inspectResult);
|
|
927
|
+
}
|
|
928
|
+
return printInspect(inspectResult);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/commands/onboard.ts
|
|
932
|
+
import { copyFile, readdir, rm } from "fs/promises";
|
|
933
|
+
import path5 from "path";
|
|
934
|
+
var ROOT_ENV_FILE_PATTERN = /^\.env(?:\.[A-Za-z0-9_-]+)*(?:\.example)?$/;
|
|
935
|
+
async function listRootEnvFiles(root) {
|
|
936
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
937
|
+
return entries.filter((entry) => entry.isFile() && ROOT_ENV_FILE_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
938
|
+
}
|
|
939
|
+
async function runOnboard(options = {}) {
|
|
940
|
+
const root = path5.resolve(options.root ?? process.cwd());
|
|
941
|
+
const workspace = options.workspace ?? path5.basename(root);
|
|
942
|
+
const cliArgs = [...options.cliArgs ?? []];
|
|
943
|
+
const move = consumeFlag(cliArgs, "--move");
|
|
944
|
+
if (cliArgs.length > 0) {
|
|
945
|
+
throw new Error(`Unsupported onboard arguments: ${cliArgs.join(" ")}`);
|
|
946
|
+
}
|
|
947
|
+
const scaffold = await scaffoldWorkspace(root, workspace);
|
|
948
|
+
const envRoot = path5.join(root, "cnos", "workspaces", workspace, "env");
|
|
949
|
+
const rootFiles = await listRootEnvFiles(root);
|
|
950
|
+
const imported = [];
|
|
951
|
+
const skipped = [];
|
|
952
|
+
for (const fileName of rootFiles) {
|
|
953
|
+
const sourcePath = path5.join(root, fileName);
|
|
954
|
+
const targetPath = path5.join(envRoot, fileName);
|
|
955
|
+
try {
|
|
956
|
+
await copyFile(sourcePath, targetPath);
|
|
957
|
+
imported.push(path5.relative(root, targetPath).replace(/\\/g, "/"));
|
|
958
|
+
if (move) {
|
|
959
|
+
await rm(sourcePath);
|
|
960
|
+
}
|
|
961
|
+
} catch {
|
|
962
|
+
skipped.push(fileName);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const result = {
|
|
966
|
+
root,
|
|
967
|
+
workspace,
|
|
968
|
+
scaffolded: scaffold.created,
|
|
969
|
+
imported,
|
|
970
|
+
skipped,
|
|
971
|
+
mode: move ? "move" : "copy"
|
|
972
|
+
};
|
|
973
|
+
if (options.json) {
|
|
974
|
+
return printJson(result);
|
|
975
|
+
}
|
|
976
|
+
const importedCount = imported.length;
|
|
977
|
+
const skippedSuffix = skipped.length > 0 ? ` (${skipped.length} skipped)` : "";
|
|
978
|
+
return `onboarded ${workspace} at ${root}; imported ${importedCount} root env files into cnos/workspaces/${workspace}/env using ${result.mode}${skippedSuffix}`;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/format/printValue.ts
|
|
982
|
+
function printValue(value, json = false) {
|
|
983
|
+
if (json) {
|
|
984
|
+
return printJson(value);
|
|
985
|
+
}
|
|
986
|
+
if (typeof value === "string") {
|
|
987
|
+
return value;
|
|
988
|
+
}
|
|
989
|
+
if (value === void 0 || value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
990
|
+
return String(value);
|
|
991
|
+
}
|
|
992
|
+
return printJson(value);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/commands/read.ts
|
|
996
|
+
async function runRead(key, options = {}) {
|
|
997
|
+
const runtime = await createRuntimeService(options);
|
|
998
|
+
const value = runtime.read(key);
|
|
999
|
+
if (value === void 0) {
|
|
1000
|
+
throw new Error(`Missing CNOS config key: ${key}`);
|
|
1001
|
+
}
|
|
1002
|
+
if (options.json) {
|
|
1003
|
+
return printJson({ key, value });
|
|
1004
|
+
}
|
|
1005
|
+
return printValue(value);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/commands/run.ts
|
|
1009
|
+
import { spawn } from "child_process";
|
|
1010
|
+
async function runCommand(command, options = {}) {
|
|
1011
|
+
if (command.length === 0) {
|
|
1012
|
+
throw new Error("run requires a command after --");
|
|
1013
|
+
}
|
|
1014
|
+
const runtime = await createRuntimeService(options);
|
|
1015
|
+
const env = {
|
|
1016
|
+
...process.env,
|
|
1017
|
+
...runtime.toEnv()
|
|
1018
|
+
};
|
|
1019
|
+
return new Promise((resolve, reject) => {
|
|
1020
|
+
const executable = command[0];
|
|
1021
|
+
if (!executable) {
|
|
1022
|
+
reject(new Error("run requires a command after --"));
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const child = spawn(executable, command.slice(1), {
|
|
1026
|
+
cwd: options.root ?? process.cwd(),
|
|
1027
|
+
env,
|
|
1028
|
+
stdio: options.stdio === "pipe" ? "pipe" : "inherit",
|
|
1029
|
+
shell: false
|
|
1030
|
+
});
|
|
1031
|
+
let stdout = "";
|
|
1032
|
+
let stderr = "";
|
|
1033
|
+
if (options.stdio === "pipe") {
|
|
1034
|
+
child.stdout?.on("data", (chunk) => {
|
|
1035
|
+
stdout += chunk.toString();
|
|
1036
|
+
});
|
|
1037
|
+
child.stderr?.on("data", (chunk) => {
|
|
1038
|
+
stderr += chunk.toString();
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
child.on("error", reject);
|
|
1042
|
+
child.on("close", (code) => {
|
|
1043
|
+
resolve({
|
|
1044
|
+
exitCode: code ?? 1,
|
|
1045
|
+
stdout,
|
|
1046
|
+
stderr
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/commands/secret.ts
|
|
1053
|
+
async function runSecret(path6, options = {}) {
|
|
1054
|
+
const runtime = await createRuntimeService(options);
|
|
1055
|
+
const value = runtime.secret(path6);
|
|
1056
|
+
if (value === void 0) {
|
|
1057
|
+
throw new Error(`Missing CNOS secret path: ${path6}`);
|
|
1058
|
+
}
|
|
1059
|
+
if (options.json) {
|
|
1060
|
+
return printJson({
|
|
1061
|
+
key: `secret.${path6}`,
|
|
1062
|
+
value
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return printValue(value);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/commands/validate.ts
|
|
1069
|
+
async function runValidate(options = {}) {
|
|
1070
|
+
const { summary } = await createValidationSummary(options);
|
|
1071
|
+
if (!summary.valid) {
|
|
1072
|
+
process.exitCode = 1;
|
|
1073
|
+
}
|
|
1074
|
+
if (options.json) {
|
|
1075
|
+
return printJson(summary);
|
|
1076
|
+
}
|
|
1077
|
+
return summary.valid ? "validation passed" : summary.issues.map((issue) => `${issue.code}: ${issue.message}`).join("\n");
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/commands/value.ts
|
|
1081
|
+
async function runValue(path6, options = {}) {
|
|
1082
|
+
const runtime = await createRuntimeService(options);
|
|
1083
|
+
const value = runtime.value(path6);
|
|
1084
|
+
if (value === void 0) {
|
|
1085
|
+
throw new Error(`Missing CNOS value path: ${path6}`);
|
|
1086
|
+
}
|
|
1087
|
+
if (options.json) {
|
|
1088
|
+
return printJson({
|
|
1089
|
+
key: `value.${path6}`,
|
|
1090
|
+
value
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
return printValue(value);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/index.ts
|
|
1097
|
+
function resolveHelpTopic(command, args) {
|
|
1098
|
+
if (command === "help" || command === "help-ai") {
|
|
1099
|
+
return normalizeHelpTopic(args);
|
|
1100
|
+
}
|
|
1101
|
+
if (command === "export" && args[0] === "env") {
|
|
1102
|
+
return normalizeHelpTopic([command, args[0]]);
|
|
1103
|
+
}
|
|
1104
|
+
return normalizeHelpTopic([command]);
|
|
1105
|
+
}
|
|
1106
|
+
async function main(argv) {
|
|
1107
|
+
const { command, args, options, passthrough } = parseArgs(argv);
|
|
1108
|
+
if (options.help) {
|
|
1109
|
+
process.stdout.write(`${runHelp(resolveHelpTopic(command, args))}
|
|
1110
|
+
`);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const runtimeOptions = {
|
|
1114
|
+
...options.root ? {
|
|
1115
|
+
root: options.root
|
|
1116
|
+
} : {},
|
|
1117
|
+
...options.workspace ? {
|
|
1118
|
+
workspace: options.workspace
|
|
1119
|
+
} : {},
|
|
1120
|
+
...options.profile ? {
|
|
1121
|
+
profile: options.profile
|
|
1122
|
+
} : {},
|
|
1123
|
+
...options.globalRoot ? {
|
|
1124
|
+
globalRoot: options.globalRoot
|
|
1125
|
+
} : {},
|
|
1126
|
+
...options.json ? {
|
|
1127
|
+
json: true
|
|
1128
|
+
} : {},
|
|
1129
|
+
...options.cliArgs.length > 0 ? {
|
|
1130
|
+
cliArgs: options.cliArgs
|
|
1131
|
+
} : {}
|
|
1132
|
+
};
|
|
1133
|
+
switch (command) {
|
|
1134
|
+
case "help":
|
|
1135
|
+
process.stdout.write(`${runHelp(resolveHelpTopic(command, args))}
|
|
1136
|
+
`);
|
|
1137
|
+
return;
|
|
1138
|
+
case "help-ai":
|
|
1139
|
+
process.stdout.write(`${runHelpAi(resolveHelpTopic(command, args), options.cliArgs)}
|
|
1140
|
+
`);
|
|
1141
|
+
return;
|
|
1142
|
+
case "init":
|
|
1143
|
+
process.stdout.write(`${await runInit(runtimeOptions)}
|
|
1144
|
+
`);
|
|
1145
|
+
return;
|
|
1146
|
+
case "onboard":
|
|
1147
|
+
process.stdout.write(`${await runOnboard(runtimeOptions)}
|
|
1148
|
+
`);
|
|
1149
|
+
return;
|
|
1150
|
+
case "read":
|
|
1151
|
+
process.stdout.write(`${await runRead(args[0] ?? "value.app.name", runtimeOptions)}
|
|
1152
|
+
`);
|
|
1153
|
+
return;
|
|
1154
|
+
case "value":
|
|
1155
|
+
process.stdout.write(`${await runValue(args[0] ?? "app.name", runtimeOptions)}
|
|
1156
|
+
`);
|
|
1157
|
+
return;
|
|
1158
|
+
case "secret":
|
|
1159
|
+
process.stdout.write(`${await runSecret(args[0] ?? "app.token", runtimeOptions)}
|
|
1160
|
+
`);
|
|
1161
|
+
return;
|
|
1162
|
+
case "define":
|
|
1163
|
+
process.stdout.write(
|
|
1164
|
+
`${await runDefine(args[0] ?? "value", args[1] ?? "app.name", args[2] ?? "", runtimeOptions)}
|
|
1165
|
+
`
|
|
1166
|
+
);
|
|
1167
|
+
return;
|
|
1168
|
+
case "inspect":
|
|
1169
|
+
process.stdout.write(`${await runInspect(args[0] ?? "meta.profile", runtimeOptions)}
|
|
1170
|
+
`);
|
|
1171
|
+
return;
|
|
1172
|
+
case "validate":
|
|
1173
|
+
process.stdout.write(`${await runValidate(runtimeOptions)}
|
|
1174
|
+
`);
|
|
1175
|
+
return;
|
|
1176
|
+
case "export":
|
|
1177
|
+
process.stdout.write(`${await runExport(args[0], runtimeOptions)}
|
|
1178
|
+
`);
|
|
1179
|
+
return;
|
|
1180
|
+
case "dump":
|
|
1181
|
+
process.stdout.write(`${await runDump(runtimeOptions)}
|
|
1182
|
+
`);
|
|
1183
|
+
return;
|
|
1184
|
+
case "run": {
|
|
1185
|
+
const result = await runCommand(passthrough.length > 0 ? passthrough : args, {
|
|
1186
|
+
...runtimeOptions,
|
|
1187
|
+
stdio: "inherit"
|
|
1188
|
+
});
|
|
1189
|
+
process.exitCode = result.exitCode;
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
case "diff":
|
|
1193
|
+
process.stdout.write(`${await runDiff(args[0] ?? "local", args[1] ?? "stage", runtimeOptions)}
|
|
1194
|
+
`);
|
|
1195
|
+
return;
|
|
1196
|
+
case "doctor":
|
|
1197
|
+
process.stdout.write(`${await runDoctor(runtimeOptions)}
|
|
1198
|
+
`);
|
|
1199
|
+
return;
|
|
1200
|
+
default:
|
|
1201
|
+
process.stderr.write(`Unknown command: ${command}
|
|
1202
|
+
`);
|
|
1203
|
+
process.exitCode = 1;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
void main(process.argv.slice(2));
|
|
1207
|
+
export {
|
|
1208
|
+
main
|
|
1209
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitsy/cnos-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI entry point and developer tooling for CNOS.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"cnos": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/kitsyai/cnos.git",
|
|
24
|
+
"directory": "packages/cli"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/kitsyai/cnos/tree/main/packages/cli",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/kitsyai/cnos/issues"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"cnos",
|
|
32
|
+
"cli",
|
|
33
|
+
"config"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@kitsy/cnos": "0.0.1",
|
|
40
|
+
"@kitsy/cnos-core": "0.0.1"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
44
|
+
"clean": "rimraf dist",
|
|
45
|
+
"dev": "tsup src/index.ts --watch --format esm --dts",
|
|
46
|
+
"lint": "eslint src test",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
49
|
+
}
|
|
50
|
+
}
|