@graphorin/cli 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/bin/graphorin.d.ts +1 -0
- package/dist/bin/graphorin.js +606 -0
- package/dist/bin/graphorin.js.map +1 -0
- package/dist/commands/audit.d.ts +59 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +156 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/auth.d.ts +48 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +148 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/consolidator.d.ts +49 -0
- package/dist/commands/consolidator.d.ts.map +1 -0
- package/dist/commands/consolidator.js +97 -0
- package/dist/commands/consolidator.js.map +1 -0
- package/dist/commands/doctor.d.ts +64 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +150 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/guard.d.ts +35 -0
- package/dist/commands/guard.d.ts.map +1 -0
- package/dist/commands/guard.js +86 -0
- package/dist/commands/guard.js.map +1 -0
- package/dist/commands/index.d.ts +21 -0
- package/dist/commands/index.js +22 -0
- package/dist/commands/init.d.ts +46 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +131 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/memory.d.ts +243 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +471 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/migrate-config.d.ts +20 -0
- package/dist/commands/migrate-config.d.ts.map +1 -0
- package/dist/commands/migrate-config.js +48 -0
- package/dist/commands/migrate-config.js.map +1 -0
- package/dist/commands/migrate-export.d.ts +26 -0
- package/dist/commands/migrate-export.d.ts.map +1 -0
- package/dist/commands/migrate-export.js +67 -0
- package/dist/commands/migrate-export.js.map +1 -0
- package/dist/commands/migrate.d.ts +31 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +59 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/pricing.d.ts +66 -0
- package/dist/commands/pricing.d.ts.map +1 -0
- package/dist/commands/pricing.js +128 -0
- package/dist/commands/pricing.js.map +1 -0
- package/dist/commands/secrets.d.ts +96 -0
- package/dist/commands/secrets.d.ts.map +1 -0
- package/dist/commands/secrets.js +182 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/skills.d.ts +58 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +181 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/start.d.ts +51 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +122 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/storage.d.ts +110 -0
- package/dist/commands/storage.d.ts.map +1 -0
- package/dist/commands/storage.js +274 -0
- package/dist/commands/storage.js.map +1 -0
- package/dist/commands/telemetry.d.ts +21 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +75 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/token.d.ts +109 -0
- package/dist/commands/token.d.ts.map +1 -0
- package/dist/commands/token.js +246 -0
- package/dist/commands/token.js.map +1 -0
- package/dist/commands/tools-lint.d.ts +103 -0
- package/dist/commands/tools-lint.d.ts.map +1 -0
- package/dist/commands/tools-lint.js +240 -0
- package/dist/commands/tools-lint.js.map +1 -0
- package/dist/commands/traces.d.ts +33 -0
- package/dist/commands/traces.d.ts.map +1 -0
- package/dist/commands/traces.js +103 -0
- package/dist/commands/traces.js.map +1 -0
- package/dist/commands/triggers.d.ts +49 -0
- package/dist/commands/triggers.d.ts.map +1 -0
- package/dist/commands/triggers.js +129 -0
- package/dist/commands/triggers.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/exit.js +17 -0
- package/dist/internal/exit.js.map +1 -0
- package/dist/internal/load-config.js +84 -0
- package/dist/internal/load-config.js.map +1 -0
- package/dist/internal/offline.d.ts +58 -0
- package/dist/internal/offline.d.ts.map +1 -0
- package/dist/internal/offline.js +72 -0
- package/dist/internal/offline.js.map +1 -0
- package/dist/internal/output.d.ts +56 -0
- package/dist/internal/output.d.ts.map +1 -0
- package/dist/internal/output.js +86 -0
- package/dist/internal/output.js.map +1 -0
- package/dist/internal/prompts.js +49 -0
- package/dist/internal/prompts.js.map +1 -0
- package/dist/internal/store-context.js +69 -0
- package/dist/internal/store-context.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { brand, defaultPrintSink, emitReport, statusMarker } from "../internal/output.js";
|
|
2
|
+
import { EXIT_CODES } from "../internal/exit.js";
|
|
3
|
+
import { stdin } from "node:process";
|
|
4
|
+
import { SecretValue, createSecretsStore, getActiveSecretsStore, parseSecretRef, resolveSecret } from "@graphorin/security";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/secrets.ts
|
|
7
|
+
/**
|
|
8
|
+
* `graphorin secrets` — manage the operator's secrets store.
|
|
9
|
+
*
|
|
10
|
+
* Surface (per Phase 15 § Secrets):
|
|
11
|
+
*
|
|
12
|
+
* - `graphorin secrets list`
|
|
13
|
+
* - `graphorin secrets get <key>`
|
|
14
|
+
* - `graphorin secrets set <key> [--value <v>] [--from-stdin] [--scope <ref>]`
|
|
15
|
+
* - `graphorin secrets delete <key>`
|
|
16
|
+
* - `graphorin secrets ref <uri>` — test resolution of a `SecretRef` URI.
|
|
17
|
+
* - `graphorin secrets rotate <key> --new-value <v>`
|
|
18
|
+
*
|
|
19
|
+
* Honours `--secrets-source <kind>` and `--strict-secrets` per
|
|
20
|
+
* DEC-136 — both flags are forwarded to `createSecretsStore(...)` so
|
|
21
|
+
* the CLI activates the same store the running server would.
|
|
22
|
+
*
|
|
23
|
+
* The CLI never logs raw secret bytes. `get` prints the value through
|
|
24
|
+
* `value.use((s) => print(s))` so the bytes are released as soon as
|
|
25
|
+
* the print callback returns; `--json` mode emits only the metadata
|
|
26
|
+
* fields, never the value, unless `--reveal` is explicitly supplied.
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
/** @stable */
|
|
31
|
+
async function runSecretsList(options = {}) {
|
|
32
|
+
const list = await (await openStore(options)).list();
|
|
33
|
+
emitReport(options, list, () => {
|
|
34
|
+
const print = options.print ?? defaultPrintSink;
|
|
35
|
+
if (list.length === 0) {
|
|
36
|
+
print(brand("no secrets recorded."));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
print(brand(`${list.length} secret(s):`));
|
|
40
|
+
for (const m of list) print(` ${statusMarker("ok")} ${m.key} (updated=${m.updatedAt ?? "-"})`);
|
|
41
|
+
});
|
|
42
|
+
return list;
|
|
43
|
+
}
|
|
44
|
+
/** @stable */
|
|
45
|
+
async function runSecretsGet(options) {
|
|
46
|
+
const value = await (await openStore(options)).get(options.key);
|
|
47
|
+
if (value === null) {
|
|
48
|
+
const out$1 = Object.freeze({
|
|
49
|
+
key: options.key,
|
|
50
|
+
found: false
|
|
51
|
+
});
|
|
52
|
+
emitReport(options, out$1, () => {
|
|
53
|
+
(options.print ?? defaultPrintSink)(brand(`secret '${options.key}' not found.`));
|
|
54
|
+
process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
55
|
+
});
|
|
56
|
+
return out$1;
|
|
57
|
+
}
|
|
58
|
+
if (options.reveal === true) {
|
|
59
|
+
const raw = await value.use((s) => String(s));
|
|
60
|
+
const out$1 = Object.freeze({
|
|
61
|
+
key: options.key,
|
|
62
|
+
found: true,
|
|
63
|
+
value: raw
|
|
64
|
+
});
|
|
65
|
+
emitReport(options, out$1, () => {
|
|
66
|
+
const print = options.print ?? defaultPrintSink;
|
|
67
|
+
print(brand(`secret '${options.key}' resolved (length=${raw.length})`));
|
|
68
|
+
print(` ${raw}`);
|
|
69
|
+
});
|
|
70
|
+
return out$1;
|
|
71
|
+
}
|
|
72
|
+
const out = Object.freeze({
|
|
73
|
+
key: options.key,
|
|
74
|
+
found: true
|
|
75
|
+
});
|
|
76
|
+
emitReport(options, out, () => {
|
|
77
|
+
(options.print ?? defaultPrintSink)(brand(`secret '${options.key}' found (use --reveal to print the bytes)`));
|
|
78
|
+
});
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/** @stable */
|
|
82
|
+
async function runSecretsSet(options) {
|
|
83
|
+
const store = await openStore(options);
|
|
84
|
+
let raw = options.value;
|
|
85
|
+
if (raw === void 0 && options.fromStdin === true) raw = await readStdin();
|
|
86
|
+
if (raw === void 0) throw new Error("[graphorin/cli] secrets set requires either --value <v> or --from-stdin (no plaintext on the command line).");
|
|
87
|
+
await store.set(options.key, SecretValue.fromString(raw, { source: { resolver: "graphorin secrets set" } }));
|
|
88
|
+
emitReport(options, { ok: true }, () => {
|
|
89
|
+
(options.print ?? defaultPrintSink)(brand(`secret '${options.key}' written.`));
|
|
90
|
+
});
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
/** @stable */
|
|
94
|
+
async function runSecretsDelete(options) {
|
|
95
|
+
await (await openStore(options)).delete(options.key);
|
|
96
|
+
emitReport(options, { ok: true }, () => {
|
|
97
|
+
(options.print ?? defaultPrintSink)(brand(`secret '${options.key}' deleted.`));
|
|
98
|
+
});
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Test resolution of a `SecretRef` URI. The CLI parses the URI first
|
|
103
|
+
* (sanity check + scheme echo), then resolves through the registered
|
|
104
|
+
* resolver chain.
|
|
105
|
+
*
|
|
106
|
+
* @stable
|
|
107
|
+
*/
|
|
108
|
+
async function runSecretsRef(options) {
|
|
109
|
+
const parsed = parseSecretRef(options.uri);
|
|
110
|
+
try {
|
|
111
|
+
const value = await resolveSecret(options.uri);
|
|
112
|
+
const length = await value.use((s) => s.length);
|
|
113
|
+
const raw = options.reveal === true ? await value.use((s) => String(s)) : void 0;
|
|
114
|
+
const out = Object.freeze({
|
|
115
|
+
uri: options.uri,
|
|
116
|
+
scheme: parsed.scheme,
|
|
117
|
+
resolved: true,
|
|
118
|
+
length,
|
|
119
|
+
...raw !== void 0 ? { value: raw } : {}
|
|
120
|
+
});
|
|
121
|
+
emitReport(options, out, () => {
|
|
122
|
+
(options.print ?? defaultPrintSink)(brand(`${statusMarker("ok")} resolved '${parsed.scheme}' ref (length=${length}${raw !== void 0 ? `, value='${raw}'` : ""})`));
|
|
123
|
+
});
|
|
124
|
+
return out;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const out = Object.freeze({
|
|
127
|
+
uri: options.uri,
|
|
128
|
+
scheme: parsed.scheme,
|
|
129
|
+
resolved: false
|
|
130
|
+
});
|
|
131
|
+
emitReport(options, out, () => {
|
|
132
|
+
(options.print ?? defaultPrintSink)(brand(`${statusMarker("fail")} ${parsed.scheme} ref failed: ${err.message}`));
|
|
133
|
+
process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
134
|
+
});
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* `graphorin secrets rotate <key>` — overwrite the existing value
|
|
140
|
+
* with a fresh one. Functionally identical to `set` but the CLI
|
|
141
|
+
* surfaces the operation explicitly so audit logs can distinguish
|
|
142
|
+
* a rotation from an initial write.
|
|
143
|
+
*
|
|
144
|
+
* @stable
|
|
145
|
+
*/
|
|
146
|
+
async function runSecretsRotate(options) {
|
|
147
|
+
const store = await openStore(options);
|
|
148
|
+
if (await store.get(options.key) === null) throw new Error(`[graphorin/cli] cannot rotate '${options.key}' — secret does not exist (use 'graphorin secrets set' to create it).`);
|
|
149
|
+
let raw = options.newValue;
|
|
150
|
+
if (raw === void 0 && options.fromStdin === true) raw = await readStdin();
|
|
151
|
+
if (raw === void 0) throw new Error("[graphorin/cli] secrets rotate requires either --new-value <v> or --from-stdin.");
|
|
152
|
+
await store.set(options.key, SecretValue.fromString(raw, { source: { resolver: "graphorin secrets rotate" } }));
|
|
153
|
+
emitReport(options, { ok: true }, () => {
|
|
154
|
+
(options.print ?? defaultPrintSink)(brand(`secret '${options.key}' rotated.`));
|
|
155
|
+
});
|
|
156
|
+
return { ok: true };
|
|
157
|
+
}
|
|
158
|
+
async function openStore(options) {
|
|
159
|
+
if (options.secretsSource === void 0) {
|
|
160
|
+
const active = getActiveSecretsStore();
|
|
161
|
+
if (active !== void 0) return active;
|
|
162
|
+
}
|
|
163
|
+
return await createSecretsStore({
|
|
164
|
+
...options.secretsSource !== void 0 ? { kind: options.secretsSource } : {},
|
|
165
|
+
...options.strictSecrets !== void 0 ? { strict: options.strictSecrets } : {}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async function readStdin() {
|
|
169
|
+
return await new Promise((resolve, reject) => {
|
|
170
|
+
let buf = "";
|
|
171
|
+
stdin.setEncoding("utf8");
|
|
172
|
+
stdin.on("data", (chunk) => {
|
|
173
|
+
buf += chunk;
|
|
174
|
+
});
|
|
175
|
+
stdin.on("end", () => resolve(buf.replace(/\r?\n$/, "")));
|
|
176
|
+
stdin.on("error", reject);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
export { runSecretsDelete, runSecretsGet, runSecretsList, runSecretsRef, runSecretsRotate, runSecretsSet };
|
|
182
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.js","names":["out: SecretsGetResult","out","out: SecretsRefResult"],"sources":["../../src/commands/secrets.ts"],"sourcesContent":["/**\n * `graphorin secrets` — manage the operator's secrets store.\n *\n * Surface (per Phase 15 § Secrets):\n *\n * - `graphorin secrets list`\n * - `graphorin secrets get <key>`\n * - `graphorin secrets set <key> [--value <v>] [--from-stdin] [--scope <ref>]`\n * - `graphorin secrets delete <key>`\n * - `graphorin secrets ref <uri>` — test resolution of a `SecretRef` URI.\n * - `graphorin secrets rotate <key> --new-value <v>`\n *\n * Honours `--secrets-source <kind>` and `--strict-secrets` per\n * DEC-136 — both flags are forwarded to `createSecretsStore(...)` so\n * the CLI activates the same store the running server would.\n *\n * The CLI never logs raw secret bytes. `get` prints the value through\n * `value.use((s) => print(s))` so the bytes are released as soon as\n * the print callback returns; `--json` mode emits only the metadata\n * fields, never the value, unless `--reveal` is explicitly supplied.\n *\n * @packageDocumentation\n */\n\nimport { stdin } from 'node:process';\n\nimport type { SecretMetadata } from '@graphorin/core/contracts';\nimport {\n createSecretsStore,\n getActiveSecretsStore,\n parseSecretRef,\n resolveSecret,\n type SecretsStoreKind,\n SecretValue,\n} from '@graphorin/security';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\n\n/** @stable */\nexport interface SecretsCommonOptions extends CommonOutputOptions {\n /** Mirrors `--secrets-source` per DEC-136. */\n readonly secretsSource?: SecretsStoreKind;\n /** Mirrors `--strict-secrets` per DEC-136. */\n readonly strictSecrets?: boolean;\n}\n\n/** @stable */\nexport interface SecretsListOptions extends SecretsCommonOptions {}\n\n/** @stable */\nexport async function runSecretsList(\n options: SecretsListOptions = {},\n): Promise<ReadonlyArray<SecretMetadata>> {\n const store = await openStore(options);\n const list = await store.list();\n emitReport(options, list, () => {\n const print = options.print ?? defaultPrintSink;\n if (list.length === 0) {\n print(brand('no secrets recorded.'));\n return;\n }\n print(brand(`${list.length} secret(s):`));\n for (const m of list) {\n print(` ${statusMarker('ok')} ${m.key} (updated=${m.updatedAt ?? '-'})`);\n }\n });\n return list;\n}\n\n/** @stable */\nexport interface SecretsGetOptions extends SecretsCommonOptions {\n readonly key: string;\n /**\n * When `true`, print the raw value through the human report. The\n * default is to print only structured metadata so the bytes do not\n * surface in shell history.\n */\n readonly reveal?: boolean;\n}\n\n/** @stable */\nexport interface SecretsGetResult {\n readonly key: string;\n readonly found: boolean;\n /** Only present when `reveal: true` AND the secret exists. */\n readonly value?: string;\n}\n\n/** @stable */\nexport async function runSecretsGet(options: SecretsGetOptions): Promise<SecretsGetResult> {\n const store = await openStore(options);\n const value = await store.get(options.key);\n if (value === null) {\n const out: SecretsGetResult = Object.freeze({ key: options.key, found: false });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`secret '${options.key}' not found.`));\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n });\n return out;\n }\n if (options.reveal === true) {\n const raw = await value.use((s) => String(s));\n const out: SecretsGetResult = Object.freeze({ key: options.key, found: true, value: raw });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`secret '${options.key}' resolved (length=${raw.length})`));\n print(` ${raw}`);\n });\n return out;\n }\n const out: SecretsGetResult = Object.freeze({ key: options.key, found: true });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`secret '${options.key}' found (use --reveal to print the bytes)`));\n });\n return out;\n}\n\n/** @stable */\nexport interface SecretsSetOptions extends SecretsCommonOptions {\n readonly key: string;\n readonly value?: string;\n /** When `true`, read the value from stdin. */\n readonly fromStdin?: boolean;\n}\n\n/** @stable */\nexport async function runSecretsSet(options: SecretsSetOptions): Promise<{ readonly ok: true }> {\n const store = await openStore(options);\n let raw = options.value;\n if (raw === undefined && options.fromStdin === true) {\n raw = await readStdin();\n }\n if (raw === undefined) {\n throw new Error(\n '[graphorin/cli] secrets set requires either --value <v> or --from-stdin (no plaintext on the command line).',\n );\n }\n await store.set(\n options.key,\n SecretValue.fromString(raw, { source: { resolver: 'graphorin secrets set' } }),\n );\n emitReport(options, { ok: true } as const, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`secret '${options.key}' written.`));\n });\n return { ok: true };\n}\n\n/** @stable */\nexport interface SecretsDeleteOptions extends SecretsCommonOptions {\n readonly key: string;\n}\n\n/** @stable */\nexport async function runSecretsDelete(\n options: SecretsDeleteOptions,\n): Promise<{ readonly ok: true }> {\n const store = await openStore(options);\n await store.delete(options.key);\n emitReport(options, { ok: true } as const, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`secret '${options.key}' deleted.`));\n });\n return { ok: true };\n}\n\n/** @stable */\nexport interface SecretsRefOptions extends CommonOutputOptions {\n readonly uri: string;\n readonly reveal?: boolean;\n}\n\n/** @stable */\nexport interface SecretsRefResult {\n readonly uri: string;\n readonly scheme: string;\n readonly resolved: boolean;\n readonly length?: number;\n readonly value?: string;\n}\n\n/**\n * Test resolution of a `SecretRef` URI. The CLI parses the URI first\n * (sanity check + scheme echo), then resolves through the registered\n * resolver chain.\n *\n * @stable\n */\nexport async function runSecretsRef(options: SecretsRefOptions): Promise<SecretsRefResult> {\n const parsed = parseSecretRef(options.uri);\n try {\n const value = await resolveSecret(options.uri);\n const length = await value.use((s) => s.length);\n const raw = options.reveal === true ? await value.use((s) => String(s)) : undefined;\n const out: SecretsRefResult = Object.freeze({\n uri: options.uri,\n scheme: parsed.scheme,\n resolved: true,\n length,\n ...(raw !== undefined ? { value: raw } : {}),\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(\n brand(\n `${statusMarker('ok')} resolved '${parsed.scheme}' ref (length=${length}${raw !== undefined ? `, value='${raw}'` : ''})`,\n ),\n );\n });\n return out;\n } catch (err) {\n const out: SecretsRefResult = Object.freeze({\n uri: options.uri,\n scheme: parsed.scheme,\n resolved: false,\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(\n brand(`${statusMarker('fail')} ${parsed.scheme} ref failed: ${(err as Error).message}`),\n );\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n });\n return out;\n }\n}\n\n/** @stable */\nexport interface SecretsRotateOptions extends SecretsCommonOptions {\n readonly key: string;\n readonly newValue?: string;\n readonly fromStdin?: boolean;\n}\n\n/**\n * `graphorin secrets rotate <key>` — overwrite the existing value\n * with a fresh one. Functionally identical to `set` but the CLI\n * surfaces the operation explicitly so audit logs can distinguish\n * a rotation from an initial write.\n *\n * @stable\n */\nexport async function runSecretsRotate(\n options: SecretsRotateOptions,\n): Promise<{ readonly ok: true }> {\n const store = await openStore(options);\n const existing = await store.get(options.key);\n if (existing === null) {\n throw new Error(\n `[graphorin/cli] cannot rotate '${options.key}' — secret does not exist (use 'graphorin secrets set' to create it).`,\n );\n }\n let raw = options.newValue;\n if (raw === undefined && options.fromStdin === true) raw = await readStdin();\n if (raw === undefined) {\n throw new Error(\n '[graphorin/cli] secrets rotate requires either --new-value <v> or --from-stdin.',\n );\n }\n await store.set(\n options.key,\n SecretValue.fromString(raw, { source: { resolver: 'graphorin secrets rotate' } }),\n );\n emitReport(options, { ok: true } as const, () => {\n const print = options.print ?? defaultPrintSink;\n print(brand(`secret '${options.key}' rotated.`));\n });\n return { ok: true };\n}\n\nasync function openStore(options: SecretsCommonOptions) {\n // Reuse the per-process active store when the caller did not request\n // a different kind. The singleton matches the running server / lib\n // process so consecutive CLI invocations within the same process\n // address the same backing store (this matters for the in-memory\n // store + for subprocess CLI flows that share a parent's resolver\n // chain via env vars).\n if (options.secretsSource === undefined) {\n const active = getActiveSecretsStore();\n if (active !== undefined) return active;\n }\n return await createSecretsStore({\n ...(options.secretsSource !== undefined ? { kind: options.secretsSource } : {}),\n ...(options.strictSecrets !== undefined ? { strict: options.strictSecrets } : {}),\n });\n}\n\nasync function readStdin(): Promise<string> {\n return await new Promise((resolve, reject) => {\n let buf = '';\n stdin.setEncoding('utf8');\n stdin.on('data', (chunk) => {\n buf += chunk;\n });\n stdin.on('end', () => resolve(buf.replace(/\\r?\\n$/, '')));\n stdin.on('error', reject);\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CommonOutputOptions } from "../internal/output.js";
|
|
2
|
+
import { SkillInstallationStatus } from "@graphorin/security";
|
|
3
|
+
import { MigrationResult } from "@graphorin/skills";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/skills.d.ts
|
|
6
|
+
|
|
7
|
+
/** @stable */
|
|
8
|
+
interface SkillsCommonOptions extends CommonOutputOptions {}
|
|
9
|
+
/** @stable */
|
|
10
|
+
type SkillTrustLevelInput = 'trusted' | 'trusted-with-scripts' | 'untrusted';
|
|
11
|
+
/** @stable */
|
|
12
|
+
interface SkillsInstallOptions extends SkillsCommonOptions {
|
|
13
|
+
/** `npm:<name>[@version]` or `git:<url>` source. */
|
|
14
|
+
readonly source: string;
|
|
15
|
+
/** Optional explicit version pin (npm sources only). */
|
|
16
|
+
readonly version?: string;
|
|
17
|
+
/** Optional git ref (git sources only). */
|
|
18
|
+
readonly ref?: string;
|
|
19
|
+
/** Trust level for the operator's project. Defaults to the helper's own default. */
|
|
20
|
+
readonly trustLevel?: SkillTrustLevelInput;
|
|
21
|
+
/** Working directory for npm installs. */
|
|
22
|
+
readonly cwd?: string;
|
|
23
|
+
readonly dryRun?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** @stable */
|
|
26
|
+
declare function runSkillsInstall(options: SkillsInstallOptions): Promise<SkillInstallationStatus>;
|
|
27
|
+
/** @stable */
|
|
28
|
+
interface SkillsInspectOptions extends SkillsCommonOptions {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
}
|
|
31
|
+
/** @stable */
|
|
32
|
+
declare function runSkillsInspect(options: SkillsInspectOptions): Promise<SkillInstallationStatus | null>;
|
|
33
|
+
/** @stable */
|
|
34
|
+
interface SkillsAuditOptions extends SkillsCommonOptions {}
|
|
35
|
+
/** @stable */
|
|
36
|
+
declare function runSkillsAudit(options?: SkillsAuditOptions): readonly SkillInstallationStatus[];
|
|
37
|
+
/** @stable */
|
|
38
|
+
interface SkillsMigrateFrontmatterOptions extends SkillsCommonOptions {
|
|
39
|
+
/** Directory to walk. Defaults to `process.cwd()`. */
|
|
40
|
+
readonly path?: string;
|
|
41
|
+
readonly recursive?: boolean;
|
|
42
|
+
readonly apply?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** @stable */
|
|
45
|
+
interface SkillsMigrateFrontmatterResult {
|
|
46
|
+
readonly directory: string;
|
|
47
|
+
readonly visited: number;
|
|
48
|
+
readonly migrated: ReadonlyArray<{
|
|
49
|
+
readonly file: string;
|
|
50
|
+
readonly result: MigrationResult;
|
|
51
|
+
}>;
|
|
52
|
+
readonly applied: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** @stable */
|
|
55
|
+
declare function runSkillsMigrateFrontmatter(options?: SkillsMigrateFrontmatterOptions): Promise<SkillsMigrateFrontmatterResult>;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { SkillTrustLevelInput, SkillsAuditOptions, SkillsCommonOptions, SkillsInspectOptions, SkillsInstallOptions, SkillsMigrateFrontmatterOptions, SkillsMigrateFrontmatterResult, runSkillsAudit, runSkillsInspect, runSkillsInstall, runSkillsMigrateFrontmatter };
|
|
58
|
+
//# sourceMappingURL=skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","names":[],"sources":["../../src/commands/skills.ts"],"sourcesContent":[],"mappings":";;;;;;AA4KA;AAWsB,UA1IL,mBAAA,SAA4B,mBA0II,CAAA;;AAE9C,KAzIS,oBAAA,GAyIT,SAAA,GAAA,sBAAA,GAAA,WAAA;;UAtIc,oBAAA,SAA6B;;;;;;;;wBAQtB;;;;;;iBAOF,gBAAA,UACX,uBACR,QAAQ;;UA2CM,oBAAA,SAA6B;;;;iBAKxB,gBAAA,UACX,uBACR,QAAQ;;UAuBM,kBAAA,SAA2B;;iBAG5B,cAAA,WAAwB,8BAAuB;;UAoB9C,+BAAA,SAAwC;;;;;;;UAQxC,8BAAA;;;qBAGI;;qBAEA;;;;;iBAMC,2BAAA,WACX,kCACR,QAAQ"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { brand, defaultPrintSink, emitReport, statusMarker } from "../internal/output.js";
|
|
2
|
+
import { EXIT_CODES } from "../internal/exit.js";
|
|
3
|
+
import { checkOfflineModeBlocked } from "../internal/offline.js";
|
|
4
|
+
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { auditInstalledSkills, installSkillFromGit, installSkillFromNpm } from "@graphorin/security";
|
|
8
|
+
import { migrateFrontmatter } from "@graphorin/skills";
|
|
9
|
+
|
|
10
|
+
//#region src/commands/skills.ts
|
|
11
|
+
/**
|
|
12
|
+
* `graphorin skills` — install + audit + migrate operator-managed
|
|
13
|
+
* skill packages.
|
|
14
|
+
*
|
|
15
|
+
* Surface (per Phase 15 § Skills):
|
|
16
|
+
*
|
|
17
|
+
* - `graphorin skills install <source>` — `npm:` or `git:` source.
|
|
18
|
+
* Honours allowlist / denylist + signature verification (DEC-140 /
|
|
19
|
+
* ADR-034). Refuses when `GRAPHORIN_OFFLINE=1` is set.
|
|
20
|
+
* - `graphorin skills inspect <name>` — frontmatter + signature +
|
|
21
|
+
* supply-chain status of an installed skill.
|
|
22
|
+
* - `graphorin skills audit` — full audit of every recorded
|
|
23
|
+
* installation.
|
|
24
|
+
* - `graphorin skills migrate-frontmatter [--apply] [--recursive]`
|
|
25
|
+
* — DEC-156: rewrites legacy `graphorin-*` frontmatter fields onto
|
|
26
|
+
* their upstream equivalents.
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
/** @stable */
|
|
31
|
+
async function runSkillsInstall(options) {
|
|
32
|
+
if (!checkOfflineModeBlocked("skills install", { ...options.print !== void 0 ? { print: options.print } : {} })) process.exit(EXIT_CODES.RECOVERABLE_FAILURE);
|
|
33
|
+
const print = options.print ?? defaultPrintSink;
|
|
34
|
+
const parsed = parseSource(options.source);
|
|
35
|
+
let result;
|
|
36
|
+
if (parsed.kind === "npm") result = await installSkillFromNpm({
|
|
37
|
+
packageName: parsed.value,
|
|
38
|
+
...options.version !== void 0 ? { version: options.version } : {},
|
|
39
|
+
...options.trustLevel !== void 0 ? { trustLevel: options.trustLevel } : {},
|
|
40
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
41
|
+
...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {}
|
|
42
|
+
});
|
|
43
|
+
else result = await installSkillFromGit({
|
|
44
|
+
repoUrl: parsed.value,
|
|
45
|
+
...options.ref !== void 0 ? { ref: options.ref } : {},
|
|
46
|
+
...options.trustLevel !== void 0 ? { trustLevel: options.trustLevel } : {},
|
|
47
|
+
...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {}
|
|
48
|
+
});
|
|
49
|
+
emitReport(options, result, () => {
|
|
50
|
+
print(brand(`installed skill '${result.id}' (${result.source.kind}, signatureVerified=${result.signatureVerified})`));
|
|
51
|
+
if (result.signature !== void 0) print(` signature valid: ${result.signature.valid}`);
|
|
52
|
+
});
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/** @stable */
|
|
56
|
+
async function runSkillsInspect(options) {
|
|
57
|
+
const match = auditInstalledSkills().find((s) => s.id === options.name) ?? null;
|
|
58
|
+
emitReport(options, match, () => {
|
|
59
|
+
const print = options.print ?? defaultPrintSink;
|
|
60
|
+
if (match === null) {
|
|
61
|
+
print(brand(`skill '${options.name}' not found in this process registry.`));
|
|
62
|
+
process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
print(brand(`skill ${match.id}`));
|
|
66
|
+
print(` source: ${match.source.kind}`);
|
|
67
|
+
print(` trust level: ${match.trustLevel}`);
|
|
68
|
+
print(` ignoreScripts: ${match.ignoreScripts}`);
|
|
69
|
+
print(` signatureVerified: ${match.signatureVerified}`);
|
|
70
|
+
print(` installedAt: ${new Date(match.installedAt).toISOString()}`);
|
|
71
|
+
if (match.installPath !== void 0) print(` installPath: ${match.installPath}`);
|
|
72
|
+
if (match.publisher !== void 0) print(` publisher: ${match.publisher}`);
|
|
73
|
+
});
|
|
74
|
+
return match;
|
|
75
|
+
}
|
|
76
|
+
/** @stable */
|
|
77
|
+
function runSkillsAudit(options = {}) {
|
|
78
|
+
const all = auditInstalledSkills();
|
|
79
|
+
emitReport(options, all, () => {
|
|
80
|
+
const print = options.print ?? defaultPrintSink;
|
|
81
|
+
if (all.length === 0) {
|
|
82
|
+
print(brand("no skills installed in this process registry."));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
print(brand(`${all.length} installed skill(s):`));
|
|
86
|
+
for (const s of all) print(` ${s.signatureVerified ? statusMarker("ok") : statusMarker("warn")} ${s.id} (${s.source.kind}, trust=${s.trustLevel}, signatureVerified=${s.signatureVerified})`);
|
|
87
|
+
});
|
|
88
|
+
return all;
|
|
89
|
+
}
|
|
90
|
+
/** @stable */
|
|
91
|
+
async function runSkillsMigrateFrontmatter(options = {}) {
|
|
92
|
+
const root = resolve(options.path ?? process.cwd());
|
|
93
|
+
const recursive = options.recursive === true;
|
|
94
|
+
const apply = options.apply === true;
|
|
95
|
+
const files = await collectSkillMd(root, recursive);
|
|
96
|
+
const migrated = [];
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
const raw = await readFile(file, "utf8");
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = migrateFrontmatter(raw, {
|
|
102
|
+
apply,
|
|
103
|
+
skillId: file
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (result.changed) {
|
|
109
|
+
if (apply) await writeFile(file, result.migratedSkillMd, { mode: 384 });
|
|
110
|
+
migrated.push({
|
|
111
|
+
file,
|
|
112
|
+
result
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const out = Object.freeze({
|
|
117
|
+
directory: root,
|
|
118
|
+
visited: files.length,
|
|
119
|
+
migrated: Object.freeze(migrated),
|
|
120
|
+
applied: apply
|
|
121
|
+
});
|
|
122
|
+
emitReport(options, out, () => {
|
|
123
|
+
const print = options.print ?? defaultPrintSink;
|
|
124
|
+
print(brand(`migrate-frontmatter visited ${out.visited} SKILL.md file(s) under ${out.directory}.`));
|
|
125
|
+
if (out.migrated.length === 0) {
|
|
126
|
+
print(brand("no rewrites required."));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
print(brand(`${out.migrated.length} file(s) ${apply ? "rewritten" : "would be rewritten (dry-run)"}:`));
|
|
130
|
+
for (const m of out.migrated) print(` - ${m.file}`);
|
|
131
|
+
});
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function parseSource(input) {
|
|
135
|
+
if (input.startsWith("npm:")) return {
|
|
136
|
+
kind: "npm",
|
|
137
|
+
value: input.slice(4)
|
|
138
|
+
};
|
|
139
|
+
if (input.startsWith("git:")) return {
|
|
140
|
+
kind: "git",
|
|
141
|
+
value: input.slice(4)
|
|
142
|
+
};
|
|
143
|
+
if (input.includes("://")) return {
|
|
144
|
+
kind: "git",
|
|
145
|
+
value: input
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
kind: "npm",
|
|
149
|
+
value: input
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function collectSkillMd(root, recursive) {
|
|
153
|
+
const out = [];
|
|
154
|
+
await walk(root, recursive, out);
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
async function walk(dir, recursive, acc) {
|
|
158
|
+
let entries;
|
|
159
|
+
try {
|
|
160
|
+
entries = await readdir(dir);
|
|
161
|
+
} catch {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
for (const name of entries) {
|
|
165
|
+
if (name === "node_modules" || name === ".git" || name === "dist") continue;
|
|
166
|
+
const full = join(dir, name);
|
|
167
|
+
let s;
|
|
168
|
+
try {
|
|
169
|
+
s = await stat(full);
|
|
170
|
+
} catch {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (s.isDirectory()) {
|
|
174
|
+
if (recursive) await walk(full, recursive, acc);
|
|
175
|
+
} else if (name === "SKILL.md") acc.push(full);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
export { runSkillsAudit, runSkillsInspect, runSkillsInstall, runSkillsMigrateFrontmatter };
|
|
181
|
+
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.js","names":["result: SkillInstallationStatus","migrated: Array<{ file: string; result: MigrationResult }>","result: MigrationResult","out: SkillsMigrateFrontmatterResult","out: string[]","entries: string[]","s: import('node:fs').Stats"],"sources":["../../src/commands/skills.ts"],"sourcesContent":["/**\n * `graphorin skills` — install + audit + migrate operator-managed\n * skill packages.\n *\n * Surface (per Phase 15 § Skills):\n *\n * - `graphorin skills install <source>` — `npm:` or `git:` source.\n * Honours allowlist / denylist + signature verification (DEC-140 /\n * ADR-034). Refuses when `GRAPHORIN_OFFLINE=1` is set.\n * - `graphorin skills inspect <name>` — frontmatter + signature +\n * supply-chain status of an installed skill.\n * - `graphorin skills audit` — full audit of every recorded\n * installation.\n * - `graphorin skills migrate-frontmatter [--apply] [--recursive]`\n * — DEC-156: rewrites legacy `graphorin-*` frontmatter fields onto\n * their upstream equivalents.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport process from 'node:process';\n\nimport {\n auditInstalledSkills,\n type InstallSkillFromGitOptions,\n type InstallSkillFromNpmOptions,\n installSkillFromGit,\n installSkillFromNpm,\n type SkillInstallationStatus,\n} from '@graphorin/security';\nimport { loadSkills, type MigrationResult, migrateFrontmatter } from '@graphorin/skills';\n\nimport { EXIT_CODES } from '../internal/exit.js';\nimport { checkOfflineModeBlocked } from '../internal/offline.js';\nimport {\n brand,\n type CommonOutputOptions,\n defaultPrintSink,\n emitReport,\n statusMarker,\n} from '../internal/output.js';\n\n/** @stable */\nexport interface SkillsCommonOptions extends CommonOutputOptions {}\n\n/** @stable */\nexport type SkillTrustLevelInput = 'trusted' | 'trusted-with-scripts' | 'untrusted';\n\n/** @stable */\nexport interface SkillsInstallOptions extends SkillsCommonOptions {\n /** `npm:<name>[@version]` or `git:<url>` source. */\n readonly source: string;\n /** Optional explicit version pin (npm sources only). */\n readonly version?: string;\n /** Optional git ref (git sources only). */\n readonly ref?: string;\n /** Trust level for the operator's project. Defaults to the helper's own default. */\n readonly trustLevel?: SkillTrustLevelInput;\n /** Working directory for npm installs. */\n readonly cwd?: string;\n readonly dryRun?: boolean;\n}\n\n/** @stable */\nexport async function runSkillsInstall(\n options: SkillsInstallOptions,\n): Promise<SkillInstallationStatus> {\n if (\n !checkOfflineModeBlocked('skills install', {\n ...(options.print !== undefined ? { print: options.print } : {}),\n })\n ) {\n process.exit(EXIT_CODES.RECOVERABLE_FAILURE);\n }\n const print = options.print ?? defaultPrintSink;\n const parsed = parseSource(options.source);\n let result: SkillInstallationStatus;\n if (parsed.kind === 'npm') {\n const installOpts: InstallSkillFromNpmOptions = {\n packageName: parsed.value,\n ...(options.version !== undefined ? { version: options.version } : {}),\n ...(options.trustLevel !== undefined ? { trustLevel: options.trustLevel } : {}),\n ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),\n ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),\n };\n result = await installSkillFromNpm(installOpts);\n } else {\n const installOpts: InstallSkillFromGitOptions = {\n repoUrl: parsed.value,\n ...(options.ref !== undefined ? { ref: options.ref } : {}),\n ...(options.trustLevel !== undefined ? { trustLevel: options.trustLevel } : {}),\n ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),\n };\n result = await installSkillFromGit(installOpts);\n }\n emitReport(options, result, () => {\n print(\n brand(\n `installed skill '${result.id}' (${result.source.kind}, signatureVerified=${result.signatureVerified})`,\n ),\n );\n if (result.signature !== undefined) {\n print(` signature valid: ${result.signature.valid}`);\n }\n });\n return result;\n}\n\n/** @stable */\nexport interface SkillsInspectOptions extends SkillsCommonOptions {\n readonly name: string;\n}\n\n/** @stable */\nexport async function runSkillsInspect(\n options: SkillsInspectOptions,\n): Promise<SkillInstallationStatus | null> {\n const all = auditInstalledSkills();\n const match = all.find((s) => s.id === options.name) ?? null;\n emitReport(options, match, () => {\n const print = options.print ?? defaultPrintSink;\n if (match === null) {\n print(brand(`skill '${options.name}' not found in this process registry.`));\n process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;\n return;\n }\n print(brand(`skill ${match.id}`));\n print(` source: ${match.source.kind}`);\n print(` trust level: ${match.trustLevel}`);\n print(` ignoreScripts: ${match.ignoreScripts}`);\n print(` signatureVerified: ${match.signatureVerified}`);\n print(` installedAt: ${new Date(match.installedAt).toISOString()}`);\n if (match.installPath !== undefined) print(` installPath: ${match.installPath}`);\n if (match.publisher !== undefined) print(` publisher: ${match.publisher}`);\n });\n return match;\n}\n\n/** @stable */\nexport interface SkillsAuditOptions extends SkillsCommonOptions {}\n\n/** @stable */\nexport function runSkillsAudit(options: SkillsAuditOptions = {}) {\n const all = auditInstalledSkills();\n emitReport(options, all, () => {\n const print = options.print ?? defaultPrintSink;\n if (all.length === 0) {\n print(brand('no skills installed in this process registry.'));\n return;\n }\n print(brand(`${all.length} installed skill(s):`));\n for (const s of all) {\n const mark = s.signatureVerified ? statusMarker('ok') : statusMarker('warn');\n print(\n ` ${mark} ${s.id} (${s.source.kind}, trust=${s.trustLevel}, signatureVerified=${s.signatureVerified})`,\n );\n }\n });\n return all;\n}\n\n/** @stable */\nexport interface SkillsMigrateFrontmatterOptions extends SkillsCommonOptions {\n /** Directory to walk. Defaults to `process.cwd()`. */\n readonly path?: string;\n readonly recursive?: boolean;\n readonly apply?: boolean;\n}\n\n/** @stable */\nexport interface SkillsMigrateFrontmatterResult {\n readonly directory: string;\n readonly visited: number;\n readonly migrated: ReadonlyArray<{\n readonly file: string;\n readonly result: MigrationResult;\n }>;\n readonly applied: boolean;\n}\n\n/** @stable */\nexport async function runSkillsMigrateFrontmatter(\n options: SkillsMigrateFrontmatterOptions = {},\n): Promise<SkillsMigrateFrontmatterResult> {\n const root = resolve(options.path ?? process.cwd());\n const recursive = options.recursive === true;\n const apply = options.apply === true;\n const files = await collectSkillMd(root, recursive);\n const migrated: Array<{ file: string; result: MigrationResult }> = [];\n for (const file of files) {\n const raw = await readFile(file, 'utf8');\n let result: MigrationResult;\n try {\n result = migrateFrontmatter(raw, { apply, skillId: file });\n } catch {\n continue;\n }\n if (result.changed) {\n if (apply) {\n await writeFile(file, result.migratedSkillMd, { mode: 0o600 });\n }\n migrated.push({ file, result });\n }\n }\n const out: SkillsMigrateFrontmatterResult = Object.freeze({\n directory: root,\n visited: files.length,\n migrated: Object.freeze(migrated),\n applied: apply,\n });\n emitReport(options, out, () => {\n const print = options.print ?? defaultPrintSink;\n print(\n brand(`migrate-frontmatter visited ${out.visited} SKILL.md file(s) under ${out.directory}.`),\n );\n if (out.migrated.length === 0) {\n print(brand('no rewrites required.'));\n return;\n }\n print(\n brand(\n `${out.migrated.length} file(s) ${apply ? 'rewritten' : 'would be rewritten (dry-run)'}:`,\n ),\n );\n for (const m of out.migrated) print(` - ${m.file}`);\n });\n return out;\n}\n\nfunction parseSource(input: string): { readonly kind: 'npm' | 'git'; readonly value: string } {\n if (input.startsWith('npm:')) return { kind: 'npm', value: input.slice(4) };\n if (input.startsWith('git:')) return { kind: 'git', value: input.slice(4) };\n if (input.includes('://')) return { kind: 'git', value: input };\n return { kind: 'npm', value: input };\n}\n\nasync function collectSkillMd(root: string, recursive: boolean): Promise<string[]> {\n const out: string[] = [];\n await walk(root, recursive, out);\n return out;\n}\n\nasync function walk(dir: string, recursive: boolean, acc: string[]): Promise<void> {\n let entries: string[];\n try {\n entries = await readdir(dir);\n } catch {\n return;\n }\n for (const name of entries) {\n if (name === 'node_modules' || name === '.git' || name === 'dist') continue;\n const full = join(dir, name);\n let s: import('node:fs').Stats;\n try {\n s = await stat(full);\n } catch {\n continue;\n }\n if (s.isDirectory()) {\n if (recursive) await walk(full, recursive, acc);\n } else if (name === 'SKILL.md') {\n acc.push(full);\n }\n }\n}\n\n// Touch the loader once so the import is preserved (downstream callers\n// that want to re-load the migrated skill consume `loadSkills(...)`).\nvoid loadSkills;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/commands/start.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `graphorin start [--config <path>]`. Boots the standalone server.
|
|
4
|
+
*
|
|
5
|
+
* Exits 1 on every recoverable failure (missing pepper, unresolvable
|
|
6
|
+
* SecretRef, missing encryption peer, migration error). The CLI never
|
|
7
|
+
* prints raw secret values; failure messages reference the offending
|
|
8
|
+
* config path + the suggested `graphorin doctor` follow-up (Phase 15).
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Selector for which `SecretsStore` flavour the server activates at
|
|
14
|
+
* startup. Mirrors `--secrets-source` from DEC-136.
|
|
15
|
+
*
|
|
16
|
+
* @stable
|
|
17
|
+
*/
|
|
18
|
+
type SecretsSourceFlag = 'auto' | 'keyring' | 'encrypted-file' | 'env';
|
|
19
|
+
/**
|
|
20
|
+
* @stable
|
|
21
|
+
*/
|
|
22
|
+
interface StartCommandOptions {
|
|
23
|
+
readonly config?: string;
|
|
24
|
+
readonly host?: string;
|
|
25
|
+
readonly port?: number;
|
|
26
|
+
readonly logResolved?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Override the `secrets.source` field of the loaded config. Mirrors
|
|
29
|
+
* the `--secrets-source <kind>` flag from DEC-136.
|
|
30
|
+
*/
|
|
31
|
+
readonly secretsSource?: SecretsSourceFlag;
|
|
32
|
+
/**
|
|
33
|
+
* Refuse to fall back when the requested primary store is
|
|
34
|
+
* unavailable. Mirrors `--strict-secrets` from DEC-136.
|
|
35
|
+
*/
|
|
36
|
+
readonly strictSecrets?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Programmatic entry — used both by the CLI binary and by tests so
|
|
40
|
+
* the spawn cost of running the binary is paid only when an operator
|
|
41
|
+
* actually invokes `graphorin start` from a shell.
|
|
42
|
+
*
|
|
43
|
+
* @stable
|
|
44
|
+
*/
|
|
45
|
+
declare function runStart(options?: StartCommandOptions): Promise<{
|
|
46
|
+
readonly host: string;
|
|
47
|
+
readonly port: number;
|
|
48
|
+
}>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { SecretsSourceFlag, StartCommandOptions, runStart };
|
|
51
|
+
//# sourceMappingURL=start.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start.d.ts","names":[],"sources":["../../src/commands/start.ts"],"sourcesContent":[],"mappings":";;AAwBA;AAKA;AAwBA;;;;;;;;;;;;;KA7BY,iBAAA;;;;UAKK,mBAAA;;;;;;;;;2BASU;;;;;;;;;;;;;;iBAeL,QAAA,WACX,sBACR"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { loadConfig } from "../internal/load-config.js";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { RefuseToRunAsRootError, applyProcessHardening } from "@graphorin/security";
|
|
4
|
+
import { ConfigInvalidError, GraphorinServerError, createServer } from "@graphorin/server";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/start.ts
|
|
7
|
+
/**
|
|
8
|
+
* `graphorin start [--config <path>]`. Boots the standalone server.
|
|
9
|
+
*
|
|
10
|
+
* Exits 1 on every recoverable failure (missing pepper, unresolvable
|
|
11
|
+
* SecretRef, missing encryption peer, migration error). The CLI never
|
|
12
|
+
* prints raw secret values; failure messages reference the offending
|
|
13
|
+
* config path + the suggested `graphorin doctor` follow-up (Phase 15).
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Programmatic entry — used both by the CLI binary and by tests so
|
|
19
|
+
* the spawn cost of running the binary is paid only when an operator
|
|
20
|
+
* actually invokes `graphorin start` from a shell.
|
|
21
|
+
*
|
|
22
|
+
* @stable
|
|
23
|
+
*/
|
|
24
|
+
async function runStart(options = {}) {
|
|
25
|
+
applyHardeningEarly();
|
|
26
|
+
const loaded = await loadConfig(options.config);
|
|
27
|
+
if (options.logResolved !== false) process.stderr.write(`[graphorin/cli] resolved config: ${loaded.path}\n`);
|
|
28
|
+
const overrides = applyCliOverrides(loaded.config, options);
|
|
29
|
+
let server;
|
|
30
|
+
try {
|
|
31
|
+
server = await createServer({ config: overrides });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
fatal(err);
|
|
34
|
+
}
|
|
35
|
+
if (server === void 0) process.exit(1);
|
|
36
|
+
const listening = await safeStart(server);
|
|
37
|
+
process.stderr.write(`[graphorin/cli] @graphorin/server v${server.version} listening on http://${listening.host}:${listening.port}${server.config.server.basePath}\n`);
|
|
38
|
+
installSignalHandlers(server);
|
|
39
|
+
return listening;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Merge CLI flag overrides into the loaded `graphorin.config` payload.
|
|
43
|
+
* Exported for unit tests so callers can assert the precedence rules
|
|
44
|
+
* without spinning up a real server.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
function applyCliOverrides(input, options) {
|
|
49
|
+
const next = { ...input ?? {} };
|
|
50
|
+
const serverInput = next.server ?? {};
|
|
51
|
+
if (options.host !== void 0 || options.port !== void 0) next.server = {
|
|
52
|
+
...serverInput,
|
|
53
|
+
...options.host !== void 0 ? { host: options.host } : {},
|
|
54
|
+
...options.port !== void 0 ? { port: options.port } : {}
|
|
55
|
+
};
|
|
56
|
+
const secretsInput = next.secrets ?? {};
|
|
57
|
+
if (options.secretsSource !== void 0 || options.strictSecrets !== void 0) next.secrets = {
|
|
58
|
+
...secretsInput,
|
|
59
|
+
...options.secretsSource !== void 0 ? { source: options.secretsSource } : {},
|
|
60
|
+
...options.strictSecrets !== void 0 ? { strict: options.strictSecrets } : {}
|
|
61
|
+
};
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
async function safeStart(server) {
|
|
65
|
+
try {
|
|
66
|
+
return await server.start();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
fatal(err);
|
|
69
|
+
}
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
function fatal(err) {
|
|
73
|
+
if (err instanceof ConfigInvalidError) {
|
|
74
|
+
process.stderr.write("[graphorin/cli] graphorin.config invalid:\n");
|
|
75
|
+
for (const issue of err.issues) process.stderr.write(` - ${issue.path.join(".") || "<root>"}: ${issue.message}\n`);
|
|
76
|
+
} else if (err instanceof GraphorinServerError) {
|
|
77
|
+
process.stderr.write(`[graphorin/cli] ${err.kind}: ${err.message}\n`);
|
|
78
|
+
if (err.hint !== void 0) process.stderr.write(` hint: ${err.hint}\n`);
|
|
79
|
+
} else if (err instanceof Error) process.stderr.write(`[graphorin/cli] ${err.message}\n`);
|
|
80
|
+
else process.stderr.write(`[graphorin/cli] unknown error: ${String(err)}\n`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Apply the standard hardening hooks before the server bootstraps.
|
|
85
|
+
* Failures (e.g. running as root on Linux/macOS) abort the start with
|
|
86
|
+
* a documented exit code.
|
|
87
|
+
*
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
function applyHardeningEarly() {
|
|
91
|
+
try {
|
|
92
|
+
applyProcessHardening({});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err instanceof RefuseToRunAsRootError) {
|
|
95
|
+
process.stderr.write(`[graphorin/cli] ${err.message}\n`);
|
|
96
|
+
process.stderr.write("[graphorin/cli] hint: drop privileges (systemd User=, k8s runAsNonRoot, docker --user) and retry.\n");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function installSignalHandlers(server) {
|
|
103
|
+
let shuttingDown = false;
|
|
104
|
+
const onSignal = (signal) => {
|
|
105
|
+
if (shuttingDown) return;
|
|
106
|
+
shuttingDown = true;
|
|
107
|
+
process.stderr.write(`[graphorin/cli] received ${signal}; draining...\n`);
|
|
108
|
+
server.stop().then(() => {
|
|
109
|
+
process.stderr.write("[graphorin/cli] graceful shutdown complete\n");
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}).catch((err) => {
|
|
112
|
+
process.stderr.write(`[graphorin/cli] shutdown error: ${err.message}\n`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
process.once("SIGTERM", onSignal);
|
|
117
|
+
process.once("SIGINT", onSignal);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
export { applyHardeningEarly, runStart };
|
|
122
|
+
//# sourceMappingURL=start.js.map
|