craftar 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/craftar.js +2 -0
- package/dist/cli.js +254 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/forge.js +108 -0
- package/dist/core/forge.js.map +1 -0
- package/dist/core/frontmatter.js +54 -0
- package/dist/core/frontmatter.js.map +1 -0
- package/dist/core/resolve.js +77 -0
- package/dist/core/resolve.js.map +1 -0
- package/dist/core/secrets.js +48 -0
- package/dist/core/secrets.js.map +1 -0
- package/dist/core/sync.js +231 -0
- package/dist/core/sync.js.map +1 -0
- package/dist/core/text.js +34 -0
- package/dist/core/text.js.map +1 -0
- package/dist/emitters/agents-md.js +40 -0
- package/dist/emitters/agents-md.js.map +1 -0
- package/dist/emitters/claude-code.js +76 -0
- package/dist/emitters/claude-code.js.map +1 -0
- package/dist/emitters/kiro.js +154 -0
- package/dist/emitters/kiro.js.map +1 -0
- package/dist/emitters/shared.js +18 -0
- package/dist/emitters/shared.js.map +1 -0
- package/dist/emitters/types.js +2 -0
- package/dist/emitters/types.js.map +1 -0
- package/dist/importers/claude-code.js +429 -0
- package/dist/importers/claude-code.js.map +1 -0
- package/dist/schema/index.js +172 -0
- package/dist/schema/index.js.map +1 -0
- package/package.json +28 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { toCrlf } from "../core/text.js";
|
|
2
|
+
import { serializeFrontmatter } from "../core/frontmatter.js";
|
|
3
|
+
import { listFiles } from "../core/forge.js";
|
|
4
|
+
import { appliesTo, outName } from "./shared.js";
|
|
5
|
+
/**
|
|
6
|
+
* Kiro target. Reproduces, then extends, the behaviour of the hand-written
|
|
7
|
+
* `.claude/scripts/sync-steering.ps1` this target was extracted from:
|
|
8
|
+
* - steering = frontmatter(inclusion) + GENERATED banner + rule body
|
|
9
|
+
* - `.claude/rules/` references are rewritten to `.kiro/steering/`
|
|
10
|
+
* - UTF-8 without BOM, CRLF (the exact shape Kiro already consumes)
|
|
11
|
+
* and additionally generates what the script never did: agents JSON, commands and skills.
|
|
12
|
+
*/
|
|
13
|
+
export const kiro = {
|
|
14
|
+
target: "kiro",
|
|
15
|
+
async emit(ctx) {
|
|
16
|
+
const out = [];
|
|
17
|
+
const t = "kiro";
|
|
18
|
+
const banner = String(ctx.resolution.params["kiro.banner"] ?? "<!-- GENERATED from {{source}} by craftar -- do not edit. -->");
|
|
19
|
+
const ruleNames = new Set(ctx.resolution.ingredients.filter((i) => i.meta.type === "rule").map((i) => outName(i.meta)));
|
|
20
|
+
const scopedRules = ctx.resolution.ingredients.filter((i) => i.meta.type === "rule" && i.meta.inclusion === "fileMatch").map((i) => outName(i.meta));
|
|
21
|
+
for (const ing of ctx.resolution.ingredients) {
|
|
22
|
+
if (!appliesTo(ing.meta.targets, t))
|
|
23
|
+
continue;
|
|
24
|
+
const m = ing.meta;
|
|
25
|
+
switch (m.type) {
|
|
26
|
+
case "rule": {
|
|
27
|
+
const body = rewrite(await ctx.text(ing, m.file));
|
|
28
|
+
const fm = m.inclusion === "fileMatch"
|
|
29
|
+
? `---\ninclusion: fileMatch\nfileMatchPattern: ${JSON.stringify(Array.isArray(m.fileMatchPattern) ? m.fileMatchPattern.join(",") : m.fileMatchPattern ?? "**")}\n---\n\n`
|
|
30
|
+
: `---\ninclusion: ${m.inclusion}\n---\n\n`;
|
|
31
|
+
const head = banner.replace("{{source}}", `.claude/rules/${outName(m)}.md`) + "\n\n";
|
|
32
|
+
out.push(crlf(`.kiro/steering/${outName(m)}.md`, fm + head + body, ing.ref));
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "steering":
|
|
36
|
+
out.push(crlf(`.kiro/steering/${outName(m)}.md`, await ctx.text(ing, m.file), ing.ref));
|
|
37
|
+
break;
|
|
38
|
+
case "agent": {
|
|
39
|
+
const body = rewrite(await ctx.text(ing, m.file));
|
|
40
|
+
const description = rewrite(m.description ?? "");
|
|
41
|
+
const tools = mapTools(m.tools, ctx);
|
|
42
|
+
const resources = m.resources ?? agentResources(outName(m), description + "\n" + body, ruleNames, scopedRules);
|
|
43
|
+
const json = JSON.stringify({ name: outName(m), description, prompt: body.replace(/^\n+/, "").replace(/\n+$/, ""), tools, allowedTools: tools, resources }, null, 2) + "\n";
|
|
44
|
+
out.push(crlf(`.kiro/agents/${outName(m)}.json`, json, ing.ref));
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case "command": {
|
|
48
|
+
const body = rewrite(await ctx.text(ing, m.file));
|
|
49
|
+
const doc = serializeFrontmatter({ description: m.description, "argument-hint": m.argumentHint, "allowed-tools": m.allowedTools }, body, { raw: m.frontmatterRaw ? rewrite(m.frontmatterRaw) : null });
|
|
50
|
+
out.push(crlf(`.kiro/steering/commands/${outName(m)}.md`, `---\ninclusion: manual\n---\n\n` + doc, ing.ref));
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "skill": {
|
|
54
|
+
if (m.layout === "file") {
|
|
55
|
+
out.push(crlf(`.kiro/skills/${outName(m)}/SKILL.md`, rewrite(await ctx.text(ing, "SKILL.md")), ing.ref));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
for (const f of await listFiles(ing.dir)) {
|
|
59
|
+
if (f === "ingredient.yaml")
|
|
60
|
+
continue;
|
|
61
|
+
out.push(await copy(ctx, ing, f, `.kiro/skills/${outName(m)}/${f}`));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "mcp":
|
|
67
|
+
// Kiro reads MCP servers from .kiro/settings/mcp.json
|
|
68
|
+
break;
|
|
69
|
+
case "script":
|
|
70
|
+
case "hook":
|
|
71
|
+
ctx.warn(`kiro: ${m.type} ${ing.ref} has no Kiro equivalent — skipped`);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const mcp = ctx.resolution.ingredients.filter((i) => i.meta.type === "mcp" && appliesTo(i.meta.targets, t));
|
|
76
|
+
if (mcp.length) {
|
|
77
|
+
const servers = {};
|
|
78
|
+
for (const i of mcp)
|
|
79
|
+
if (i.meta.type === "mcp")
|
|
80
|
+
servers[i.meta.name] = i.meta.server;
|
|
81
|
+
out.push(crlf(".kiro/settings/mcp.json", JSON.stringify({ mcpServers: servers }, null, 2) + "\n", "mcp/*"));
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
export function rewrite(text) {
|
|
87
|
+
return text.replace(/\.claude\/rules\//g, ".kiro/steering/");
|
|
88
|
+
}
|
|
89
|
+
function crlf(path, text, ingredient) {
|
|
90
|
+
return { path, content: Buffer.from(toCrlf(text), "utf8"), target: "kiro", ingredient };
|
|
91
|
+
}
|
|
92
|
+
async function copy(ctx, ing, file, relPath) {
|
|
93
|
+
if (/\.(md|txt|json|ya?ml)$/i.test(file))
|
|
94
|
+
return crlf(relPath, rewrite(await ctx.text(ing, file)), ing.ref);
|
|
95
|
+
return { path: relPath, content: await ctx.bytes(ing, file), target: "kiro", ingredient: ing.ref };
|
|
96
|
+
}
|
|
97
|
+
const TOOL_MAP = {
|
|
98
|
+
read: "read",
|
|
99
|
+
grep: "grep",
|
|
100
|
+
glob: "glob",
|
|
101
|
+
bash: "shell",
|
|
102
|
+
edit: "write",
|
|
103
|
+
write: "write",
|
|
104
|
+
multiedit: "write",
|
|
105
|
+
notebookedit: "write",
|
|
106
|
+
webfetch: "web_fetch",
|
|
107
|
+
websearch: "web_search",
|
|
108
|
+
agent: null,
|
|
109
|
+
task: null,
|
|
110
|
+
askuserquestion: null,
|
|
111
|
+
todowrite: null,
|
|
112
|
+
};
|
|
113
|
+
function mapTools(tools, ctx) {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const raw of tools) {
|
|
116
|
+
const key = raw.trim().replace(/\(.*\)$/, "").toLowerCase();
|
|
117
|
+
if (!key)
|
|
118
|
+
continue;
|
|
119
|
+
const mapped = key in TOOL_MAP ? TOOL_MAP[key] : key;
|
|
120
|
+
if (mapped === null) {
|
|
121
|
+
ctx.warn(`kiro: tool "${raw.trim()}" has no Kiro equivalent; dropped`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!out.includes(mapped))
|
|
125
|
+
out.push(mapped);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Kiro custom agents do not auto-load steering, so each agent declares what it reads.
|
|
131
|
+
* A stack reviewer (`backend-api-reviewer` ← scoped rule `backend-api`, `frontend-reviewer` ← `frontend-angular`)
|
|
132
|
+
* gets its own rule + `repo-discovery`; any other agent gets the whole steering tree.
|
|
133
|
+
*/
|
|
134
|
+
export function agentResources(agentName, text, known, scoped) {
|
|
135
|
+
const cited = referencedRules(text, known);
|
|
136
|
+
const bound = scoped.find((r) => agentName.startsWith(r) || (agentName.startsWith(r.split("-")[0] + "-") && cited.includes(r)));
|
|
137
|
+
if (!bound)
|
|
138
|
+
return ["file://.kiro/steering/**/*.md"];
|
|
139
|
+
const list = [bound];
|
|
140
|
+
if (known.has("repo-discovery"))
|
|
141
|
+
list.push("repo-discovery");
|
|
142
|
+
return list.map((r) => `file://.kiro/steering/${r}.md`);
|
|
143
|
+
}
|
|
144
|
+
/** Rule names referenced as `.claude/rules/<name>.md` or `.kiro/steering/<name>.md`, in order of first appearance. */
|
|
145
|
+
export function referencedRules(text, known) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const m of text.matchAll(/\.(?:claude\/rules|kiro\/steering)\/([A-Za-z0-9._-]+)\.md/g)) {
|
|
148
|
+
const name = m[1];
|
|
149
|
+
if (known.has(name) && !out.includes(name))
|
|
150
|
+
out.push(name);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=kiro.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kiro.js","sourceRoot":"","sources":["../../src/emitters/kiro.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { detectEol, hasBom, withEol } from "../core/text.js";
|
|
2
|
+
export function appliesTo(targets, target) {
|
|
3
|
+
return targets === "*" || targets.includes(target);
|
|
4
|
+
}
|
|
5
|
+
const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]);
|
|
6
|
+
/** Text file that keeps the EOL and the BOM of the file it replaces (LF, no BOM when new). */
|
|
7
|
+
export async function textFile(ctx, relPath, text, target, ingredient) {
|
|
8
|
+
const existing = await ctx.readExisting(relPath);
|
|
9
|
+
const eol = existing ? detectEol(existing.toString("utf8")) : "lf";
|
|
10
|
+
const body = Buffer.from(withEol(text, eol), "utf8");
|
|
11
|
+
const content = existing && hasBom(existing) ? Buffer.concat([UTF8_BOM, body]) : body;
|
|
12
|
+
return { path: relPath, content, target, ingredient };
|
|
13
|
+
}
|
|
14
|
+
/** Output basename: a variant (`workflow--acme` with `as: workflow`) emits under the original name. */
|
|
15
|
+
export function outName(m) {
|
|
16
|
+
return m.as ?? m.name;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/emitters/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAY,MAAM,iBAAiB,CAAC;AAGvE,MAAM,UAAU,SAAS,CAAC,OAAuB,EAAE,MAAc;IAC/D,OAAO,OAAO,KAAK,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAEjD,8FAA8F;AAC9F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAgB,EAAE,OAAe,EAAE,IAAY,EAAE,MAA6B,EAAE,UAAkB;IAC/H,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,GAAG,GAAQ,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AACxD,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,OAAO,CAAC,CAAgC;IACtD,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/emitters/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { parseFrontmatter } from "../core/frontmatter.js";
|
|
5
|
+
import { exists, listFiles, typeFolder, FORGE_MANIFEST } from "../core/forge.js";
|
|
6
|
+
import { findSecrets, secretValueKind } from "../core/secrets.js";
|
|
7
|
+
import { hashNormalized, stripBom, toLf } from "../core/text.js";
|
|
8
|
+
/** Rules that every workspace shares by intent — they seed the `base` recipe. */
|
|
9
|
+
const BASE_RULES = new Set([
|
|
10
|
+
"README",
|
|
11
|
+
"workflow",
|
|
12
|
+
"commit-conventions",
|
|
13
|
+
"commit-identity",
|
|
14
|
+
"branch-and-pr",
|
|
15
|
+
"review-posture",
|
|
16
|
+
"worktrees",
|
|
17
|
+
"repo-discovery",
|
|
18
|
+
"handoff",
|
|
19
|
+
"kiro-execution",
|
|
20
|
+
"frontend-visual-verification",
|
|
21
|
+
]);
|
|
22
|
+
const GENERATED_BANNER = /<!--\s*GENERATED from /;
|
|
23
|
+
export async function importClaudeCode(opts) {
|
|
24
|
+
const ws = path.resolve(opts.workspaceRoot);
|
|
25
|
+
const forge = path.resolve(opts.forgeRoot);
|
|
26
|
+
const report = { created: [], reused: [], variants: [], rejected: [], recipes: [], profile: opts.profileName, warnings: [] };
|
|
27
|
+
const claudeDir = path.join(ws, ".claude");
|
|
28
|
+
if (!(await exists(claudeDir)))
|
|
29
|
+
throw new Error(`${claudeDir} not found — is this a Claude Code workspace?`);
|
|
30
|
+
await fs.mkdir(forge, { recursive: true });
|
|
31
|
+
const manifest = path.join(forge, FORGE_MANIFEST);
|
|
32
|
+
if (!(await exists(manifest))) {
|
|
33
|
+
await fs.writeFile(manifest, YAML.stringify({ name: path.basename(forge), schema: 1, description: "Craftar Forge — shared harness ingredients, recipes and client profiles." }));
|
|
34
|
+
report.created.push(FORGE_MANIFEST);
|
|
35
|
+
}
|
|
36
|
+
const origin = (rel) => ({ workspace: path.basename(ws), path: rel.replace(/\\/g, "/") });
|
|
37
|
+
const refs = { base: [], stacks: new Map(), steering: [] };
|
|
38
|
+
const ruleNames = [];
|
|
39
|
+
const scopedRules = new Map(); // rule name → fileMatchPattern
|
|
40
|
+
/* ---- Kiro steering gives us inclusion modes and hand-written steering ---- */
|
|
41
|
+
const steeringDir = path.join(ws, ".kiro", "steering");
|
|
42
|
+
const steeringMeta = new Map();
|
|
43
|
+
if (await exists(steeringDir)) {
|
|
44
|
+
for (const f of await fs.readdir(steeringDir)) {
|
|
45
|
+
if (!f.endsWith(".md"))
|
|
46
|
+
continue;
|
|
47
|
+
const text = toLf(stripBom(await fs.readFile(path.join(steeringDir, f), "utf8")));
|
|
48
|
+
const { data, body } = parseFrontmatter(text);
|
|
49
|
+
steeringMeta.set(f.replace(/\.md$/, ""), {
|
|
50
|
+
inclusion: data.inclusion ?? "always",
|
|
51
|
+
fileMatchPattern: data.fileMatchPattern,
|
|
52
|
+
generated: GENERATED_BANNER.test(body.slice(0, 300)),
|
|
53
|
+
text,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/* ---- rules ---- */
|
|
58
|
+
for (const f of await safeList(path.join(claudeDir, "rules"))) {
|
|
59
|
+
if (!f.endsWith(".md"))
|
|
60
|
+
continue;
|
|
61
|
+
const name = f.replace(/\.md$/, "");
|
|
62
|
+
const text = toLf(stripBom(await fs.readFile(path.join(claudeDir, "rules", f), "utf8")));
|
|
63
|
+
const sm = steeringMeta.get(name);
|
|
64
|
+
const meta = {
|
|
65
|
+
type: "rule",
|
|
66
|
+
name,
|
|
67
|
+
inclusion: sm?.inclusion ?? "always",
|
|
68
|
+
fileMatchPattern: sm?.fileMatchPattern,
|
|
69
|
+
file: "rule.md",
|
|
70
|
+
targets: "*",
|
|
71
|
+
tags: [],
|
|
72
|
+
origin: origin(`.claude/rules/${f}`),
|
|
73
|
+
};
|
|
74
|
+
const ref = await writeIngredient(forge, meta, { "rule.md": text }, opts.profileName, report);
|
|
75
|
+
if (!ref)
|
|
76
|
+
continue;
|
|
77
|
+
ruleNames.push(ref.split("/")[1]);
|
|
78
|
+
if (meta.inclusion === "fileMatch")
|
|
79
|
+
scopedRules.set(ref, sm?.fileMatchPattern ?? "");
|
|
80
|
+
else
|
|
81
|
+
refs.base.push(ref);
|
|
82
|
+
}
|
|
83
|
+
/* ---- agents ---- */
|
|
84
|
+
const agentBodies = new Map();
|
|
85
|
+
for (const f of await safeList(path.join(claudeDir, "agents"))) {
|
|
86
|
+
if (!f.endsWith(".md"))
|
|
87
|
+
continue;
|
|
88
|
+
const text = await fs.readFile(path.join(claudeDir, "agents", f), "utf8");
|
|
89
|
+
const { data, body, raw } = parseFrontmatter(text, { loose: true });
|
|
90
|
+
const name = (data.name || f.replace(/\.md$/, "")).trim();
|
|
91
|
+
const meta = {
|
|
92
|
+
type: "agent",
|
|
93
|
+
name,
|
|
94
|
+
description: data.description?.trim(),
|
|
95
|
+
tools: splitList(data.tools),
|
|
96
|
+
model: data.model?.trim() || undefined,
|
|
97
|
+
file: "agent.md",
|
|
98
|
+
frontmatterRaw: raw ?? undefined,
|
|
99
|
+
targets: "*",
|
|
100
|
+
tags: [],
|
|
101
|
+
origin: origin(`.claude/agents/${f}`),
|
|
102
|
+
};
|
|
103
|
+
const ref = await writeIngredient(forge, meta, { "agent.md": body }, opts.profileName, report);
|
|
104
|
+
if (!ref)
|
|
105
|
+
continue;
|
|
106
|
+
agentBodies.set(ref, (data.description ?? "") + "\n" + body);
|
|
107
|
+
refs.base.push(ref);
|
|
108
|
+
}
|
|
109
|
+
/* ---- commands ---- */
|
|
110
|
+
for (const f of await safeList(path.join(claudeDir, "commands"))) {
|
|
111
|
+
if (!f.endsWith(".md"))
|
|
112
|
+
continue;
|
|
113
|
+
const text = await fs.readFile(path.join(claudeDir, "commands", f), "utf8");
|
|
114
|
+
const { data, body, raw } = parseFrontmatter(text, { loose: true });
|
|
115
|
+
const meta = {
|
|
116
|
+
type: "command",
|
|
117
|
+
name: f.replace(/\.md$/, ""),
|
|
118
|
+
description: data.description?.trim(),
|
|
119
|
+
argumentHint: data["argument-hint"]?.trim() || undefined,
|
|
120
|
+
allowedTools: data["allowed-tools"]?.trim() || undefined,
|
|
121
|
+
file: "command.md",
|
|
122
|
+
frontmatterRaw: raw ?? undefined,
|
|
123
|
+
targets: "*",
|
|
124
|
+
tags: [],
|
|
125
|
+
origin: origin(`.claude/commands/${f}`),
|
|
126
|
+
};
|
|
127
|
+
addRef(refs.base, await writeIngredient(forge, meta, { "command.md": body }, opts.profileName, report));
|
|
128
|
+
}
|
|
129
|
+
/* ---- skills ---- */
|
|
130
|
+
const skillsDir = path.join(claudeDir, "skills");
|
|
131
|
+
if (await exists(skillsDir)) {
|
|
132
|
+
for (const e of await fs.readdir(skillsDir, { withFileTypes: true })) {
|
|
133
|
+
if (e.name === ".gitkeep")
|
|
134
|
+
continue;
|
|
135
|
+
if (e.isDirectory()) {
|
|
136
|
+
const files = {};
|
|
137
|
+
for (const rel of await listFiles(path.join(skillsDir, e.name))) {
|
|
138
|
+
const abs = path.join(skillsDir, e.name, rel);
|
|
139
|
+
files[rel] = /\.(md|txt|json|ya?ml|ps1|py|sh|js|ts)$/i.test(rel) ? toLf(stripBom(await fs.readFile(abs, "utf8"))) : await fs.readFile(abs);
|
|
140
|
+
}
|
|
141
|
+
if (!files["SKILL.md"]) {
|
|
142
|
+
report.warnings.push(`skill dir ${e.name} has no SKILL.md; skipped`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const meta = { type: "skill", name: e.name, layout: "dir", targets: "*", tags: [], origin: origin(`.claude/skills/${e.name}/`) };
|
|
146
|
+
addRef(refs.base, await writeIngredient(forge, meta, files, opts.profileName, report));
|
|
147
|
+
}
|
|
148
|
+
else if (e.name.endsWith(".md")) {
|
|
149
|
+
const text = toLf(stripBom(await fs.readFile(path.join(skillsDir, e.name), "utf8")));
|
|
150
|
+
const meta = { type: "skill", name: e.name.replace(/\.md$/, ""), layout: "file", targets: "*", tags: [], origin: origin(`.claude/skills/${e.name}`) };
|
|
151
|
+
addRef(refs.base, await writeIngredient(forge, meta, { "SKILL.md": text }, opts.profileName, report));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/* ---- scripts & hooks ---- */
|
|
156
|
+
for (const kind of ["scripts", "hooks"]) {
|
|
157
|
+
for (const f of await safeList(path.join(claudeDir, kind))) {
|
|
158
|
+
if (f === ".gitkeep" || f.startsWith("__pycache__") || f.endsWith(".pyc"))
|
|
159
|
+
continue;
|
|
160
|
+
const abs = path.join(claudeDir, kind, f);
|
|
161
|
+
if (!(await fs.stat(abs)).isFile())
|
|
162
|
+
continue;
|
|
163
|
+
const content = /\.(ps1|py|sh|js|ts|cjs|mjs|json|md|txt|ya?ml)$/i.test(f) ? toLf(stripBom(await fs.readFile(abs, "utf8"))) : await fs.readFile(abs);
|
|
164
|
+
const meta = {
|
|
165
|
+
type: kind === "scripts" ? "script" : "hook",
|
|
166
|
+
name: f.replace(/\.[^.]+$/, "").toLowerCase(),
|
|
167
|
+
files: [f],
|
|
168
|
+
targets: ["claude-code"],
|
|
169
|
+
tags: [],
|
|
170
|
+
origin: origin(`.claude/${kind}/${f}`),
|
|
171
|
+
};
|
|
172
|
+
addRef(refs.base, await writeIngredient(forge, meta, { [f]: content }, opts.profileName, report));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/* ---- MCP servers ---- */
|
|
176
|
+
const mcpFile = path.join(ws, ".mcp.json");
|
|
177
|
+
if (await exists(mcpFile)) {
|
|
178
|
+
const raw = await fs.readFile(mcpFile, "utf8");
|
|
179
|
+
let json;
|
|
180
|
+
try {
|
|
181
|
+
json = JSON.parse(stripBom(raw));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
throw new Error(".mcp.json is not valid JSON — fix the file and re-run import");
|
|
185
|
+
}
|
|
186
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
187
|
+
if (!isPlainObject(json) || ("mcpServers" in json && !isPlainObject(json.mcpServers))) {
|
|
188
|
+
throw new Error(".mcp.json has no valid mcpServers object — fix the file and re-run import");
|
|
189
|
+
}
|
|
190
|
+
for (const [name, server] of Object.entries(json.mcpServers ?? {})) {
|
|
191
|
+
if (!isPlainObject(server)) {
|
|
192
|
+
throw new Error(`.mcp.json server "${name}" is not an object — fix the file and re-run import`);
|
|
193
|
+
}
|
|
194
|
+
const meta = { type: "mcp", name, server, targets: "*", tags: [], origin: origin(".mcp.json") };
|
|
195
|
+
addRef(refs.base, await writeIngredient(forge, meta, {}, opts.profileName, report));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/* ---- hand-written Kiro steering (no Claude counterpart) ---- */
|
|
199
|
+
for (const [name, sm] of steeringMeta) {
|
|
200
|
+
if (ruleNames.includes(name) || sm.generated)
|
|
201
|
+
continue;
|
|
202
|
+
if (name === "commands")
|
|
203
|
+
continue;
|
|
204
|
+
const meta = { type: "steering", name, file: "steering.md", targets: ["kiro"], tags: ["client"], origin: origin(`.kiro/steering/${name}.md`) };
|
|
205
|
+
addRef(refs.steering, await writeIngredient(forge, meta, { "steering.md": sm.text }, opts.profileName, report));
|
|
206
|
+
}
|
|
207
|
+
/* ---- recipes ---- */
|
|
208
|
+
const recipesDir = path.join(forge, "recipes");
|
|
209
|
+
await fs.mkdir(recipesDir, { recursive: true });
|
|
210
|
+
const isVariant = (ref) => report.variants.some((v) => v.name === ref);
|
|
211
|
+
const suffixIf = (name, ingredients) => (ingredients.some(isVariant) ? `${name}--${opts.profileName}` : name);
|
|
212
|
+
const stackRecipes = [];
|
|
213
|
+
for (const [ruleRef, pattern] of scopedRules) {
|
|
214
|
+
const ruleName = ruleRef.split("/")[1].replace(/--.*$/, "");
|
|
215
|
+
const agents = [...agentBodies].filter(([ref, body]) => agentBelongsTo(ref.split("/")[1], ruleName, body)).map(([ref]) => ref);
|
|
216
|
+
for (const a of agents)
|
|
217
|
+
refs.base = refs.base.filter((r) => r !== a);
|
|
218
|
+
const ingredients = [ruleRef, ...agents];
|
|
219
|
+
const recipeName = suffixIf(`stack-${ruleName}`, ingredients);
|
|
220
|
+
await writeRecipe(recipesDir, { name: recipeName, description: `Conventions + reviewer for repos matching ${pattern}`, extends: [], ingredients, params: {} }, report);
|
|
221
|
+
stackRecipes.push(recipeName);
|
|
222
|
+
}
|
|
223
|
+
const baseIngredients = unique(refs.base);
|
|
224
|
+
const baseName = suffixIf("base", baseIngredients);
|
|
225
|
+
await writeRecipe(recipesDir, { name: baseName, description: "Always-on conventions, commands, agents, scripts and MCP servers.", extends: [], ingredients: baseIngredients, params: {} }, report);
|
|
226
|
+
const profileRecipes = [baseName, ...stackRecipes];
|
|
227
|
+
if (refs.steering.length) {
|
|
228
|
+
const n = `${opts.profileName}-steering`;
|
|
229
|
+
await writeRecipe(recipesDir, { name: n, description: `Hand-written Kiro steering specific to ${opts.profileName}.`, extends: [], ingredients: refs.steering, params: {} }, report);
|
|
230
|
+
profileRecipes.push(n);
|
|
231
|
+
}
|
|
232
|
+
/* ---- profile ---- */
|
|
233
|
+
const targets = ["claude-code"];
|
|
234
|
+
if (await exists(path.join(ws, ".kiro")))
|
|
235
|
+
targets.push("kiro");
|
|
236
|
+
const profile = {
|
|
237
|
+
name: opts.profileName,
|
|
238
|
+
description: `Imported from ${path.basename(ws)} on ${new Date().toISOString().slice(0, 10)}.`,
|
|
239
|
+
recipes: profileRecipes,
|
|
240
|
+
targets,
|
|
241
|
+
language: {},
|
|
242
|
+
identity: {},
|
|
243
|
+
scm: await sniffScm(claudeDir),
|
|
244
|
+
naming: {},
|
|
245
|
+
frontend: {},
|
|
246
|
+
executor: (await exists(path.join(claudeDir, "rules", "kiro-execution.md"))) ? { kind: "kiro", terminal: "orca" } : {},
|
|
247
|
+
integrations: {},
|
|
248
|
+
params: {},
|
|
249
|
+
repos: [],
|
|
250
|
+
};
|
|
251
|
+
const profDir = path.join(forge, "profiles", opts.profileName);
|
|
252
|
+
await fs.mkdir(profDir, { recursive: true });
|
|
253
|
+
await fs.writeFile(path.join(profDir, "profile.yaml"), YAML.stringify(profile));
|
|
254
|
+
report.created.push(`profiles/${opts.profileName}/profile.yaml`);
|
|
255
|
+
if (opts.writeWorkspaceConfig) {
|
|
256
|
+
const rel = path.relative(ws, forge).replace(/\\/g, "/") || ".";
|
|
257
|
+
await fs.writeFile(path.join(ws, "craftar.yaml"), YAML.stringify({ forge: rel, profile: opts.profileName, targets }));
|
|
258
|
+
report.created.push("craftar.yaml (workspace)");
|
|
259
|
+
}
|
|
260
|
+
return report;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* A reviewer agent belongs to the stack of a scoped rule when its name is derived from the rule name
|
|
264
|
+
* (`backend-api-reviewer` ← `backend-api`, `frontend-reviewer` ← `frontend-angular`) and it cites the rule.
|
|
265
|
+
* Generic agents such as `docs-author` stay in `base` even when they mention a stack rule.
|
|
266
|
+
*/
|
|
267
|
+
function agentBelongsTo(agentName, ruleName, body) {
|
|
268
|
+
const cites = body.includes(`.claude/rules/${ruleName}.md`);
|
|
269
|
+
const stem = ruleName.split("-")[0];
|
|
270
|
+
return agentName.startsWith(ruleName) || (agentName.startsWith(stem + "-") && cites);
|
|
271
|
+
}
|
|
272
|
+
function unique(xs) {
|
|
273
|
+
return [...new Set(xs)];
|
|
274
|
+
}
|
|
275
|
+
function addRef(list, ref) {
|
|
276
|
+
if (ref)
|
|
277
|
+
list.push(ref);
|
|
278
|
+
}
|
|
279
|
+
/** First secret-like value in an ingredient about to be imported, described by location only. */
|
|
280
|
+
function secretIn(meta, files) {
|
|
281
|
+
const origin = meta.origin?.path ?? `${meta.type}/${meta.name}`;
|
|
282
|
+
const raw = "frontmatterRaw" in meta ? meta.frontmatterRaw : undefined;
|
|
283
|
+
// Bodies of agents and commands start after `---`, the raw block and the closing `---`.
|
|
284
|
+
const bodyOffset = raw ? raw.split("\n").length + 2 : 0;
|
|
285
|
+
const texts = [];
|
|
286
|
+
if (raw)
|
|
287
|
+
texts.push({ where: origin, text: raw, offset: 1 });
|
|
288
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
289
|
+
const where = meta.type === "skill" && meta.layout === "dir" ? `${origin}${rel}` : origin;
|
|
290
|
+
// A Buffer with no NUL byte is treated as text (e.g. non-allowlisted extensions such
|
|
291
|
+
// as .pem or .bat are still read as Buffer by the importer); a NUL byte marks it binary.
|
|
292
|
+
if (typeof content === "string")
|
|
293
|
+
texts.push({ where, text: content, offset: bodyOffset });
|
|
294
|
+
else if (!content.includes(0))
|
|
295
|
+
texts.push({ where, text: content.toString("utf8"), offset: bodyOffset });
|
|
296
|
+
}
|
|
297
|
+
for (const t of texts) {
|
|
298
|
+
const hit = findSecrets(t.text)[0];
|
|
299
|
+
if (hit)
|
|
300
|
+
return `secret-like value (${hit.kind}) in ${t.where} line ${hit.line + t.offset}`;
|
|
301
|
+
}
|
|
302
|
+
if (meta.type === "mcp") {
|
|
303
|
+
// Walk every field of the server config, not just env/args: headers, url, command, etc.
|
|
304
|
+
// can carry a token too. Entropy stays reserved for env/args; every other field is
|
|
305
|
+
// checked against known patterns only.
|
|
306
|
+
const server = meta.server;
|
|
307
|
+
for (const [key, value] of Object.entries(server)) {
|
|
308
|
+
const hit = secretInServerField(meta.name, key, value, key === "env" || key === "args");
|
|
309
|
+
if (hit)
|
|
310
|
+
return hit;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
/** Recursively scan one MCP server field for a secret-like value, building a dotted/bracketed field path. */
|
|
316
|
+
function secretInServerField(server, field, value, useEntropy) {
|
|
317
|
+
if (Array.isArray(value)) {
|
|
318
|
+
for (const [i, v] of value.entries()) {
|
|
319
|
+
const hit = secretInServerField(server, `${field}[${i}]`, v, useEntropy);
|
|
320
|
+
if (hit)
|
|
321
|
+
return hit;
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
if (value !== null && typeof value === "object") {
|
|
326
|
+
for (const [k, v] of Object.entries(value)) {
|
|
327
|
+
const hit = secretInServerField(server, `${field}.${k}`, v, useEntropy);
|
|
328
|
+
if (hit)
|
|
329
|
+
return hit;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
if (typeof value !== "string" && typeof value !== "number")
|
|
334
|
+
return null;
|
|
335
|
+
const kind = useEntropy ? secretValueKind(String(value)) : (findSecrets(String(value))[0]?.kind ?? null);
|
|
336
|
+
return kind ? `secret-like value (${kind}) in .mcp.json → mcpServers.${server}.${field}` : null;
|
|
337
|
+
}
|
|
338
|
+
function splitList(v) {
|
|
339
|
+
if (!v)
|
|
340
|
+
return [];
|
|
341
|
+
return v
|
|
342
|
+
.split(",")
|
|
343
|
+
.map((s) => s.trim())
|
|
344
|
+
.filter(Boolean);
|
|
345
|
+
}
|
|
346
|
+
async function safeList(dir) {
|
|
347
|
+
return (await exists(dir)) ? (await fs.readdir(dir)).sort() : [];
|
|
348
|
+
}
|
|
349
|
+
async function sniffScm(claudeDir) {
|
|
350
|
+
let text = "";
|
|
351
|
+
for (const f of await safeList(path.join(claudeDir, "commands")))
|
|
352
|
+
text += await fs.readFile(path.join(claudeDir, "commands", f), "utf8");
|
|
353
|
+
if (/az repos pr create|dev\.azure\.com/.test(text))
|
|
354
|
+
return { kind: "azure-devops", prTool: /az repos pr create/.test(text) ? "az" : "rest" };
|
|
355
|
+
if (/\bgh pr create\b/.test(text))
|
|
356
|
+
return { kind: "github", prTool: "gh" };
|
|
357
|
+
return {};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Write an ingredient dir. When an ingredient of the same name already exists in the Forge:
|
|
361
|
+
* identical content → reuse; different content → write a `<name>--<profile>` variant and report it,
|
|
362
|
+
* so the human decides whether to parameterize or keep a client-specific copy.
|
|
363
|
+
*/
|
|
364
|
+
async function writeIngredient(forge, meta, files, profile, report) {
|
|
365
|
+
const secret = secretIn(meta, files);
|
|
366
|
+
if (secret) {
|
|
367
|
+
report.rejected.push({ name: `${meta.type}/${meta.name}`, reason: secret });
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const folder = typeFolder(meta.type);
|
|
371
|
+
let name = meta.name;
|
|
372
|
+
let dir = path.join(forge, "ingredients", folder, name);
|
|
373
|
+
const fingerprint = fingerprintOf(meta, files);
|
|
374
|
+
if (await exists(path.join(dir, "ingredient.yaml"))) {
|
|
375
|
+
const existing = await fingerprintDir(dir);
|
|
376
|
+
if (existing === fingerprint) {
|
|
377
|
+
report.reused.push(`${meta.type}/${name}`);
|
|
378
|
+
return `${meta.type}/${name}`;
|
|
379
|
+
}
|
|
380
|
+
const as = meta.name;
|
|
381
|
+
name = `${meta.name}--${profile}`;
|
|
382
|
+
dir = path.join(forge, "ingredients", folder, name);
|
|
383
|
+
report.variants.push({ name: `${meta.type}/${name}`, reason: `differs from ${meta.type}/${as} already in the Forge` });
|
|
384
|
+
meta = { ...meta, name, as };
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
report.created.push(`${meta.type}/${name}`);
|
|
388
|
+
}
|
|
389
|
+
await fs.mkdir(dir, { recursive: true });
|
|
390
|
+
const yamlMeta = { ...meta };
|
|
391
|
+
if (yamlMeta.targets === "*")
|
|
392
|
+
yamlMeta.targets = "*";
|
|
393
|
+
await fs.writeFile(path.join(dir, "ingredient.yaml"), YAML.stringify(yamlMeta, { lineWidth: 0 }));
|
|
394
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
395
|
+
const abs = path.join(dir, rel);
|
|
396
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
397
|
+
await fs.writeFile(abs, content);
|
|
398
|
+
}
|
|
399
|
+
return `${meta.type}/${name}`;
|
|
400
|
+
}
|
|
401
|
+
function fingerprintOf(meta, files) {
|
|
402
|
+
const m = { ...meta };
|
|
403
|
+
delete m.origin;
|
|
404
|
+
delete m.name;
|
|
405
|
+
delete m.as;
|
|
406
|
+
const parts = [JSON.stringify(m, Object.keys(m).sort())];
|
|
407
|
+
for (const k of Object.keys(files).sort())
|
|
408
|
+
parts.push(k, hashNormalized(files[k]));
|
|
409
|
+
return hashNormalized(parts.join("\0"));
|
|
410
|
+
}
|
|
411
|
+
async function fingerprintDir(dir) {
|
|
412
|
+
const meta = YAML.parse(await fs.readFile(path.join(dir, "ingredient.yaml"), "utf8"));
|
|
413
|
+
const files = {};
|
|
414
|
+
for (const rel of await listFiles(dir))
|
|
415
|
+
if (rel !== "ingredient.yaml")
|
|
416
|
+
files[rel] = await fs.readFile(path.join(dir, rel));
|
|
417
|
+
return fingerprintOf(meta, files);
|
|
418
|
+
}
|
|
419
|
+
async function writeRecipe(dir, recipe, report) {
|
|
420
|
+
const file = path.join(dir, `${recipe.name}.yaml`);
|
|
421
|
+
const clean = JSON.parse(JSON.stringify(recipe)); // drop undefined
|
|
422
|
+
if (await exists(file)) {
|
|
423
|
+
const prev = YAML.parse(await fs.readFile(file, "utf8"));
|
|
424
|
+
clean.ingredients = unique([...(prev.ingredients ?? []), ...clean.ingredients]);
|
|
425
|
+
}
|
|
426
|
+
await fs.writeFile(file, YAML.stringify(clean));
|
|
427
|
+
report.recipes.push(recipe.name);
|
|
428
|
+
}
|
|
429
|
+
//# sourceMappingURL=claude-code.js.map
|