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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luiz Lima
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Craftar
|
|
2
|
+
|
|
3
|
+
Craft, sync and convert AI-coding workspace harnesses — rules, agents, commands, skills, MCP servers — across every client workspace you maintain and every AI coder each team uses.
|
|
4
|
+
|
|
5
|
+
> Status: **phase 0 prototype**. `import`, `sync`, `status`, `diff`, `explain`, `ls` work end-to-end for the `claude-code` and `kiro` targets and are validated byte-for-byte against a real workspace (see *Oracle*). Everything else in the spec (profiles with IDP/PM-tool integrations, local services, templates, `craftar ui`, `craftar mcp`) is not built yet.
|
|
6
|
+
|
|
7
|
+
## The idea in one paragraph
|
|
8
|
+
|
|
9
|
+
You write the harness **once**, in a central repository called the **Forge**, as small reusable **ingredients** (a rule, an agent, a command, a skill, an MCP server). **Recipes** bundle ingredients (`base`, `stack-backend-api`, `stack-frontend-angular`…). A **profile** describes a client: which recipes, which targets (Claude Code, Kiro, AGENTS.md…), and the parameters that differ per client. A workspace holds a tiny `craftar.yaml` pointing at the Forge and a profile; `craftar sync` generates `.claude/`, `.kiro/`, `AGENTS.md`… from it and records what it wrote in `craftar.lock`. Generated files are never edited by hand again — you edit the Forge and sync everywhere.
|
|
10
|
+
|
|
11
|
+
## Install (from source, for now)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npm run build # → dist/
|
|
16
|
+
node bin/craftar.js --help
|
|
17
|
+
# or during development
|
|
18
|
+
npx tsx src/cli.ts --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Node ≥ 22.
|
|
22
|
+
|
|
23
|
+
## Quick start: bring an existing workspace into a Forge
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. Import the harness of a Claude Code workspace into a (new or existing) Forge
|
|
27
|
+
craftar import --from claude-code \
|
|
28
|
+
--workspace C:/Projects/acme-portal-workspace \
|
|
29
|
+
--forge C:/Projects/forge \
|
|
30
|
+
--profile acme-portal \
|
|
31
|
+
--write-config # writes craftar.yaml into the workspace
|
|
32
|
+
|
|
33
|
+
# 2. See what sync would do. On a freshly imported workspace everything is "adopt":
|
|
34
|
+
# the files already on disk are exactly what the Forge produces, so they just become managed.
|
|
35
|
+
craftar status --workspace C:/Projects/acme-portal-workspace
|
|
36
|
+
|
|
37
|
+
# 3. Generate + lock
|
|
38
|
+
craftar sync --workspace C:/Projects/acme-portal-workspace
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
From then on, change a rule in `forge/ingredients/rules/<name>/rule.md`, run `craftar sync` in each workspace (or `craftar sync --check` in CI) and every target is regenerated.
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
| Command | What it does |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `craftar import --from claude-code --forge <dir> --profile <name> [--workspace .] [--write-config]` | Reads `.claude/{rules,agents,commands,skills,scripts,hooks}`, `.mcp.json` and, when present, `.kiro/steering` (for inclusion modes and hand-written steering). Creates ingredients, recipes (`base`, one `stack-*` per scoped rule, `<profile>-steering`) and a profile. Identical ingredients already in the Forge are reused; differing ones become `<name>--<profile>` variants that emit under the original name, so the workspace still round-trips while you decide what to unify. Ingredients holding a secret-like value in UTF-8 text (tokens, private keys, high-entropy MCP `env`/`args` values) are rejected and listed by location — the value is never printed. UTF-16 files are not scanned yet. |
|
|
48
|
+
| `craftar status` | Classifies every file the Forge would produce: `new`, `update`, `unchanged`, `adopt`, `drift`, `collision`, `orphan`, `orphan-drift`. `--json` for tooling. |
|
|
49
|
+
| `craftar sync` | Writes the plan and `craftar.lock`. `--dry-run` shows without writing. `--check` exits 1 when anything is out of sync (CI). `--overwrite-drift` regenerates hand-edited files (explicit, never default). |
|
|
50
|
+
| `craftar diff [path]` | Line diff between disk and what the Forge would generate. |
|
|
51
|
+
| `craftar explain <path>` | Which ingredient, recipe chain, target and origin produced a file. |
|
|
52
|
+
| `craftar ls` | Recipes and ingredients resolved for this workspace. |
|
|
53
|
+
|
|
54
|
+
All commands take `--workspace <dir>` (default: current directory).
|
|
55
|
+
|
|
56
|
+
## Forge layout
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
forge/
|
|
60
|
+
├── craftar.forge.yaml
|
|
61
|
+
├── ingredients/
|
|
62
|
+
│ ├── rules/<name>/{ingredient.yaml, rule.md}
|
|
63
|
+
│ ├── agents/<name>/{ingredient.yaml, agent.md}
|
|
64
|
+
│ ├── commands/<name>/{ingredient.yaml, command.md}
|
|
65
|
+
│ ├── skills/<name>/{ingredient.yaml, SKILL.md, …}
|
|
66
|
+
│ ├── scripts/<name>/{ingredient.yaml, <file>}
|
|
67
|
+
│ ├── hooks/<name>/{ingredient.yaml, <file>}
|
|
68
|
+
│ ├── mcp/<name>/ingredient.yaml # server definition
|
|
69
|
+
│ └── steerings/<name>/{ingredient.yaml, steering.md} # Kiro-only, no Claude counterpart
|
|
70
|
+
├── recipes/<name>.yaml
|
|
71
|
+
└── profiles/<name>/profile.yaml
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`ingredient.yaml` (rule):
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
type: rule
|
|
78
|
+
name: frontend-angular
|
|
79
|
+
inclusion: fileMatch # always | fileMatch | manual | auto (Kiro steering modes)
|
|
80
|
+
fileMatchPattern: projects/acme-portal-front/**
|
|
81
|
+
targets: "*" # or [claude-code, kiro]
|
|
82
|
+
tags: []
|
|
83
|
+
origin: { workspace: acme-portal-workspace, path: .claude/rules/frontend-angular.md }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Recipe:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
name: stack-frontend-angular
|
|
90
|
+
extends: []
|
|
91
|
+
slot: frontend # optional: two recipes on one slot cannot coexist
|
|
92
|
+
ingredients: [rule/frontend-angular, agent/frontend-reviewer]
|
|
93
|
+
params: { angularVersion: { default: "20" } }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Workspace `craftar.yaml`:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
forge: ../forge # path today; git URL later
|
|
100
|
+
profile: acme-portal
|
|
101
|
+
targets: [claude-code, kiro] # optional, overrides the profile
|
|
102
|
+
recipes: { add: [], remove: [] }
|
|
103
|
+
overrides:
|
|
104
|
+
params: { }
|
|
105
|
+
ingredients: { disable: [] }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Layer precedence, weakest → strongest: recipe defaults → profile → `craftar.yaml` → `craftar.local.yaml` (personal, git-ignored). Bodies may use `{{param}}` placeholders; a placeholder with no value in any layer is left untouched and reported as a warning by `status` and `sync` (Angular's `{{ 'X' | localize }}` does not look like a placeholder and passes silently). Between layers, objects merge key by key while arrays and scalars from the stronger layer replace the weaker one — `targets: [kiro]` in `craftar.local.yaml` means exactly `[kiro]`. Omit a key in `craftar.local.yaml` to inherit it — an empty list there means empty.
|
|
109
|
+
|
|
110
|
+
## Targets
|
|
111
|
+
|
|
112
|
+
**claude-code** — emits `.claude/rules|agents|commands|skills|scripts|hooks` and `.mcp.json` verbatim from the Forge (frontmatter kept byte-for-byte). Existing files keep their line endings and BOM; new files are LF without a BOM.
|
|
113
|
+
|
|
114
|
+
**kiro** — reproduces, then extends, the hand-written `sync-steering.ps1` script it replaces:
|
|
115
|
+
steering = `inclusion` frontmatter + `GENERATED` banner + rule body, with `.claude/rules/` rewritten to `.kiro/steering/`, UTF-8 without BOM, CRLF. On top of what the script did, it also generates `.kiro/agents/*.json` (tools mapped to Kiro names, `resources` bound to the agent's stack rule + `repo-discovery`, or `**/*.md` for generic agents), `.kiro/steering/commands/*.md`, `.kiro/skills/*/SKILL.md` and `.kiro/settings/mcp.json`. The banner text is a parameter (`kiro.banner`) so existing workspaces can adopt without a rewrite. Scripts and hooks have no Kiro equivalent: they are skipped with a warning.
|
|
116
|
+
|
|
117
|
+
**agents-md** — one `AGENTS.md` with the always-on rules concatenated and the scoped rules listed, for tools that read the open standard (Codex, Cursor, Warp, Copilot, Kimi…).
|
|
118
|
+
|
|
119
|
+
Conversion of hooks/subagents to other tools is out of scope here: the plan is to delegate that to [rulesync](https://github.com/dyoshikawa/rulesync) rather than reimplement it.
|
|
120
|
+
|
|
121
|
+
## Design rules learned from the previous generator
|
|
122
|
+
|
|
123
|
+
These come straight from workspaces that removed an earlier generator, and the code enforces them:
|
|
124
|
+
|
|
125
|
+
1. **Only files in `craftar.lock` are ever written or removed.** A file that exists but was never generated is a *collision*: reported, never touched. A managed file that was hand-edited is *drift*: reported, never overwritten unless `--overwrite-drift`.
|
|
126
|
+
2. **Hashes are EOL- and BOM-normalized.** A CRLF checkout on Windows does not read as a hand edit. (`sha256` over LF/no-BOM text.)
|
|
127
|
+
3. **Orphans are removed.** When an ingredient leaves a recipe, its generated files disappear in every target — no stale steering loading into Kiro sessions.
|
|
128
|
+
4. **No hand-kept mirrors.** Kiro agents, commands and skills are generated from the same ingredients as their Claude counterparts.
|
|
129
|
+
5. **Reuse, don't clobber.** Importing a second workspace into the same Forge reuses identical ingredients and creates explicit `--<profile>` variants for the ones that differ, listing them so a human unifies or parameterizes.
|
|
130
|
+
|
|
131
|
+
## Tests
|
|
132
|
+
|
|
133
|
+
`npm test` runs two layers that need nothing outside the repository, on any OS:
|
|
134
|
+
|
|
135
|
+
- **Unit tests** build a Forge and a workspace in a temp dir (`test/helpers/forge.ts`) and cover
|
|
136
|
+
resolution, every file state, apply, each emitter, the importer, the secret guard and the CLI
|
|
137
|
+
exit codes.
|
|
138
|
+
- **Golden workspaces** — `test/golden/acme-portal` and `test/golden/acme-web`, synthetic — run
|
|
139
|
+
the oracle script end to end: import → every file `adopt`, sync byte-identical (a CRLF file and a
|
|
140
|
+
BOM file included), second sync a no-op, Forge edits, drift, orphans, variants.
|
|
141
|
+
`test/golden/**` is `-text` in `.gitattributes`, so line endings survive checkout. The golden
|
|
142
|
+
`.kiro/` is a snapshot of the emitter's output: after an intended emitter change, regenerate it
|
|
143
|
+
with `npx tsx test/helpers/regen-golden.ts` and review the diff.
|
|
144
|
+
|
|
145
|
+
CI runs typecheck, build and tests on Linux and Windows with Node 22 and 24. The oracle below is
|
|
146
|
+
skipped there — a green CI is not evidence against a real workspace.
|
|
147
|
+
|
|
148
|
+
## Oracle
|
|
149
|
+
|
|
150
|
+
`test/oracle.test.ts` imports a real client workspace (a private fixture, never committed) and asserts that `craftar sync` reproduces its 13 generated steering files, 5 hand-written steering files, 3 Kiro agent JSONs and every `.claude/*` file **byte for byte**, that a second sync is a no-op, that a Forge edit flows to both targets, that a hand edit becomes drift and survives, that orphans are removed, and that unrelated files are left alone.
|
|
151
|
+
|
|
152
|
+
The fixture is not in this repository. Point the suite at a workspace of your own:
|
|
153
|
+
set `CRAFTAR_ORACLE_FIXTURE` to its path, or drop a single workspace directory into
|
|
154
|
+
`fixtures/`. Without it the oracle suite is skipped and the remaining tests still run.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm test
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Copy a workspace with `.claude/` and `.kiro/` into `fixtures/` (keep `local-stack/` and `projects/` out).
|
|
161
|
+
|
|
162
|
+
## Roadmap (from the functional spec)
|
|
163
|
+
|
|
164
|
+
Phase 0 (this): schema, import, sync/status/diff/explain, claude-code + kiro + agents-md targets, lock, drift, orphans.
|
|
165
|
+
Next: `craftar init` from a profile; profile-driven integrations (PM tool → MCP, IDP → MCP); `service` ingredients with compose fragments (`craftar services up`); `dotnet new` template registration; rulesync bridge for Codex/Kimi/Cursor specifics; remote Forge (git URL + ref); `craftar docs validate`; GitHub Action and Azure Pipelines task around `sync --check`; `craftar ui`; `craftar mcp` + Claude Code / Agent Plugin packaging.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
package/bin/craftar.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promises as fs } from "node:fs";
|
|
5
|
+
import { importClaudeCode } from "./importers/claude-code.js";
|
|
6
|
+
import { loadWorkspace, plan, readLock, status, apply } from "./core/sync.js";
|
|
7
|
+
import { hashNormalized, toLf, stripBom } from "./core/text.js";
|
|
8
|
+
process.stdout.on("error", (e) => { if (e.code === "EPIPE")
|
|
9
|
+
process.exit(0); });
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.0.3");
|
|
12
|
+
/* ---------------------------------------------------------------- import */
|
|
13
|
+
program
|
|
14
|
+
.command("import")
|
|
15
|
+
.description("Import an existing workspace harness into a Forge (creates ingredients, recipes and a profile)")
|
|
16
|
+
.requiredOption("--from <tool>", "source tool: claude-code")
|
|
17
|
+
.requiredOption("--forge <dir>", "Forge directory (created if missing)")
|
|
18
|
+
.requiredOption("--profile <name>", "client profile name to create")
|
|
19
|
+
.option("--workspace <dir>", "workspace to import", ".")
|
|
20
|
+
.option("--write-config", "write craftar.yaml into the workspace", false)
|
|
21
|
+
.action(async (o) => {
|
|
22
|
+
if (o.from !== "claude-code")
|
|
23
|
+
fail(`unsupported source "${o.from}" (only claude-code for now)`);
|
|
24
|
+
const r = await importClaudeCode({ workspaceRoot: o.workspace, forgeRoot: o.forge, profileName: o.profile, writeWorkspaceConfig: o.writeConfig });
|
|
25
|
+
console.log(pc.bold(`Imported ${path.resolve(o.workspace)} → ${path.resolve(o.forge)} as profile "${r.profile}"`));
|
|
26
|
+
console.log(` ${pc.green(String(r.created.length))} created, ${pc.cyan(String(r.reused.length))} reused, ${pc.yellow(String(r.variants.length))} variants, ${pc.red(String(r.rejected.length))} rejected`);
|
|
27
|
+
for (const v of r.variants)
|
|
28
|
+
console.log(` ${pc.yellow("variant")} ${v.name} — ${v.reason}`);
|
|
29
|
+
for (const x of r.rejected)
|
|
30
|
+
console.log(` ${pc.red("rejected")} ${x.name} — ${x.reason}`);
|
|
31
|
+
console.log(` recipes: ${r.recipes.join(", ")}`);
|
|
32
|
+
for (const w of r.warnings)
|
|
33
|
+
console.log(` ${pc.yellow("warn")} ${w}`);
|
|
34
|
+
});
|
|
35
|
+
/* ---------------------------------------------------------------- status */
|
|
36
|
+
program
|
|
37
|
+
.command("status")
|
|
38
|
+
.description("Show what sync would do: new, update, drift, orphan, collision")
|
|
39
|
+
.option("--workspace <dir>", "workspace root", ".")
|
|
40
|
+
.option("--json", "machine-readable output", false)
|
|
41
|
+
.action(async (o) => {
|
|
42
|
+
const ws = await loadWorkspace(o.workspace);
|
|
43
|
+
const p = await plan(ws);
|
|
44
|
+
const st = await status(ws, p, await readLock(ws.root));
|
|
45
|
+
if (o.json)
|
|
46
|
+
return console.log(JSON.stringify({ statuses: st.map(({ planned, ...s }) => s), warnings: p.warnings }, null, 2));
|
|
47
|
+
printStatus(st, p.warnings, ws.config.profile, p.resolution.recipes);
|
|
48
|
+
});
|
|
49
|
+
/* ---------------------------------------------------------------- sync */
|
|
50
|
+
program
|
|
51
|
+
.command("sync")
|
|
52
|
+
.description("Generate the harness for every target from the Forge and update craftar.lock")
|
|
53
|
+
.option("--workspace <dir>", "workspace root", ".")
|
|
54
|
+
.option("--check", "exit 1 when the workspace is out of date or has drift (CI mode)", false)
|
|
55
|
+
.option("--dry-run", "show the plan, write nothing", false)
|
|
56
|
+
.option("--overwrite-drift", "regenerate files that were hand-edited (their edits are lost)", false)
|
|
57
|
+
.action(async (o) => {
|
|
58
|
+
const ws = await loadWorkspace(o.workspace);
|
|
59
|
+
const p = await plan(ws);
|
|
60
|
+
const st = await status(ws, p, await readLock(ws.root));
|
|
61
|
+
if (o.check) {
|
|
62
|
+
const bad = st.filter((s) => !["unchanged", "adopt"].includes(s.state));
|
|
63
|
+
printStatus(st, p.warnings, ws.config.profile, p.resolution.recipes, true);
|
|
64
|
+
if (bad.length) {
|
|
65
|
+
console.log(pc.red(`\n${bad.length} file(s) out of sync`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
console.log(pc.green("\nworkspace in sync"));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const r = await apply(ws, p, st, { dryRun: o.dryRun, overwriteDrift: o.overwriteDrift });
|
|
72
|
+
const verb = o.dryRun ? "would write" : "wrote";
|
|
73
|
+
console.log(pc.bold(`craftar sync — profile ${ws.config.profile} · recipes ${p.resolution.recipes.join(" → ")} · targets ${p.resolution.targets.join(", ")}`));
|
|
74
|
+
console.log(` ${verb} ${pc.green(String(r.written.length))}, removed ${pc.magenta(String(r.removed.length))} orphan(s), skipped ${pc.yellow(String(r.skipped.length))}`);
|
|
75
|
+
for (const f of r.written)
|
|
76
|
+
console.log(` ${pc.green("+")} ${f}`);
|
|
77
|
+
for (const f of r.removed)
|
|
78
|
+
console.log(` ${pc.magenta("-")} ${f} (orphan: no longer produced by the Forge)`);
|
|
79
|
+
for (const s of r.skipped)
|
|
80
|
+
console.log(` ${pc.yellow("!")} ${s.path} ${explainSkip(s)}`);
|
|
81
|
+
for (const w of p.warnings)
|
|
82
|
+
console.log(` ${pc.yellow("warn")} ${w}`);
|
|
83
|
+
});
|
|
84
|
+
/* ---------------------------------------------------------------- diff */
|
|
85
|
+
program
|
|
86
|
+
.command("diff")
|
|
87
|
+
.description("Unified diff between the files on disk and what the Forge would generate")
|
|
88
|
+
.option("--workspace <dir>", "workspace root", ".")
|
|
89
|
+
.argument("[path]", "limit to one file")
|
|
90
|
+
.action(async (only, o) => {
|
|
91
|
+
const ws = await loadWorkspace(o.workspace);
|
|
92
|
+
const p = await plan(ws);
|
|
93
|
+
const st = await status(ws, p, await readLock(ws.root));
|
|
94
|
+
let shown = 0;
|
|
95
|
+
for (const s of st) {
|
|
96
|
+
if (only && s.path !== only)
|
|
97
|
+
continue;
|
|
98
|
+
if (!["update", "drift", "collision", "new"].includes(s.state))
|
|
99
|
+
continue;
|
|
100
|
+
shown++;
|
|
101
|
+
const disk = await readText(path.join(ws.root, s.path));
|
|
102
|
+
const next = s.planned ? toLf(stripBom(s.planned.content.toString("utf8"))) : "";
|
|
103
|
+
console.log(pc.bold(`--- ${s.path} (disk, ${s.state})`));
|
|
104
|
+
console.log(pc.bold(`+++ ${s.path} (forge)`));
|
|
105
|
+
console.log(simpleDiff(disk ?? "", next));
|
|
106
|
+
}
|
|
107
|
+
if (!shown)
|
|
108
|
+
console.log(pc.green("no differences"));
|
|
109
|
+
});
|
|
110
|
+
/* ---------------------------------------------------------------- explain */
|
|
111
|
+
program
|
|
112
|
+
.command("explain")
|
|
113
|
+
.description("Why does this file exist? Which ingredient, recipe chain and target produced it")
|
|
114
|
+
.argument("<path>", "workspace-relative path of a generated file")
|
|
115
|
+
.option("--workspace <dir>", "workspace root", ".")
|
|
116
|
+
.action(async (file, o) => {
|
|
117
|
+
const ws = await loadWorkspace(o.workspace);
|
|
118
|
+
const p = await plan(ws);
|
|
119
|
+
const f = p.files.find((x) => x.path === file.replace(/\\/g, "/"));
|
|
120
|
+
if (!f)
|
|
121
|
+
fail(`${file} is not produced by the Forge for profile ${ws.config.profile}`);
|
|
122
|
+
const ing = p.resolution.ingredients.find((i) => i.ref === f.ingredient);
|
|
123
|
+
console.log(pc.bold(f.path));
|
|
124
|
+
console.log(` target ${f.target}`);
|
|
125
|
+
console.log(` ingredient ${f.ingredient}${ing?.meta.origin ? pc.dim(` (imported from ${ing.meta.origin.workspace}:${ing.meta.origin.path})`) : ""}`);
|
|
126
|
+
if (ing)
|
|
127
|
+
console.log(` via recipes ${ing.via.join(" → ")}`);
|
|
128
|
+
console.log(` profile ${ws.config.profile} (recipes: ${p.resolution.recipes.join(", ")})`);
|
|
129
|
+
console.log(` hash ${hashNormalized(f.content)}`);
|
|
130
|
+
});
|
|
131
|
+
/* ---------------------------------------------------------------- forge ls */
|
|
132
|
+
program
|
|
133
|
+
.command("ls")
|
|
134
|
+
.description("List recipes and ingredients resolved for this workspace")
|
|
135
|
+
.option("--workspace <dir>", "workspace root", ".")
|
|
136
|
+
.action(async (o) => {
|
|
137
|
+
const ws = await loadWorkspace(o.workspace);
|
|
138
|
+
const p = await plan(ws);
|
|
139
|
+
console.log(pc.bold(`Forge ${ws.forge.manifest.name} @ ${ws.forge.commit?.slice(0, 8) ?? "no git"} · profile ${ws.config.profile}`));
|
|
140
|
+
for (const r of p.resolution.recipes) {
|
|
141
|
+
const rec = ws.forge.recipes.get(r);
|
|
142
|
+
console.log(`\n${pc.cyan(r)}${rec.slot ? pc.dim(` [slot ${rec.slot}]`) : ""}${rec.description ? pc.dim(" — " + rec.description) : ""}`);
|
|
143
|
+
for (const ref of rec.ingredients) {
|
|
144
|
+
const disabled = p.resolution.disabled.includes(ref);
|
|
145
|
+
console.log(` ${disabled ? pc.strikethrough(ref) : ref}${disabled ? pc.dim(" (disabled by workspace)") : ""}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
console.log(`\n${p.files.length} files across targets ${p.resolution.targets.join(", ")}`);
|
|
149
|
+
});
|
|
150
|
+
program.parseAsync().catch((e) => fail(e instanceof Error ? e.message : String(e)));
|
|
151
|
+
/* ---------------------------------------------------------------- helpers */
|
|
152
|
+
function fail(msg) {
|
|
153
|
+
console.error(pc.red("error: ") + msg);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
function printStatus(st, warnings, profile, recipes, compact = false) {
|
|
157
|
+
const counts = {};
|
|
158
|
+
for (const s of st)
|
|
159
|
+
counts[s.state] = (counts[s.state] ?? 0) + 1;
|
|
160
|
+
console.log(pc.bold(`craftar status — profile ${profile} · recipes ${recipes.join(" → ")}`));
|
|
161
|
+
console.log(" " +
|
|
162
|
+
Object.entries(counts)
|
|
163
|
+
.map(([k, v]) => `${color(k)(k)} ${v}`)
|
|
164
|
+
.join(" "));
|
|
165
|
+
for (const s of st) {
|
|
166
|
+
if (compact && s.state === "unchanged")
|
|
167
|
+
continue;
|
|
168
|
+
console.log(` ${color(s.state)(s.state.padEnd(13))} ${s.path}${s.state === "unchanged" ? "" : " " + pc.dim(s.ingredient ?? "")}`);
|
|
169
|
+
}
|
|
170
|
+
for (const w of warnings)
|
|
171
|
+
console.log(` ${pc.yellow("warn")} ${w}`);
|
|
172
|
+
}
|
|
173
|
+
function color(state) {
|
|
174
|
+
switch (state) {
|
|
175
|
+
case "new":
|
|
176
|
+
case "update":
|
|
177
|
+
return pc.green;
|
|
178
|
+
case "drift":
|
|
179
|
+
case "orphan-drift":
|
|
180
|
+
return pc.red;
|
|
181
|
+
case "collision":
|
|
182
|
+
return pc.yellow;
|
|
183
|
+
case "orphan":
|
|
184
|
+
return pc.magenta;
|
|
185
|
+
case "adopt":
|
|
186
|
+
return pc.cyan;
|
|
187
|
+
default:
|
|
188
|
+
return pc.dim;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function explainSkip(s) {
|
|
192
|
+
switch (s.state) {
|
|
193
|
+
case "drift":
|
|
194
|
+
return "hand-edited since last sync — run `craftar diff` and either `--overwrite-drift` or promote the change to the Forge";
|
|
195
|
+
case "collision":
|
|
196
|
+
return "exists but was never generated by craftar and differs from the Forge — rename it or import it";
|
|
197
|
+
case "orphan-drift":
|
|
198
|
+
return "no longer produced by the Forge but hand-edited — kept; delete it yourself if unwanted";
|
|
199
|
+
default:
|
|
200
|
+
return "";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function readText(p) {
|
|
204
|
+
try {
|
|
205
|
+
return toLf(stripBom(await fs.readFile(p, "utf8")));
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Small LCS-based line diff; good enough for markdown-sized files. */
|
|
212
|
+
function simpleDiff(a, b) {
|
|
213
|
+
const A = a.split("\n"), B = b.split("\n");
|
|
214
|
+
const n = A.length, m = B.length;
|
|
215
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
216
|
+
for (let i = n - 1; i >= 0; i--)
|
|
217
|
+
for (let j = m - 1; j >= 0; j--)
|
|
218
|
+
dp[i][j] = A[i] === B[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
219
|
+
const out = [];
|
|
220
|
+
let i = 0, j = 0;
|
|
221
|
+
while (i < n && j < m) {
|
|
222
|
+
if (A[i] === B[j]) {
|
|
223
|
+
out.push(pc.dim(" " + A[i]));
|
|
224
|
+
i++;
|
|
225
|
+
j++;
|
|
226
|
+
}
|
|
227
|
+
else if (dp[i + 1][j] >= dp[i][j + 1])
|
|
228
|
+
out.push(pc.red("- " + A[i++]));
|
|
229
|
+
else
|
|
230
|
+
out.push(pc.green("+ " + B[j++]));
|
|
231
|
+
}
|
|
232
|
+
while (i < n)
|
|
233
|
+
out.push(pc.red("- " + A[i++]));
|
|
234
|
+
while (j < m)
|
|
235
|
+
out.push(pc.green("+ " + B[j++]));
|
|
236
|
+
// collapse long unchanged runs
|
|
237
|
+
const res = [];
|
|
238
|
+
let run = [];
|
|
239
|
+
const flush = () => { if (run.length > 6)
|
|
240
|
+
res.push(...run.slice(0, 3), pc.dim(` … ${run.length - 6} unchanged lines …`), ...run.slice(-3));
|
|
241
|
+
else
|
|
242
|
+
res.push(...run); run = []; };
|
|
243
|
+
for (const l of out) {
|
|
244
|
+
if (l.startsWith(pc.dim(" ")))
|
|
245
|
+
run.push(l);
|
|
246
|
+
else {
|
|
247
|
+
flush();
|
|
248
|
+
res.push(l);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
flush();
|
|
252
|
+
return res.join("\n");
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { ForgeManifestSchema, IngredientSchema, ProfileSchema, RecipeSchema, } from "../schema/index.js";
|
|
7
|
+
const execFileP = promisify(execFile);
|
|
8
|
+
export const FORGE_MANIFEST = "craftar.forge.yaml";
|
|
9
|
+
export async function exists(p) {
|
|
10
|
+
try {
|
|
11
|
+
await fs.access(p);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function readYaml(file, schema) {
|
|
19
|
+
const text = await fs.readFile(file, "utf8");
|
|
20
|
+
try {
|
|
21
|
+
return schema.parse(YAML.parse(text) ?? {});
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
throw new Error(`invalid ${path.relative(process.cwd(), file)}: ${e.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function loadForge(root) {
|
|
28
|
+
root = path.resolve(root);
|
|
29
|
+
const manifestPath = path.join(root, FORGE_MANIFEST);
|
|
30
|
+
if (!(await exists(manifestPath)))
|
|
31
|
+
throw new Error(`not a Forge: ${manifestPath} not found`);
|
|
32
|
+
const manifest = await readYaml(manifestPath, ForgeManifestSchema);
|
|
33
|
+
const ingredients = new Map();
|
|
34
|
+
const ingRoot = path.join(root, "ingredients");
|
|
35
|
+
if (await exists(ingRoot)) {
|
|
36
|
+
for (const typeDir of await fs.readdir(ingRoot, { withFileTypes: true })) {
|
|
37
|
+
if (!typeDir.isDirectory())
|
|
38
|
+
continue;
|
|
39
|
+
const typeName = typeDir.name; // plural folder: rules, agents, …
|
|
40
|
+
for (const ingDir of await fs.readdir(path.join(ingRoot, typeName), { withFileTypes: true })) {
|
|
41
|
+
if (!ingDir.isDirectory())
|
|
42
|
+
continue;
|
|
43
|
+
const dir = path.join(ingRoot, typeName, ingDir.name);
|
|
44
|
+
const metaFile = path.join(dir, "ingredient.yaml");
|
|
45
|
+
if (!(await exists(metaFile)))
|
|
46
|
+
continue;
|
|
47
|
+
const meta = await readYaml(metaFile, IngredientSchema);
|
|
48
|
+
const ref = `${meta.type}/${meta.name}`;
|
|
49
|
+
if (ingredients.has(ref))
|
|
50
|
+
throw new Error(`duplicate ingredient ${ref} (${dir})`);
|
|
51
|
+
ingredients.set(ref, { ref, dir, meta });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const recipes = new Map();
|
|
56
|
+
const recRoot = path.join(root, "recipes");
|
|
57
|
+
if (await exists(recRoot)) {
|
|
58
|
+
for (const f of await fs.readdir(recRoot)) {
|
|
59
|
+
if (!/\.ya?ml$/.test(f))
|
|
60
|
+
continue;
|
|
61
|
+
const r = await readYaml(path.join(recRoot, f), RecipeSchema);
|
|
62
|
+
recipes.set(r.name, r);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const profiles = new Map();
|
|
66
|
+
const profRoot = path.join(root, "profiles");
|
|
67
|
+
if (await exists(profRoot)) {
|
|
68
|
+
for (const d of await fs.readdir(profRoot, { withFileTypes: true })) {
|
|
69
|
+
if (!d.isDirectory())
|
|
70
|
+
continue;
|
|
71
|
+
const f = path.join(profRoot, d.name, "profile.yaml");
|
|
72
|
+
if (!(await exists(f)))
|
|
73
|
+
continue;
|
|
74
|
+
const p = await readYaml(f, ProfileSchema);
|
|
75
|
+
profiles.set(p.name, p);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { root, manifest, ingredients, recipes, profiles, commit: await gitHead(root) };
|
|
79
|
+
}
|
|
80
|
+
async function gitHead(dir) {
|
|
81
|
+
try {
|
|
82
|
+
const { stdout } = await execFileP("git", ["-C", dir, "rev-parse", "HEAD"]);
|
|
83
|
+
return stdout.trim() || null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Plural folder name for an ingredient type (rules/, agents/, …). */
|
|
90
|
+
export function typeFolder(type) {
|
|
91
|
+
return type === "mcp" ? "mcp" : `${type}s`;
|
|
92
|
+
}
|
|
93
|
+
export async function readIngredientText(ing, file) {
|
|
94
|
+
return fs.readFile(path.join(ing.dir, file), "utf8");
|
|
95
|
+
}
|
|
96
|
+
/** Recursively list files under a directory, relative to it, sorted. */
|
|
97
|
+
export async function listFiles(dir, rel = "") {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const e of await fs.readdir(path.join(dir, rel), { withFileTypes: true })) {
|
|
100
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
101
|
+
if (e.isDirectory())
|
|
102
|
+
out.push(...(await listFiles(dir, r)));
|
|
103
|
+
else
|
|
104
|
+
out.push(r);
|
|
105
|
+
}
|
|
106
|
+
return out.sort();
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=forge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forge.js","sourceRoot":"","sources":["../../src/core/forge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import { toLf, stripBom } from "./text.js";
|
|
3
|
+
const FENCE = /^---\n([\s\S]*?)\n---\n?/;
|
|
4
|
+
/** Parse a YAML frontmatter block at the very start of a markdown document. */
|
|
5
|
+
export function parseFrontmatter(input, opts = {}) {
|
|
6
|
+
const text = toLf(stripBom(input));
|
|
7
|
+
const m = FENCE.exec(text);
|
|
8
|
+
if (!m)
|
|
9
|
+
return { data: {}, body: text, raw: null };
|
|
10
|
+
let data;
|
|
11
|
+
if (opts.loose)
|
|
12
|
+
return { data: looseParse(m[1]), body: text.slice(m[0].length), raw: m[1] };
|
|
13
|
+
try {
|
|
14
|
+
data = (YAML.parse(m[1]) ?? {});
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Claude Code frontmatter is often "loose" (unquoted colons, commas). Fall back to line parsing.
|
|
18
|
+
data = looseParse(m[1]);
|
|
19
|
+
}
|
|
20
|
+
return { data, body: text.slice(m[0].length), raw: m[1] };
|
|
21
|
+
}
|
|
22
|
+
/** Line-based `key: value` parser for frontmatter YAML would reject (e.g. "tools: Read, Grep"). */
|
|
23
|
+
export function looseParse(raw) {
|
|
24
|
+
const out = {};
|
|
25
|
+
let lastKey = null;
|
|
26
|
+
for (const line of raw.split("\n")) {
|
|
27
|
+
const m = /^([A-Za-z0-9_-]+):\s?(.*)$/.exec(line);
|
|
28
|
+
if (m) {
|
|
29
|
+
lastKey = m[1];
|
|
30
|
+
out[lastKey] = m[2];
|
|
31
|
+
}
|
|
32
|
+
else if (lastKey && /^\s+\S/.test(line)) {
|
|
33
|
+
out[lastKey] = (out[lastKey] + "\n" + line.trim()).trim();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Serialize frontmatter + body. Values are emitted verbatim (no quoting) to round-trip Claude Code files byte-for-byte. */
|
|
39
|
+
export function serializeFrontmatter(data, body, opts = {}) {
|
|
40
|
+
if (opts.raw != null)
|
|
41
|
+
return `---\n${opts.raw}\n---\n${body}`;
|
|
42
|
+
const lines = Object.entries(data)
|
|
43
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
44
|
+
.map(([k, v]) => `${k}: ${formatValue(v)}`);
|
|
45
|
+
return `---\n${lines.join("\n")}\n---\n${body}`;
|
|
46
|
+
}
|
|
47
|
+
function formatValue(v) {
|
|
48
|
+
if (Array.isArray(v))
|
|
49
|
+
return v.join(", ");
|
|
50
|
+
if (typeof v === "object")
|
|
51
|
+
return JSON.stringify(v);
|
|
52
|
+
return String(v);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=frontmatter.js.map
|