@graphorin/cli 0.6.0 → 0.7.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +4 -4
  3. package/dist/bin/graphorin.js +51 -13
  4. package/dist/bin/graphorin.js.map +1 -1
  5. package/dist/commands/audit.d.ts.map +1 -1
  6. package/dist/commands/audit.js +2 -1
  7. package/dist/commands/audit.js.map +1 -1
  8. package/dist/commands/consolidator.d.ts +56 -1
  9. package/dist/commands/consolidator.d.ts.map +1 -1
  10. package/dist/commands/consolidator.js +88 -2
  11. package/dist/commands/consolidator.js.map +1 -1
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +2 -1
  14. package/dist/commands/doctor.js.map +1 -1
  15. package/dist/commands/index.d.ts +4 -4
  16. package/dist/commands/index.js +4 -4
  17. package/dist/commands/init.d.ts +11 -4
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +15 -11
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/memory.d.ts +26 -1
  22. package/dist/commands/memory.d.ts.map +1 -1
  23. package/dist/commands/memory.js +56 -3
  24. package/dist/commands/memory.js.map +1 -1
  25. package/dist/commands/migrate-export.d.ts +1 -2
  26. package/dist/commands/migrate-export.d.ts.map +1 -1
  27. package/dist/commands/migrate-export.js +2 -1
  28. package/dist/commands/migrate-export.js.map +1 -1
  29. package/dist/commands/pricing.d.ts +6 -0
  30. package/dist/commands/pricing.d.ts.map +1 -1
  31. package/dist/commands/pricing.js +5 -2
  32. package/dist/commands/pricing.js.map +1 -1
  33. package/dist/commands/secrets.d.ts.map +1 -1
  34. package/dist/commands/secrets.js +2 -2
  35. package/dist/commands/secrets.js.map +1 -1
  36. package/dist/commands/skills.d.ts.map +1 -1
  37. package/dist/commands/skills.js +1 -1
  38. package/dist/commands/skills.js.map +1 -1
  39. package/dist/commands/storage.d.ts +41 -1
  40. package/dist/commands/storage.d.ts.map +1 -1
  41. package/dist/commands/storage.js +75 -1
  42. package/dist/commands/storage.js.map +1 -1
  43. package/dist/commands/token.d.ts.map +1 -1
  44. package/dist/commands/token.js +2 -2
  45. package/dist/commands/token.js.map +1 -1
  46. package/dist/commands/tools-lint.js +1 -1
  47. package/dist/commands/tools-lint.js.map +1 -1
  48. package/dist/commands/traces.d.ts +14 -2
  49. package/dist/commands/traces.d.ts.map +1 -1
  50. package/dist/commands/traces.js +39 -22
  51. package/dist/commands/traces.js.map +1 -1
  52. package/dist/commands/triggers.d.ts.map +1 -1
  53. package/dist/commands/triggers.js +5 -2
  54. package/dist/commands/triggers.js.map +1 -1
  55. package/dist/index.d.ts +5 -6
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +7 -7
  58. package/dist/index.js.map +1 -1
  59. package/dist/internal/output.js +6 -0
  60. package/dist/internal/output.js.map +1 -1
  61. package/dist/internal/store-context.js +13 -2
  62. package/dist/internal/store-context.js.map +1 -1
  63. package/dist/package.js +6 -0
  64. package/dist/package.js.map +1 -0
  65. package/package.json +18 -14
  66. package/src/bin/graphorin.ts +1387 -0
  67. package/src/commands/audit.ts +256 -0
  68. package/src/commands/auth.ts +238 -0
  69. package/src/commands/consolidator.ts +382 -0
  70. package/src/commands/doctor.ts +253 -0
  71. package/src/commands/guard.ts +144 -0
  72. package/src/commands/index.ts +223 -0
  73. package/src/commands/init.ts +194 -0
  74. package/src/commands/memory.ts +1052 -0
  75. package/src/commands/migrate-config.ts +77 -0
  76. package/src/commands/migrate-export.ts +117 -0
  77. package/src/commands/migrate.ts +83 -0
  78. package/src/commands/pricing.ts +244 -0
  79. package/src/commands/secrets.ts +309 -0
  80. package/src/commands/skills.ts +272 -0
  81. package/src/commands/start.ts +180 -0
  82. package/src/commands/storage.ts +659 -0
  83. package/src/commands/telemetry.ts +91 -0
  84. package/src/commands/token.ts +361 -0
  85. package/src/commands/tools-lint.ts +430 -0
  86. package/src/commands/traces.ts +188 -0
  87. package/src/commands/triggers.ts +237 -0
  88. package/src/index.ts +30 -0
  89. package/src/internal/exit.ts +62 -0
  90. package/src/internal/load-config.ts +107 -0
  91. package/src/internal/offline.ts +81 -0
  92. package/src/internal/output.ts +146 -0
  93. package/src/internal/prompts.ts +58 -0
  94. package/src/internal/store-context.ts +165 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * `graphorin secrets` - manage the operator's secrets store.
3
+ *
4
+ * Surface (per Phase 15 § Secrets):
5
+ *
6
+ * - `graphorin secrets list`
7
+ * - `graphorin secrets get <key>`
8
+ * - `graphorin secrets set <key> [--value <v>] [--from-stdin] [--scope <ref>]`
9
+ * - `graphorin secrets delete <key>`
10
+ * - `graphorin secrets ref <uri>` - test resolution of a `SecretRef` URI.
11
+ * - `graphorin secrets rotate <key> --new-value <v>`
12
+ *
13
+ * Honours `--secrets-source <kind>` and `--strict-secrets` per
14
+ * DEC-136 - both flags are forwarded to `createSecretsStore(...)` so
15
+ * the CLI activates the same store the running server would.
16
+ *
17
+ * The CLI never logs raw secret bytes. `get` prints the value through
18
+ * `value.use((s) => print(s))` so the bytes are released as soon as
19
+ * the print callback returns; `--json` mode emits only the metadata
20
+ * fields, never the value, unless `--reveal` is explicitly supplied.
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+
25
+ import { stdin } from 'node:process';
26
+
27
+ import type { SecretMetadata } from '@graphorin/core/contracts';
28
+ import {
29
+ createSecretsStore,
30
+ getActiveSecretsStore,
31
+ parseSecretRef,
32
+ resolveSecret,
33
+ type SecretsStoreKind,
34
+ SecretValue,
35
+ } from '@graphorin/security';
36
+
37
+ import { EXIT_CODES } from '../internal/exit.js';
38
+ import {
39
+ brand,
40
+ type CommonOutputOptions,
41
+ defaultPrintSink,
42
+ emitReport,
43
+ statusMarker,
44
+ } from '../internal/output.js';
45
+
46
+ /** @stable */
47
+ export interface SecretsCommonOptions extends CommonOutputOptions {
48
+ /** Mirrors `--secrets-source` per DEC-136. */
49
+ readonly secretsSource?: SecretsStoreKind;
50
+ /** Mirrors `--strict-secrets` per DEC-136. */
51
+ readonly strictSecrets?: boolean;
52
+ }
53
+
54
+ /** @stable */
55
+ export interface SecretsListOptions extends SecretsCommonOptions {}
56
+
57
+ /** @stable */
58
+ export async function runSecretsList(
59
+ options: SecretsListOptions = {},
60
+ ): Promise<ReadonlyArray<SecretMetadata>> {
61
+ const store = await openStore(options);
62
+ const list = await store.list();
63
+ emitReport(options, list, () => {
64
+ const print = options.print ?? defaultPrintSink;
65
+ if (list.length === 0) {
66
+ print(brand('no secrets recorded.'));
67
+ return;
68
+ }
69
+ print(brand(`${list.length} secret(s):`));
70
+ for (const m of list) {
71
+ print(` ${statusMarker('ok')} ${m.key} (updated=${m.updatedAt ?? '-'})`);
72
+ }
73
+ });
74
+ return list;
75
+ }
76
+
77
+ /** @stable */
78
+ export interface SecretsGetOptions extends SecretsCommonOptions {
79
+ readonly key: string;
80
+ /**
81
+ * When `true`, print the raw value through the human report. The
82
+ * default is to print only structured metadata so the bytes do not
83
+ * surface in shell history.
84
+ */
85
+ readonly reveal?: boolean;
86
+ }
87
+
88
+ /** @stable */
89
+ export interface SecretsGetResult {
90
+ readonly key: string;
91
+ readonly found: boolean;
92
+ /** Only present when `reveal: true` AND the secret exists. */
93
+ readonly value?: string;
94
+ }
95
+
96
+ /** @stable */
97
+ export async function runSecretsGet(options: SecretsGetOptions): Promise<SecretsGetResult> {
98
+ const store = await openStore(options);
99
+ const value = await store.get(options.key);
100
+ if (value === null) {
101
+ const out: SecretsGetResult = Object.freeze({ key: options.key, found: false });
102
+ emitReport(options, out, () => {
103
+ const print = options.print ?? defaultPrintSink;
104
+ print(brand(`secret '${options.key}' not found.`));
105
+ });
106
+ // W-002: exit code independent of --json (see runAuditVerify).
107
+ process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
108
+ return out;
109
+ }
110
+ if (options.reveal === true) {
111
+ const raw = await value.use((s) => String(s));
112
+ const out: SecretsGetResult = Object.freeze({ key: options.key, found: true, value: raw });
113
+ emitReport(options, out, () => {
114
+ const print = options.print ?? defaultPrintSink;
115
+ print(brand(`secret '${options.key}' resolved (length=${raw.length})`));
116
+ print(` ${raw}`);
117
+ });
118
+ return out;
119
+ }
120
+ const out: SecretsGetResult = Object.freeze({ key: options.key, found: true });
121
+ emitReport(options, out, () => {
122
+ const print = options.print ?? defaultPrintSink;
123
+ print(brand(`secret '${options.key}' found (use --reveal to print the bytes)`));
124
+ });
125
+ return out;
126
+ }
127
+
128
+ /** @stable */
129
+ export interface SecretsSetOptions extends SecretsCommonOptions {
130
+ readonly key: string;
131
+ readonly value?: string;
132
+ /** When `true`, read the value from stdin. */
133
+ readonly fromStdin?: boolean;
134
+ }
135
+
136
+ /** @stable */
137
+ export async function runSecretsSet(options: SecretsSetOptions): Promise<{ readonly ok: true }> {
138
+ const store = await openStore(options);
139
+ let raw = options.value;
140
+ if (raw === undefined && options.fromStdin === true) {
141
+ raw = await readStdin();
142
+ }
143
+ if (raw === undefined) {
144
+ throw new Error(
145
+ '[graphorin/cli] secrets set requires either --value <v> or --from-stdin (no plaintext on the command line).',
146
+ );
147
+ }
148
+ await store.set(
149
+ options.key,
150
+ SecretValue.fromString(raw, { source: { resolver: 'graphorin secrets set' } }),
151
+ );
152
+ emitReport(options, { ok: true } as const, () => {
153
+ const print = options.print ?? defaultPrintSink;
154
+ print(brand(`secret '${options.key}' written.`));
155
+ });
156
+ return { ok: true };
157
+ }
158
+
159
+ /** @stable */
160
+ export interface SecretsDeleteOptions extends SecretsCommonOptions {
161
+ readonly key: string;
162
+ }
163
+
164
+ /** @stable */
165
+ export async function runSecretsDelete(
166
+ options: SecretsDeleteOptions,
167
+ ): Promise<{ readonly ok: true }> {
168
+ const store = await openStore(options);
169
+ await store.delete(options.key);
170
+ emitReport(options, { ok: true } as const, () => {
171
+ const print = options.print ?? defaultPrintSink;
172
+ print(brand(`secret '${options.key}' deleted.`));
173
+ });
174
+ return { ok: true };
175
+ }
176
+
177
+ /** @stable */
178
+ export interface SecretsRefOptions extends CommonOutputOptions {
179
+ readonly uri: string;
180
+ readonly reveal?: boolean;
181
+ }
182
+
183
+ /** @stable */
184
+ export interface SecretsRefResult {
185
+ readonly uri: string;
186
+ readonly scheme: string;
187
+ readonly resolved: boolean;
188
+ readonly length?: number;
189
+ readonly value?: string;
190
+ }
191
+
192
+ /**
193
+ * Test resolution of a `SecretRef` URI. The CLI parses the URI first
194
+ * (sanity check + scheme echo), then resolves through the registered
195
+ * resolver chain.
196
+ *
197
+ * @stable
198
+ */
199
+ export async function runSecretsRef(options: SecretsRefOptions): Promise<SecretsRefResult> {
200
+ const parsed = parseSecretRef(options.uri);
201
+ try {
202
+ const value = await resolveSecret(options.uri);
203
+ const length = await value.use((s) => s.length);
204
+ const raw = options.reveal === true ? await value.use((s) => String(s)) : undefined;
205
+ const out: SecretsRefResult = Object.freeze({
206
+ uri: options.uri,
207
+ scheme: parsed.scheme,
208
+ resolved: true,
209
+ length,
210
+ ...(raw !== undefined ? { value: raw } : {}),
211
+ });
212
+ emitReport(options, out, () => {
213
+ const print = options.print ?? defaultPrintSink;
214
+ print(
215
+ brand(
216
+ `${statusMarker('ok')} resolved '${parsed.scheme}' ref (length=${length}${raw !== undefined ? `, value='${raw}'` : ''})`,
217
+ ),
218
+ );
219
+ });
220
+ return out;
221
+ } catch (err) {
222
+ const out: SecretsRefResult = Object.freeze({
223
+ uri: options.uri,
224
+ scheme: parsed.scheme,
225
+ resolved: false,
226
+ });
227
+ emitReport(options, out, () => {
228
+ const print = options.print ?? defaultPrintSink;
229
+ print(
230
+ brand(`${statusMarker('fail')} ${parsed.scheme} ref failed: ${(err as Error).message}`),
231
+ );
232
+ });
233
+ // W-002: exit code independent of --json (see runAuditVerify).
234
+ process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
235
+ return out;
236
+ }
237
+ }
238
+
239
+ /** @stable */
240
+ export interface SecretsRotateOptions extends SecretsCommonOptions {
241
+ readonly key: string;
242
+ readonly newValue?: string;
243
+ readonly fromStdin?: boolean;
244
+ }
245
+
246
+ /**
247
+ * `graphorin secrets rotate <key>` - overwrite the existing value
248
+ * with a fresh one. Functionally identical to `set` but the CLI
249
+ * surfaces the operation explicitly so audit logs can distinguish
250
+ * a rotation from an initial write.
251
+ *
252
+ * @stable
253
+ */
254
+ export async function runSecretsRotate(
255
+ options: SecretsRotateOptions,
256
+ ): Promise<{ readonly ok: true }> {
257
+ const store = await openStore(options);
258
+ const existing = await store.get(options.key);
259
+ if (existing === null) {
260
+ throw new Error(
261
+ `[graphorin/cli] cannot rotate '${options.key}' - secret does not exist (use 'graphorin secrets set' to create it).`,
262
+ );
263
+ }
264
+ let raw = options.newValue;
265
+ if (raw === undefined && options.fromStdin === true) raw = await readStdin();
266
+ if (raw === undefined) {
267
+ throw new Error(
268
+ '[graphorin/cli] secrets rotate requires either --new-value <v> or --from-stdin.',
269
+ );
270
+ }
271
+ await store.set(
272
+ options.key,
273
+ SecretValue.fromString(raw, { source: { resolver: 'graphorin secrets rotate' } }),
274
+ );
275
+ emitReport(options, { ok: true } as const, () => {
276
+ const print = options.print ?? defaultPrintSink;
277
+ print(brand(`secret '${options.key}' rotated.`));
278
+ });
279
+ return { ok: true };
280
+ }
281
+
282
+ async function openStore(options: SecretsCommonOptions) {
283
+ // Reuse the per-process active store when the caller did not request
284
+ // a different kind. The singleton matches the running server / lib
285
+ // process so consecutive CLI invocations within the same process
286
+ // address the same backing store (this matters for the in-memory
287
+ // store + for subprocess CLI flows that share a parent's resolver
288
+ // chain via env vars).
289
+ if (options.secretsSource === undefined) {
290
+ const active = getActiveSecretsStore();
291
+ if (active !== undefined) return active;
292
+ }
293
+ return await createSecretsStore({
294
+ ...(options.secretsSource !== undefined ? { kind: options.secretsSource } : {}),
295
+ ...(options.strictSecrets !== undefined ? { strict: options.strictSecrets } : {}),
296
+ });
297
+ }
298
+
299
+ async function readStdin(): Promise<string> {
300
+ return await new Promise((resolve, reject) => {
301
+ let buf = '';
302
+ stdin.setEncoding('utf8');
303
+ stdin.on('data', (chunk) => {
304
+ buf += chunk;
305
+ });
306
+ stdin.on('end', () => resolve(buf.replace(/\r?\n$/, '')));
307
+ stdin.on('error', reject);
308
+ });
309
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * `graphorin skills` - install + audit + migrate operator-managed
3
+ * skill packages.
4
+ *
5
+ * Surface (per Phase 15 § Skills):
6
+ *
7
+ * - `graphorin skills install <source>` - `npm:` or `git:` source.
8
+ * Honours allowlist / denylist + signature verification (DEC-140 /
9
+ * ADR-034). Refuses when `GRAPHORIN_OFFLINE=1` is set.
10
+ * - `graphorin skills inspect <name>` - frontmatter + signature +
11
+ * supply-chain status of an installed skill.
12
+ * - `graphorin skills audit` - full audit of every recorded
13
+ * installation.
14
+ * - `graphorin skills migrate-frontmatter [--apply] [--recursive]`
15
+ * - DEC-156: rewrites legacy `graphorin-*` frontmatter fields onto
16
+ * their upstream equivalents.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+
21
+ import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
22
+ import { join, resolve } from 'node:path';
23
+ import process from 'node:process';
24
+
25
+ import {
26
+ auditInstalledSkills,
27
+ type InstallSkillFromGitOptions,
28
+ type InstallSkillFromNpmOptions,
29
+ installSkillFromGit,
30
+ installSkillFromNpm,
31
+ type SkillInstallationStatus,
32
+ } from '@graphorin/security';
33
+ import { loadSkills, type MigrationResult, migrateFrontmatter } from '@graphorin/skills';
34
+
35
+ import { EXIT_CODES } from '../internal/exit.js';
36
+ import { checkOfflineModeBlocked } from '../internal/offline.js';
37
+ import {
38
+ brand,
39
+ type CommonOutputOptions,
40
+ defaultPrintSink,
41
+ emitReport,
42
+ statusMarker,
43
+ } from '../internal/output.js';
44
+
45
+ /** @stable */
46
+ export interface SkillsCommonOptions extends CommonOutputOptions {}
47
+
48
+ /** @stable */
49
+ export type SkillTrustLevelInput = 'trusted' | 'trusted-with-scripts' | 'untrusted';
50
+
51
+ /** @stable */
52
+ export interface SkillsInstallOptions extends SkillsCommonOptions {
53
+ /** `npm:<name>[@version]` or `git:<url>` source. */
54
+ readonly source: string;
55
+ /** Optional explicit version pin (npm sources only). */
56
+ readonly version?: string;
57
+ /** Optional git ref (git sources only). */
58
+ readonly ref?: string;
59
+ /** Trust level for the operator's project. Defaults to the helper's own default. */
60
+ readonly trustLevel?: SkillTrustLevelInput;
61
+ /** Working directory for npm installs. */
62
+ readonly cwd?: string;
63
+ readonly dryRun?: boolean;
64
+ }
65
+
66
+ /** @stable */
67
+ export async function runSkillsInstall(
68
+ options: SkillsInstallOptions,
69
+ ): Promise<SkillInstallationStatus> {
70
+ if (
71
+ !checkOfflineModeBlocked('skills install', {
72
+ ...(options.print !== undefined ? { print: options.print } : {}),
73
+ })
74
+ ) {
75
+ process.exit(EXIT_CODES.RECOVERABLE_FAILURE);
76
+ }
77
+ const print = options.print ?? defaultPrintSink;
78
+ const parsed = parseSource(options.source);
79
+ let result: SkillInstallationStatus;
80
+ if (parsed.kind === 'npm') {
81
+ const installOpts: InstallSkillFromNpmOptions = {
82
+ packageName: parsed.value,
83
+ ...(options.version !== undefined ? { version: options.version } : {}),
84
+ ...(options.trustLevel !== undefined ? { trustLevel: options.trustLevel } : {}),
85
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
86
+ ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
87
+ };
88
+ result = await installSkillFromNpm(installOpts);
89
+ } else {
90
+ const installOpts: InstallSkillFromGitOptions = {
91
+ repoUrl: parsed.value,
92
+ ...(options.ref !== undefined ? { ref: options.ref } : {}),
93
+ ...(options.trustLevel !== undefined ? { trustLevel: options.trustLevel } : {}),
94
+ ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
95
+ };
96
+ result = await installSkillFromGit(installOpts);
97
+ }
98
+ emitReport(options, result, () => {
99
+ print(
100
+ brand(
101
+ `installed skill '${result.id}' (${result.source.kind}, signatureVerified=${result.signatureVerified})`,
102
+ ),
103
+ );
104
+ if (result.signature !== undefined) {
105
+ print(` signature valid: ${result.signature.valid}`);
106
+ }
107
+ });
108
+ return result;
109
+ }
110
+
111
+ /** @stable */
112
+ export interface SkillsInspectOptions extends SkillsCommonOptions {
113
+ readonly name: string;
114
+ }
115
+
116
+ /** @stable */
117
+ export async function runSkillsInspect(
118
+ options: SkillsInspectOptions,
119
+ ): Promise<SkillInstallationStatus | null> {
120
+ const all = auditInstalledSkills();
121
+ const match = all.find((s) => s.id === options.name) ?? null;
122
+ emitReport(options, match, () => {
123
+ const print = options.print ?? defaultPrintSink;
124
+ if (match === null) {
125
+ print(brand(`skill '${options.name}' not found in this process registry.`));
126
+ return;
127
+ }
128
+ print(brand(`skill ${match.id}`));
129
+ print(` source: ${match.source.kind}`);
130
+ print(` trust level: ${match.trustLevel}`);
131
+ print(` ignoreScripts: ${match.ignoreScripts}`);
132
+ print(` signatureVerified: ${match.signatureVerified}`);
133
+ print(` installedAt: ${new Date(match.installedAt).toISOString()}`);
134
+ if (match.installPath !== undefined) print(` installPath: ${match.installPath}`);
135
+ if (match.publisher !== undefined) print(` publisher: ${match.publisher}`);
136
+ });
137
+ // W-002: exit code independent of --json (see runAuditVerify).
138
+ if (match === null) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
139
+ return match;
140
+ }
141
+
142
+ /** @stable */
143
+ export interface SkillsAuditOptions extends SkillsCommonOptions {}
144
+
145
+ /** @stable */
146
+ export function runSkillsAudit(options: SkillsAuditOptions = {}) {
147
+ const all = auditInstalledSkills();
148
+ emitReport(options, all, () => {
149
+ const print = options.print ?? defaultPrintSink;
150
+ if (all.length === 0) {
151
+ print(brand('no skills installed in this process registry.'));
152
+ return;
153
+ }
154
+ print(brand(`${all.length} installed skill(s):`));
155
+ for (const s of all) {
156
+ const mark = s.signatureVerified ? statusMarker('ok') : statusMarker('warn');
157
+ print(
158
+ ` ${mark} ${s.id} (${s.source.kind}, trust=${s.trustLevel}, signatureVerified=${s.signatureVerified})`,
159
+ );
160
+ }
161
+ });
162
+ return all;
163
+ }
164
+
165
+ /** @stable */
166
+ export interface SkillsMigrateFrontmatterOptions extends SkillsCommonOptions {
167
+ /** Directory to walk. Defaults to `process.cwd()`. */
168
+ readonly path?: string;
169
+ readonly recursive?: boolean;
170
+ readonly apply?: boolean;
171
+ }
172
+
173
+ /** @stable */
174
+ export interface SkillsMigrateFrontmatterResult {
175
+ readonly directory: string;
176
+ readonly visited: number;
177
+ readonly migrated: ReadonlyArray<{
178
+ readonly file: string;
179
+ readonly result: MigrationResult;
180
+ }>;
181
+ readonly applied: boolean;
182
+ }
183
+
184
+ /** @stable */
185
+ export async function runSkillsMigrateFrontmatter(
186
+ options: SkillsMigrateFrontmatterOptions = {},
187
+ ): Promise<SkillsMigrateFrontmatterResult> {
188
+ const root = resolve(options.path ?? process.cwd());
189
+ const recursive = options.recursive === true;
190
+ const apply = options.apply === true;
191
+ const files = await collectSkillMd(root, recursive);
192
+ const migrated: Array<{ file: string; result: MigrationResult }> = [];
193
+ for (const file of files) {
194
+ const raw = await readFile(file, 'utf8');
195
+ let result: MigrationResult;
196
+ try {
197
+ result = migrateFrontmatter(raw, { apply, skillId: file });
198
+ } catch {
199
+ continue;
200
+ }
201
+ if (result.changed) {
202
+ if (apply) {
203
+ await writeFile(file, result.migratedSkillMd, { mode: 0o600 });
204
+ }
205
+ migrated.push({ file, result });
206
+ }
207
+ }
208
+ const out: SkillsMigrateFrontmatterResult = Object.freeze({
209
+ directory: root,
210
+ visited: files.length,
211
+ migrated: Object.freeze(migrated),
212
+ applied: apply,
213
+ });
214
+ emitReport(options, out, () => {
215
+ const print = options.print ?? defaultPrintSink;
216
+ print(
217
+ brand(`migrate-frontmatter visited ${out.visited} SKILL.md file(s) under ${out.directory}.`),
218
+ );
219
+ if (out.migrated.length === 0) {
220
+ print(brand('no rewrites required.'));
221
+ return;
222
+ }
223
+ print(
224
+ brand(
225
+ `${out.migrated.length} file(s) ${apply ? 'rewritten' : 'would be rewritten (dry-run)'}:`,
226
+ ),
227
+ );
228
+ for (const m of out.migrated) print(` - ${m.file}`);
229
+ });
230
+ return out;
231
+ }
232
+
233
+ function parseSource(input: string): { readonly kind: 'npm' | 'git'; readonly value: string } {
234
+ if (input.startsWith('npm:')) return { kind: 'npm', value: input.slice(4) };
235
+ if (input.startsWith('git:')) return { kind: 'git', value: input.slice(4) };
236
+ if (input.includes('://')) return { kind: 'git', value: input };
237
+ return { kind: 'npm', value: input };
238
+ }
239
+
240
+ async function collectSkillMd(root: string, recursive: boolean): Promise<string[]> {
241
+ const out: string[] = [];
242
+ await walk(root, recursive, out);
243
+ return out;
244
+ }
245
+
246
+ async function walk(dir: string, recursive: boolean, acc: string[]): Promise<void> {
247
+ let entries: string[];
248
+ try {
249
+ entries = await readdir(dir);
250
+ } catch {
251
+ return;
252
+ }
253
+ for (const name of entries) {
254
+ if (name === 'node_modules' || name === '.git' || name === 'dist') continue;
255
+ const full = join(dir, name);
256
+ let s: import('node:fs').Stats;
257
+ try {
258
+ s = await stat(full);
259
+ } catch {
260
+ continue;
261
+ }
262
+ if (s.isDirectory()) {
263
+ if (recursive) await walk(full, recursive, acc);
264
+ } else if (name === 'SKILL.md') {
265
+ acc.push(full);
266
+ }
267
+ }
268
+ }
269
+
270
+ // Touch the loader once so the import is preserved (downstream callers
271
+ // that want to re-load the migrated skill consume `loadSkills(...)`).
272
+ void loadSkills;