craftar 0.21.1 → 0.22.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/README.md +16 -1
- package/dist/cli.js +341 -44
- package/dist/cli.js.map +1 -1
- package/dist/core/doctor.js +12 -30
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/drift.js +29 -0
- package/dist/core/drift.js.map +1 -0
- package/dist/core/forge.js +33 -1
- package/dist/core/forge.js.map +1 -1
- package/dist/core/impact.js +14 -11
- package/dist/core/impact.js.map +1 -1
- package/dist/core/param-writes.js +2 -1
- package/dist/core/param-writes.js.map +1 -1
- package/dist/core/recipe-edit.js +2 -1
- package/dist/core/recipe-edit.js.map +1 -1
- package/dist/core/registry.js +4 -3
- package/dist/core/registry.js.map +1 -1
- package/dist/core/schema-fault.js +50 -0
- package/dist/core/schema-fault.js.map +1 -0
- package/dist/core/sync.js +27 -9
- package/dist/core/sync.js.map +1 -1
- package/dist/core/unify.js +4 -3
- package/dist/core/unify.js.map +1 -1
- package/dist/importers/claude-code.js +191 -94
- package/dist/importers/claude-code.js.map +1 -1
- package/dist/importers/decide.js +5 -4
- package/dist/importers/decide.js.map +1 -1
- package/dist/importers/drift-promote.js +1218 -0
- package/dist/importers/drift-promote.js.map +1 -0
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { parseFrontmatter } from "../core/frontmatter.js";
|
|
|
5
5
|
import { exists, listFiles, parseYaml, typeFolder, FORGE_MANIFEST } from "../core/forge.js";
|
|
6
6
|
import { manifestWithSections } from "../core/manifest-edit.js";
|
|
7
7
|
import { decodeForScan, findSecrets, hasUtf16Bom, secretValueKind } from "../core/secrets.js";
|
|
8
|
+
import { schemaFault } from "../core/schema-fault.js";
|
|
8
9
|
import { stripBom, toLf } from "../core/text.js";
|
|
9
10
|
import { fingerprintDir, fingerprintOf } from "../core/fingerprint.js";
|
|
10
11
|
import { FORGE_SCHEMA_SECTIONS, IngredientSchema, ProfileSchema, RecipeSchema, WorkspaceConfigSchema } from "../schema/index.js";
|
|
@@ -22,6 +23,7 @@ import { GENERATED_BANNER, kiroReport } from "./kiro-report.js";
|
|
|
22
23
|
import { deepMerge } from "../core/merge.js";
|
|
23
24
|
import { outName } from "../emitters/shared.js";
|
|
24
25
|
import { parseYamlText } from "../core/yaml-read.js";
|
|
26
|
+
import { note } from "../core/unify.js";
|
|
25
27
|
/** Rules that every workspace shares by intent — they seed the `base` recipe. */
|
|
26
28
|
const BASE_RULES = new Set([
|
|
27
29
|
"README",
|
|
@@ -119,22 +121,14 @@ async function planImport(opts, stage) {
|
|
|
119
121
|
if (!f.endsWith(".md"))
|
|
120
122
|
continue;
|
|
121
123
|
const name = f.replace(/\.md$/, "");
|
|
122
|
-
const src = await readSource(path.join(claudeDir, "rules", f));
|
|
123
|
-
const text = toLf(stripBom(src.text));
|
|
124
124
|
const sm = steeringMeta.get(name);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
file: "rule.md",
|
|
131
|
-
targets: "*",
|
|
132
|
-
tags: [],
|
|
133
|
-
origin: origin(`.claude/rules/${f}`),
|
|
134
|
-
};
|
|
135
|
-
read(meta, { "rule.md": text }, scanOf("rule.md", src), (ref) => {
|
|
125
|
+
const emitted = await readEmitted(claudeDir, "rule", f, origin, sm ? { inclusion: sm.inclusion, fileMatchPattern: sm.fileMatchPattern } : undefined);
|
|
126
|
+
if (!emitted)
|
|
127
|
+
continue;
|
|
128
|
+
const ruleMeta = emitted.meta;
|
|
129
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => {
|
|
136
130
|
ruleNames.push(ref.split("/")[1]);
|
|
137
|
-
if (
|
|
131
|
+
if (ruleMeta.inclusion === "fileMatch")
|
|
138
132
|
scopedRules.set(ref, sm?.fileMatchPattern ?? "");
|
|
139
133
|
else
|
|
140
134
|
refs.base.push(ref);
|
|
@@ -145,24 +139,12 @@ async function planImport(opts, stage) {
|
|
|
145
139
|
for (const f of await safeList(path.join(claudeDir, "agents"))) {
|
|
146
140
|
if (!f.endsWith(".md"))
|
|
147
141
|
continue;
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
name,
|
|
155
|
-
description: data.description?.trim(),
|
|
156
|
-
tools: splitList(data.tools),
|
|
157
|
-
model: data.model?.trim() || undefined,
|
|
158
|
-
file: "agent.md",
|
|
159
|
-
frontmatterRaw: raw ?? undefined,
|
|
160
|
-
targets: "*",
|
|
161
|
-
tags: [],
|
|
162
|
-
origin: origin(`.claude/agents/${f}`),
|
|
163
|
-
};
|
|
164
|
-
read(meta, { "agent.md": body }, scanOf("agent.md", src), (ref) => {
|
|
165
|
-
agentBodies.set(ref, (data.description ?? "") + "\n" + body);
|
|
142
|
+
const emitted = await readEmitted(claudeDir, "agent", f, origin);
|
|
143
|
+
if (!emitted)
|
|
144
|
+
continue;
|
|
145
|
+
const agentMeta = emitted.meta;
|
|
146
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => {
|
|
147
|
+
agentBodies.set(ref, (agentMeta.description ?? "") + "\n" + emitted.files["agent.md"]);
|
|
166
148
|
refs.base.push(ref);
|
|
167
149
|
});
|
|
168
150
|
}
|
|
@@ -170,22 +152,10 @@ async function planImport(opts, stage) {
|
|
|
170
152
|
for (const f of await safeList(path.join(claudeDir, "commands"))) {
|
|
171
153
|
if (!f.endsWith(".md"))
|
|
172
154
|
continue;
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
type: "command",
|
|
178
|
-
name: f.replace(/\.md$/, ""),
|
|
179
|
-
description: data.description?.trim(),
|
|
180
|
-
argumentHint: data["argument-hint"]?.trim() || undefined,
|
|
181
|
-
allowedTools: data["allowed-tools"]?.trim() || undefined,
|
|
182
|
-
file: "command.md",
|
|
183
|
-
frontmatterRaw: raw ?? undefined,
|
|
184
|
-
targets: "*",
|
|
185
|
-
tags: [],
|
|
186
|
-
origin: origin(`.claude/commands/${f}`),
|
|
187
|
-
};
|
|
188
|
-
read(meta, { "command.md": body }, scanOf("command.md", src), (ref) => addRef(refs.base, ref));
|
|
155
|
+
const emitted = await readEmitted(claudeDir, "command", f, origin);
|
|
156
|
+
if (!emitted)
|
|
157
|
+
continue;
|
|
158
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => addRef(refs.base, ref));
|
|
189
159
|
}
|
|
190
160
|
/* ---- skills ---- */
|
|
191
161
|
const skillsDir = path.join(claudeDir, "skills");
|
|
@@ -194,32 +164,13 @@ async function planImport(opts, stage) {
|
|
|
194
164
|
for (const e of (await fs.readdir(skillsDir, { withFileTypes: true })).sort((x, y) => x.name.localeCompare(y.name))) {
|
|
195
165
|
if (e.name === ".gitkeep")
|
|
196
166
|
continue;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for (const rel of await listFiles(path.join(skillsDir, e.name))) {
|
|
201
|
-
const abs = path.join(skillsDir, e.name, rel);
|
|
202
|
-
if (/\.(md|txt|json|ya?ml|ps1|py|sh|js|ts)$/i.test(rel)) {
|
|
203
|
-
const src = await readSource(abs);
|
|
204
|
-
files[rel] = toLf(stripBom(src.text));
|
|
205
|
-
Object.assign(scan, scanOf(rel, src));
|
|
206
|
-
}
|
|
207
|
-
else
|
|
208
|
-
files[rel] = await fs.readFile(abs);
|
|
209
|
-
}
|
|
210
|
-
if (!files["SKILL.md"]) {
|
|
167
|
+
const emitted = await readEmitted(claudeDir, "skill", e.name, origin);
|
|
168
|
+
if (!emitted) {
|
|
169
|
+
if (e.isDirectory())
|
|
211
170
|
report.warnings.push(`skill dir ${e.name} has no SKILL.md; skipped`);
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
const meta = { type: "skill", name: e.name, layout: "dir", targets: "*", tags: [], origin: origin(`.claude/skills/${e.name}/`) };
|
|
215
|
-
read(meta, files, scan, (ref) => addRef(refs.base, ref));
|
|
216
|
-
}
|
|
217
|
-
else if (e.name.endsWith(".md")) {
|
|
218
|
-
const src = await readSource(path.join(skillsDir, e.name));
|
|
219
|
-
const text = toLf(stripBom(src.text));
|
|
220
|
-
const meta = { type: "skill", name: e.name.replace(/\.md$/, ""), layout: "file", targets: "*", tags: [], origin: origin(`.claude/skills/${e.name}`) };
|
|
221
|
-
read(meta, { "SKILL.md": text }, scanOf("SKILL.md", src), (ref) => addRef(refs.base, ref));
|
|
171
|
+
continue;
|
|
222
172
|
}
|
|
173
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => addRef(refs.base, ref));
|
|
223
174
|
}
|
|
224
175
|
}
|
|
225
176
|
/* ---- scripts & hooks ---- */
|
|
@@ -227,20 +178,10 @@ async function planImport(opts, stage) {
|
|
|
227
178
|
for (const f of await safeList(path.join(claudeDir, kind))) {
|
|
228
179
|
if (f === ".gitkeep" || f.startsWith("__pycache__") || f.endsWith(".pyc"))
|
|
229
180
|
continue;
|
|
230
|
-
const
|
|
231
|
-
if (!
|
|
181
|
+
const emitted = await readEmitted(claudeDir, kind === "scripts" ? "script" : "hook", f, origin);
|
|
182
|
+
if (!emitted)
|
|
232
183
|
continue;
|
|
233
|
-
|
|
234
|
-
const content = src ? toLf(stripBom(src.text)) : await fs.readFile(abs);
|
|
235
|
-
const meta = {
|
|
236
|
-
type: kind === "scripts" ? "script" : "hook",
|
|
237
|
-
name: f.replace(/\.[^.]+$/, "").toLowerCase(),
|
|
238
|
-
files: [f],
|
|
239
|
-
targets: ["claude-code"],
|
|
240
|
-
tags: [],
|
|
241
|
-
origin: origin(`.claude/${kind}/${f}`),
|
|
242
|
-
};
|
|
243
|
-
read(meta, { [f]: content }, src ? scanOf(f, src) : {}, (ref) => addRef(refs.base, ref));
|
|
184
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => addRef(refs.base, ref));
|
|
244
185
|
}
|
|
245
186
|
}
|
|
246
187
|
/* ---- MCP servers ---- */
|
|
@@ -471,6 +412,162 @@ export class ForgeStage {
|
|
|
471
412
|
throw new Error(`${e instanceof Error ? e.message : String(e)}\nThe import failed while writing into the Forge (${already}); restore or remove those paths before re-running.`, { cause: e });
|
|
472
413
|
}
|
|
473
414
|
}
|
|
415
|
+
/** Staged paths in staging order, Forge-relative with `/`; `created` is false when the path exists on disk under `root`. */
|
|
416
|
+
async entries() {
|
|
417
|
+
const result = [];
|
|
418
|
+
for (const [abs] of this.files) {
|
|
419
|
+
const rel = path.relative(this.root, abs).split(path.sep).join("/");
|
|
420
|
+
const created = !(await exists(abs));
|
|
421
|
+
result.push({ rel, abs, created });
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
/** Write every staged file under another root (same relative paths), in the order given by `order` (a comparator over `rel`), noting each in `journal` before it is touched. */
|
|
426
|
+
async flushTo(root, opts) {
|
|
427
|
+
// Build entries in staging order
|
|
428
|
+
const entries = [];
|
|
429
|
+
for (const [abs, content] of this.files) {
|
|
430
|
+
const rel = path.relative(this.root, abs).split(path.sep).join("/");
|
|
431
|
+
entries.push({ rel, content });
|
|
432
|
+
}
|
|
433
|
+
// Sort if order given
|
|
434
|
+
if (opts?.order) {
|
|
435
|
+
entries.sort((a, b) => opts.order(a.rel, b.rel));
|
|
436
|
+
}
|
|
437
|
+
// Write each entry to the target root
|
|
438
|
+
for (const { rel, content } of entries) {
|
|
439
|
+
const targetAbs = path.join(root, rel);
|
|
440
|
+
await note(opts?.journal, targetAbs);
|
|
441
|
+
await fs.mkdir(path.dirname(targetAbs), { recursive: true });
|
|
442
|
+
await fs.writeFile(targetAbs, content);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* One ingredient as import reads it from a workspace's `.claude/` tree. `rel` is the path under
|
|
448
|
+
* `.claude/<folder>/` (a file, or a skill's directory name). Returns `null` where `planImport`
|
|
449
|
+
* skips: a skill directory without `SKILL.md`, a non-file under scripts/hooks.
|
|
450
|
+
*/
|
|
451
|
+
export async function readEmitted(claudeDir, type, rel, origin, steering) {
|
|
452
|
+
if (type === "rule") {
|
|
453
|
+
const name = rel.replace(/\.md$/, "");
|
|
454
|
+
const src = await readSource(path.join(claudeDir, "rules", rel));
|
|
455
|
+
const text = toLf(stripBom(src.text));
|
|
456
|
+
const meta = {
|
|
457
|
+
type: "rule",
|
|
458
|
+
name,
|
|
459
|
+
inclusion: steering?.inclusion ?? "always",
|
|
460
|
+
fileMatchPattern: steering?.fileMatchPattern,
|
|
461
|
+
file: "rule.md",
|
|
462
|
+
targets: "*",
|
|
463
|
+
tags: [],
|
|
464
|
+
origin: origin(`.claude/rules/${rel}`),
|
|
465
|
+
};
|
|
466
|
+
return { meta, files: { "rule.md": text }, scan: scanOf("rule.md", src) };
|
|
467
|
+
}
|
|
468
|
+
if (type === "agent") {
|
|
469
|
+
const src = await readSource(path.join(claudeDir, "agents", rel));
|
|
470
|
+
const text = src.text;
|
|
471
|
+
const { data, body, raw } = parseFrontmatter(text, { loose: true });
|
|
472
|
+
const name = (data.name || rel.replace(/\.md$/, "")).trim();
|
|
473
|
+
const meta = {
|
|
474
|
+
type: "agent",
|
|
475
|
+
name,
|
|
476
|
+
description: data.description?.trim(),
|
|
477
|
+
tools: splitList(data.tools),
|
|
478
|
+
model: data.model?.trim() || undefined,
|
|
479
|
+
file: "agent.md",
|
|
480
|
+
frontmatterRaw: raw ?? undefined,
|
|
481
|
+
targets: "*",
|
|
482
|
+
tags: [],
|
|
483
|
+
origin: origin(`.claude/agents/${rel}`),
|
|
484
|
+
};
|
|
485
|
+
return { meta, files: { "agent.md": body }, scan: scanOf("agent.md", src) };
|
|
486
|
+
}
|
|
487
|
+
if (type === "command") {
|
|
488
|
+
const src = await readSource(path.join(claudeDir, "commands", rel));
|
|
489
|
+
const text = src.text;
|
|
490
|
+
const { data, body, raw } = parseFrontmatter(text, { loose: true });
|
|
491
|
+
const meta = {
|
|
492
|
+
type: "command",
|
|
493
|
+
name: rel.replace(/\.md$/, ""),
|
|
494
|
+
description: data.description?.trim(),
|
|
495
|
+
argumentHint: data["argument-hint"]?.trim() || undefined,
|
|
496
|
+
allowedTools: data["allowed-tools"]?.trim() || undefined,
|
|
497
|
+
file: "command.md",
|
|
498
|
+
frontmatterRaw: raw ?? undefined,
|
|
499
|
+
targets: "*",
|
|
500
|
+
tags: [],
|
|
501
|
+
origin: origin(`.claude/commands/${rel}`),
|
|
502
|
+
};
|
|
503
|
+
return { meta, files: { "command.md": body }, scan: scanOf("command.md", src) };
|
|
504
|
+
}
|
|
505
|
+
if (type === "skill") {
|
|
506
|
+
const skillDir = path.join(claudeDir, "skills", rel);
|
|
507
|
+
const stat = await fs.lstat(skillDir).catch(() => null);
|
|
508
|
+
if (!stat)
|
|
509
|
+
return null;
|
|
510
|
+
if (stat.isDirectory()) {
|
|
511
|
+
const files = {};
|
|
512
|
+
const scan = {};
|
|
513
|
+
for (const fileRel of await listFiles(skillDir)) {
|
|
514
|
+
const abs = path.join(skillDir, fileRel);
|
|
515
|
+
if (/\.(md|txt|json|ya?ml|ps1|py|sh|js|ts)$/i.test(fileRel)) {
|
|
516
|
+
const src = await readSource(abs);
|
|
517
|
+
files[fileRel] = toLf(stripBom(src.text));
|
|
518
|
+
Object.assign(scan, scanOf(fileRel, src));
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
files[fileRel] = await fs.readFile(abs);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (!files["SKILL.md"])
|
|
525
|
+
return null;
|
|
526
|
+
const meta = {
|
|
527
|
+
type: "skill",
|
|
528
|
+
name: rel,
|
|
529
|
+
layout: "dir",
|
|
530
|
+
targets: "*",
|
|
531
|
+
tags: [],
|
|
532
|
+
origin: origin(`.claude/skills/${rel}/`),
|
|
533
|
+
};
|
|
534
|
+
return { meta, files, scan };
|
|
535
|
+
}
|
|
536
|
+
else if (rel.endsWith(".md")) {
|
|
537
|
+
const src = await readSource(skillDir);
|
|
538
|
+
const text = toLf(stripBom(src.text));
|
|
539
|
+
const meta = {
|
|
540
|
+
type: "skill",
|
|
541
|
+
name: rel.replace(/\.md$/, ""),
|
|
542
|
+
layout: "file",
|
|
543
|
+
targets: "*",
|
|
544
|
+
tags: [],
|
|
545
|
+
origin: origin(`.claude/skills/${rel}`),
|
|
546
|
+
};
|
|
547
|
+
return { meta, files: { "SKILL.md": text }, scan: scanOf("SKILL.md", src) };
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
if (type === "script" || type === "hook") {
|
|
552
|
+
const kind = type === "script" ? "scripts" : "hooks";
|
|
553
|
+
const abs = path.join(claudeDir, kind, rel);
|
|
554
|
+
// fs.stat throws on a dangling link (ENOENT) — spec 30r1 §3.4 says we must let it throw
|
|
555
|
+
const stat = await fs.stat(abs);
|
|
556
|
+
if (!stat.isFile())
|
|
557
|
+
return null;
|
|
558
|
+
const src = /\.(ps1|py|sh|js|ts|cjs|mjs|json|md|txt|ya?ml)$/i.test(rel) ? await readSource(abs) : null;
|
|
559
|
+
const content = src ? toLf(stripBom(src.text)) : await fs.readFile(abs);
|
|
560
|
+
const meta = {
|
|
561
|
+
type,
|
|
562
|
+
name: rel.replace(/\.[^.]+$/, "").toLowerCase(),
|
|
563
|
+
files: [rel],
|
|
564
|
+
targets: ["claude-code"],
|
|
565
|
+
tags: [],
|
|
566
|
+
origin: origin(`.claude/${kind}/${rel}`),
|
|
567
|
+
};
|
|
568
|
+
return { meta, files: { [rel]: content }, scan: src ? scanOf(rel, src) : {} };
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
474
571
|
}
|
|
475
572
|
/**
|
|
476
573
|
* A reviewer agent belongs to the stack of a scoped rule when its name is derived from the rule name
|
|
@@ -490,7 +587,7 @@ function addRef(list, ref) {
|
|
|
490
587
|
list.push(ref);
|
|
491
588
|
}
|
|
492
589
|
/** First secret-like value in an ingredient about to be imported, described by location only. */
|
|
493
|
-
function secretIn(meta, files, scan = {}) {
|
|
590
|
+
export function secretIn(meta, files, scan = {}) {
|
|
494
591
|
const origin = meta.origin?.path ?? `${meta.type}/${meta.name}`;
|
|
495
592
|
const raw = "frontmatterRaw" in meta ? meta.frontmatterRaw : undefined;
|
|
496
593
|
// Bodies of agents and commands start after `---`, the raw block and the closing `---`.
|
|
@@ -698,12 +795,12 @@ async function writeIngredient(stage, meta, files, profile, report, scan = {}, r
|
|
|
698
795
|
* `env` value, a name that is not slug-like) fails the whole import, naming its source, before the
|
|
699
796
|
* first write — 0.2.4 wrote a Forge no command could load (spec 07, Ruling 6).
|
|
700
797
|
*/
|
|
701
|
-
function validateImported(meta) {
|
|
798
|
+
export function validateImported(meta) {
|
|
702
799
|
const r = IngredientSchema.safeParse(meta);
|
|
703
800
|
if (r.success)
|
|
704
801
|
return r.data;
|
|
705
802
|
const source = meta.origin?.path ?? `${meta.type}/${meta.name}`;
|
|
706
|
-
throw new Error(`${source} (${meta.type}/${meta.name}) does not fit the ingredient schema: ${r.error
|
|
803
|
+
throw new Error(`${source} (${meta.type}/${meta.name}) does not fit the ingredient schema: ${schemaFault(r.error)}`);
|
|
707
804
|
}
|
|
708
805
|
function currentRuleOrder(forge, profile) {
|
|
709
806
|
if (!forge?.profiles.has(profile))
|
|
@@ -834,7 +931,7 @@ async function writeOwnedRecipe(o, name, list, description) {
|
|
|
834
931
|
* declaring another name, would leave two recipes under one name and loadForge would keep one.
|
|
835
932
|
* Refused before the first write, as I4 refuses the same split for a profile.
|
|
836
933
|
*/
|
|
837
|
-
async function recipeFile(o, name) {
|
|
934
|
+
export async function recipeFile(o, name, command = "import") {
|
|
838
935
|
const file = path.join(o.dir, `${name}.yaml`);
|
|
839
936
|
for (const f of await o.stage.reader().list(o.dir)) {
|
|
840
937
|
if (f.includes("/") || !/\.ya?ml$/.test(f))
|
|
@@ -842,14 +939,14 @@ async function recipeFile(o, name) {
|
|
|
842
939
|
const abs = path.join(o.dir, f);
|
|
843
940
|
const declared = parseYaml(abs, stripBom(await o.stage.readText(abs)), RecipeSchema).name;
|
|
844
941
|
if (abs === file && declared !== name)
|
|
845
|
-
throw new Error(
|
|
942
|
+
throw new Error(`${command}: recipes/${f} is recipe ${declared} — ${command} writes recipe ${name} there`);
|
|
846
943
|
if (abs !== file && declared === name)
|
|
847
|
-
throw new Error(
|
|
944
|
+
throw new Error(`${command}: recipe ${name} is recipes/${f} — ${command} writes recipes/${name}.yaml`);
|
|
848
945
|
}
|
|
849
946
|
return file;
|
|
850
947
|
}
|
|
851
948
|
/** The recipes a profile resolves inside the Forge; fails closed — a profile that does not resolve counts as using everything. */
|
|
852
|
-
function recipesOf(forge, profile) {
|
|
949
|
+
export function recipesOf(forge, profile) {
|
|
853
950
|
try {
|
|
854
951
|
return new Set(resolve(forge, WorkspaceConfigSchema.parse({ forge: ".", profile })).recipes);
|
|
855
952
|
}
|
|
@@ -885,7 +982,7 @@ async function existingProfile(forge, profile, loaded) {
|
|
|
885
982
|
* created or variant source, which sync would read as structure. The line counts from the top of
|
|
886
983
|
* the workspace file, as the secret scan's does: an agent or command body starts after its frontmatter.
|
|
887
984
|
*/
|
|
888
|
-
function markerIn(meta, files, source) {
|
|
985
|
+
export function markerIn(meta, files, source) {
|
|
889
986
|
const raw = "frontmatterRaw" in meta ? meta.frontmatterRaw : undefined;
|
|
890
987
|
const offset = raw ? raw.split("\n").length + 2 : 0;
|
|
891
988
|
for (const [rel, content] of Object.entries(files)) {
|
|
@@ -1045,7 +1142,7 @@ async function planConfig(ws, forge, profile, targets) {
|
|
|
1045
1142
|
throw i9("the edit does not read back as exactly forge, profile and targets set");
|
|
1046
1143
|
const loaded = WorkspaceConfigSchema.safeParse(after);
|
|
1047
1144
|
if (!loaded.success)
|
|
1048
|
-
throw i9(`it no longer loads: ${loaded.error
|
|
1145
|
+
throw i9(`it no longer loads: ${schemaFault(loaded.error)}`);
|
|
1049
1146
|
return { abs, content, action: content === raw ? "unchanged" : "edited", keptRemote };
|
|
1050
1147
|
}
|
|
1051
1148
|
//# sourceMappingURL=claude-code.js.map
|