Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9387c53
test(init): pin init's both-flags stdout and the profile question's `…
llima Oct 18, 2026
eba15c6
refactor(promote): the owned-recipe predicate is exported once, named…
llima Oct 18, 2026
aee74c3
feat(init): newProfile builds a new profile's file — the template's r…
llima Oct 18, 2026
026025a
refactor(init): newProfile's targets read as written — the flag, else…
llima Oct 18, 2026
c80f1f6
refactor(forge): assertNoLinkOnPath names the writing command in its …
llima Oct 18, 2026
fe93647
feat(init): newProfileGate — P5–P8 for the one path a new profile cre…
llima Oct 18, 2026
e8e29f9
fix(init): the cache-tree check fails closed on any error but a missi…
llima Oct 18, 2026
fee0959
feat(init): planInit plans a new profile in memory — the copy that ho…
llima Oct 18, 2026
c1c934d
fix(init): P12 is the resolver's own refusal — no warning turned into…
llima Oct 18, 2026
a1e2410
feat(init): the interactive init offers a new profile — the 0) entry,…
llima Oct 18, 2026
76dc7c9
feat(init): the profile write and the second proof from disk, with in…
llima Oct 18, 2026
5ad9839
feat(init): --new-profile and --like create a profile in the Forge di…
llima Oct 18, 2026
58f57eb
refactor(init): --new-profile's N10 goes through checkLocalKeys — one…
llima Oct 18, 2026
1e2b5f4
docs(readme): init --new-profile and --like, a new client's quick sta…
llima Oct 18, 2026
43a0838
fix(init): the second proof uses the typed ref, and any failure of it…
llima Oct 18, 2026
aeb8e6a
fix(init): a new profile's notes come from step 1, craftar.yaml failu…
llima Oct 18, 2026
59095a2
fix(init): the interactive new profile honours typed flags, re-asks a…
llima Oct 18, 2026
e6d448e
fix(init): targetsFrom for a local targets, the gate's comments, the …
llima Oct 18, 2026
ac199d6
fix(init): a new profile's targets read "from the profile" in the con…
llima Oct 19, 2026
b69ece8
fix(init): a new profile's file reads back as the profile proved — na…
llima Oct 19, 2026
27ad68a
fix(init): the new profile's recipes step checks P10 on every exit an…
llima Oct 19, 2026
11a66b1
refactor(init): the late-failure wraps word a raw error through error…
llima Oct 19, 2026
a2fbf50
fix(init): type annotation for tempConfig.recipes (review)
llima Oct 19, 2026
f2e59c7
fix(init): a new profile's name line is YAML-safe too — 123 or null r…
llima Oct 19, 2026
0a4d983
fix(init): the new profile's recipes step re-asks every refusal, N6 a…
llima Oct 19, 2026
0f7e031
chore: update version
llima Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "craftar",
"version": "0.33.0",
"version": "0.34.0",
"description": "Craft, sync and convert AI-coding workspace harnesses across clients and tools.",
"license": "MIT",
"type": "module",
Expand Down
168 changes: 144 additions & 24 deletions src/cli.ts

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions src/core/forge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,10 @@ export async function listFiles(dir: string, rel = ""): Promise<string[]> {

/* ---- the scratch copy a Forge proof runs on, and the link check its writes share (spec 30 `drift promote`, spec 29 `forge promote`) ---- */

const SYMLINK_REFUSAL = (rel: string) =>
`${rel} is a symbolic link in the Forge — promote does not write through a link; replace it with the file and commit`;
const SYMLINK_DIR_REFUSAL = (rel: string) =>
`${rel} is a symbolic link in the Forge — promote does not read a linked recipes, ingredients or profiles directory; replace it with the directory and commit`;
const SYMLINK_REFUSAL = (rel: string, command: string) =>
`${rel} is a symbolic link in the Forge — ${command} does not write through a link; replace it with the file and commit`;
const SYMLINK_DIR_REFUSAL = (rel: string, command: string) =>
`${rel} is a symbolic link in the Forge — ${command} does not read a linked recipes, ingredients or profiles directory; replace it with the directory and commit`;
const SYMLINK_OUT_OR_DANGLING = (rel: string) =>
`${rel} is a symbolic link out of the Forge, or to nothing — promote cannot copy the Forge to prove its edit; fix or remove it`;
const NON_REGULAR_FILE_REFUSAL = (rel: string) =>
Expand Down Expand Up @@ -312,7 +312,7 @@ export async function copyForgeForProof(forgeDir: string, scratchDir: string): P
// A root directory link (ingredients, recipes, profiles) is refused: loadForge follows it
// but the scratch would skip it, so the scratch would not load the same as the real Forge.
if (rel === "" && ROOT_DIRS.has(ent.name)) {
throw new Error(SYMLINK_DIR_REFUSAL(ent.name));
throw new Error(SYMLINK_DIR_REFUSAL(ent.name, "promote"));
}
// Other directory links are skipped entirely — loadForge doesn't traverse them
continue;
Expand Down Expand Up @@ -357,7 +357,7 @@ export async function copyForgeForProof(forgeDir: string, scratchDir: string): P
* Refuse a write through a symbolic link: every path, and every ancestor between it and `forgeDir`, that exists must
* not be one. A path that does not exist yet is fine — it will be created.
*/
export async function assertNoLinkOnPath(forgeDir: string, paths: string[]): Promise<void> {
export async function assertNoLinkOnPath(forgeDir: string, paths: string[], command = "promote"): Promise<void> {
const checked = new Set<string>();
for (const abs of paths) {
// The path and every ancestor up to but not including forgeDir
Expand All @@ -371,9 +371,9 @@ export async function assertNoLinkOnPath(forgeDir: string, paths: string[]): Pro
const rel = path.relative(forgeDir, current).split(path.sep).join("/");
// The directory-specific message for a root directory (ingredients, recipes, profiles)
if (ROOT_DIRS.has(rel)) {
throw new Error(SYMLINK_DIR_REFUSAL(rel));
throw new Error(SYMLINK_DIR_REFUSAL(rel, command));
}
throw new Error(SYMLINK_REFUSAL(rel));
throw new Error(SYMLINK_REFUSAL(rel, command));
}
} catch (e) {
// ENOENT means the path doesn't exist yet (it will be created) — that's fine
Expand Down
Loading
Loading