craftar 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,402 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { exists } from "./forge.js";
6
+ import { hashNormalized } from "./text.js";
7
+ /* ------------------------------------------------------------------ */
8
+ /* What counts as a URL, and credentials (spec 13 §6.1, §4.3) */
9
+ /* ------------------------------------------------------------------ */
10
+ const SCHEME_URL = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
11
+ /** The SCP-like form `<user>@<host>:<path>`; without a user, `host:path` stays a path (a drive letter looks the same). */
12
+ const SCP_URL = /^[^\s/\\:@]+@[^\s/\\:]+:/;
13
+ /** A `forge:` value is a git URL (a remote Forge) or a path resolved from the workspace root. */
14
+ export function classifyForge(value) {
15
+ const v = value.trim();
16
+ return SCHEME_URL.test(v) || SCP_URL.test(v) ? "url" : "path";
17
+ }
18
+ /**
19
+ * True when a `forge:` value carries a credential: a user or password in an `http(s)` URL, a password
20
+ * in any other scheme, or a `<x>:<y>@` before the first `/` or `\` whatever the value reads as — so a
21
+ * password never reaches a message that prints the value (spec 13 §6.1, §14 item 11).
22
+ */
23
+ export function credentialFault(raw) {
24
+ const value = raw.trim();
25
+ const head = value.split(/[/\\]/, 1)[0];
26
+ if (/^[^@]*:[^@]*@/.test(head) && !SCHEME_URL.test(value))
27
+ return true;
28
+ const scheme = SCHEME_URL.exec(value);
29
+ if (!scheme)
30
+ return false;
31
+ // Read the authority as written, never through a parser: a spelling WHATWG URL rejects
32
+ // (`host:badport`) must not slip through and be printed later. It runs to the first `/`, as git
33
+ // reads an ssh URL — a `?` or `#` in a password stays inside it. Fail closed.
34
+ const rest = value.slice(scheme[0].length);
35
+ const authority = rest.slice(0, rest.search(/\/|$/));
36
+ const at = authority.lastIndexOf("@");
37
+ if (at === -1)
38
+ return false;
39
+ const userinfo = authority.slice(0, at);
40
+ if (/^https?:\/\/$/i.test(scheme[0]))
41
+ return userinfo !== "";
42
+ return userinfo.includes(":");
43
+ }
44
+ /* ------------------------------------------------------------------ */
45
+ /* The cache key (spec 13 §6.3) */
46
+ /* ------------------------------------------------------------------ */
47
+ /** Scheme and host lowercased; a trailing `/`, then `.git`, dropped. */
48
+ function normalizeUrl(url) {
49
+ let u = url.trim();
50
+ const scheme = SCHEME_URL.exec(u);
51
+ if (scheme) {
52
+ const rest = u.slice(scheme[0].length);
53
+ const slash = rest.indexOf("/");
54
+ const host = slash === -1 ? rest : rest.slice(0, slash);
55
+ u = scheme[0].toLowerCase() + host.toLowerCase() + (slash === -1 ? "" : rest.slice(slash));
56
+ }
57
+ else {
58
+ const at = u.indexOf("@");
59
+ const colon = u.indexOf(":", at);
60
+ if (at !== -1 && colon !== -1)
61
+ u = u.slice(0, at + 1) + u.slice(at + 1, colon).toLowerCase() + u.slice(colon);
62
+ }
63
+ u = u.replace(/\/+$/, "");
64
+ return u.replace(/\.git$/, "");
65
+ }
66
+ /** `<readable slug of host and path>-<first 12 hex of sha256(normalized url)>`. The URL holds no credentials (§4.3). */
67
+ export function cacheKey(url) {
68
+ const normalized = normalizeUrl(url);
69
+ // The one hash helper (node-cli rule), though a URL has no line endings to normalize.
70
+ const hash = hashNormalized(normalized).slice("sha256:".length, "sha256:".length + 12);
71
+ const withoutScheme = normalized.replace(SCHEME_URL, "").replace(/^[^@/]*@/, "");
72
+ const slug = withoutScheme
73
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
74
+ .replace(/^-+|-+$/g, "")
75
+ .toLowerCase()
76
+ .slice(0, 60)
77
+ .replace(/-+$/, "");
78
+ return `${slug}-${hash}`;
79
+ }
80
+ export function parseLsRemote(stdout) {
81
+ const out = { head: null, heads: new Map(), tags: new Map() };
82
+ for (const line of stdout.split(/\r?\n/)) {
83
+ if (!line.trim())
84
+ continue;
85
+ const symref = /^ref:\s+refs\/heads\/(\S+)\s+HEAD$/.exec(line);
86
+ if (symref) {
87
+ out.head = symref[1];
88
+ continue;
89
+ }
90
+ const [sha, name] = line.split("\t");
91
+ if (!sha || !name)
92
+ continue;
93
+ if (name.startsWith("refs/heads/"))
94
+ out.heads.set(name.slice("refs/heads/".length), sha);
95
+ else if (name.startsWith("refs/tags/")) {
96
+ const tag = name.slice("refs/tags/".length);
97
+ // An annotated tag's object comes first; its `^{}` line is the commit, which wins.
98
+ if (tag.endsWith("^{}"))
99
+ out.tags.set(tag.slice(0, -3), sha);
100
+ else if (!out.tags.has(tag))
101
+ out.tags.set(tag, sha);
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ const FULL_SHA = /^[0-9a-f]{40}$/;
107
+ /**
108
+ * The commit a requested ref names, in the order of spec 13 §6.2. A full SHA is returned as is: the
109
+ * caller checks it exists once the fetch has run. Abbreviated SHAs are refused on purpose — their
110
+ * meaning changes as the repository grows.
111
+ */
112
+ export function resolveRef(refs, ref, url) {
113
+ if (ref === null) {
114
+ const branch = refs.head;
115
+ const commit = branch === null ? undefined : refs.heads.get(branch);
116
+ if (branch === null || commit === undefined)
117
+ throw new Error(`${url} has no default branch to follow — set ref in craftar.yaml`);
118
+ return { commit, defaultBranch: branch };
119
+ }
120
+ if (FULL_SHA.test(ref))
121
+ return { commit: ref, defaultBranch: null };
122
+ const qualified = /^refs\/(heads|tags)\/(.+)$/.exec(ref);
123
+ if (qualified) {
124
+ const commit = (qualified[1] === "heads" ? refs.heads : refs.tags).get(qualified[2]);
125
+ if (commit === undefined)
126
+ throw new Error(`ref "${ref}" not found in ${url}`);
127
+ return { commit, defaultBranch: null };
128
+ }
129
+ const branch = refs.heads.get(ref);
130
+ const tag = refs.tags.get(ref);
131
+ if (branch !== undefined && tag !== undefined)
132
+ throw new Error(`ref "${ref}" is both a branch and a tag in ${url} — write refs/heads/${ref} or refs/tags/${ref}`);
133
+ const commit = branch ?? tag;
134
+ if (commit === undefined)
135
+ throw new Error(`ref "${ref}" not found in ${url}`);
136
+ return { commit, defaultBranch: null };
137
+ }
138
+ /** The remote could not be reached; `cached` says what the cache could still offer. */
139
+ export class ForgeFetchError extends Error {
140
+ url;
141
+ gitMessage;
142
+ haveCopy;
143
+ cached;
144
+ constructor(url, gitMessage,
145
+ /** A completed fetch left a copy, whether or not it can resolve the ref. */
146
+ haveCopy, cached) {
147
+ super(`cannot fetch the Forge ${url}: ${gitMessage}`);
148
+ this.url = url;
149
+ this.gitMessage = gitMessage;
150
+ this.haveCopy = haveCopy;
151
+ this.cached = cached;
152
+ }
153
+ }
154
+ /** Reading the cache only (`--offline`, `targets`) found no copy of this Forge. */
155
+ export class NoCachedCopyError extends Error {
156
+ url;
157
+ constructor(url) {
158
+ super(`the Forge ${url} has no cached copy yet`);
159
+ this.url = url;
160
+ }
161
+ }
162
+ const execFileP = promisify(execFile);
163
+ export const defaultGit = async (args, opts = {}) => {
164
+ const env = { ...process.env };
165
+ // CI fails fast instead of hanging on a credential prompt; a terminal may still prompt (spec 13 §14 item 4).
166
+ if (opts.remote && !process.stdin.isTTY)
167
+ env.GIT_TERMINAL_PROMPT = "0";
168
+ try {
169
+ const { stdout } = await execFileP("git", args, { env, maxBuffer: 64 * 1024 * 1024 });
170
+ return stdout;
171
+ }
172
+ catch (e) {
173
+ const err = e;
174
+ const line = (err.stderr ?? "").split(/\r?\n/).map((l) => l.trim()).find((l) => l) ?? err.message;
175
+ throw new Error(line.replace(/^fatal:\s*/, ""));
176
+ }
177
+ };
178
+ /** The tree for `ref` of the remote Forge at `url`, fetching first unless offline or the ref is a full SHA already cached. */
179
+ export async function ensureTree(written, ref, opts) {
180
+ // As classifyForge reads it: git would choke on a stray leading space.
181
+ const url = written.trim();
182
+ const git = opts.git ?? defaultGit;
183
+ // Absolute once: `git -C repo.git worktree add <dir>` would read a relative dir from inside repo.git.
184
+ const home = path.resolve(opts.home);
185
+ const entry = path.join(home, "forges", cacheKey(url));
186
+ const repo = path.join(entry, "repo.git");
187
+ const stamp = path.join(entry, "fetched");
188
+ // A copy exists once a fetch completed: an init whose fetch then failed leaves no stamp.
189
+ const haveCopy = await exists(stamp);
190
+ const fetchedAt = async () => {
191
+ const text = (await exists(stamp)) ? (await fs.readFile(stamp, "utf8")).trim() : "";
192
+ return /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/.test(text) ? text : null;
193
+ };
194
+ const label = ref ?? "the default branch";
195
+ // A full SHA already in the cache names the same commit whatever the remote does: no fetch (§14 item 3).
196
+ const cachedSha = ref !== null && FULL_SHA.test(ref) && haveCopy && (await hasCommit(git, repo, ref));
197
+ let resolved;
198
+ let fetched = false;
199
+ if (opts.offline || cachedSha) {
200
+ if (!haveCopy)
201
+ throw new NoCachedCopyError(url);
202
+ resolved = cachedSha ? { commit: ref, defaultBranch: null } : resolveRef(await cachedRefs(git, repo), ref, url);
203
+ if (!(await hasCommit(git, repo, resolved.commit)))
204
+ throw new Error(`ref "${label}" not found in ${url}`);
205
+ }
206
+ else {
207
+ let refs;
208
+ try {
209
+ refs = parseLsRemote(await git(["ls-remote", "--symref", url], { remote: true }));
210
+ }
211
+ catch (e) {
212
+ throw new ForgeFetchError(url, e.message, haveCopy, await cachedFallback(git, repo, haveCopy, ref, url, await fetchedAt()));
213
+ }
214
+ resolved = resolveRef(refs, ref, url);
215
+ try {
216
+ await fs.mkdir(entry, { recursive: true });
217
+ }
218
+ catch (e) {
219
+ throw new Error(`cannot write the Forge cache in ${home}: ${e.code ?? e.message}`);
220
+ }
221
+ await withLock(entry, opts, async () => {
222
+ if (!(await exists(path.join(repo, "HEAD"))))
223
+ await git(["init", "--quiet", "--bare", repo]);
224
+ try {
225
+ await git(["-C", repo, "fetch", "--quiet", "--prune", url, "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*"], { remote: true });
226
+ }
227
+ catch (e) {
228
+ throw new ForgeFetchError(url, e.message, haveCopy, await cachedFallback(git, repo, haveCopy, ref, url, await fetchedAt()));
229
+ }
230
+ // A bare repository filled by fetch does not keep the remote's HEAD; --offline without a ref needs it.
231
+ if (refs.head)
232
+ await git(["-C", repo, "symbolic-ref", "HEAD", `refs/heads/${refs.head}`]);
233
+ await fs.writeFile(stamp, new Date().toISOString() + "\n");
234
+ });
235
+ fetched = true;
236
+ if (!(await hasCommit(git, repo, resolved.commit)))
237
+ throw new Error(`ref "${label}" not found in ${url}`);
238
+ }
239
+ const dir = await treeFor(git, entry, repo, resolved.commit, opts);
240
+ if (fetched)
241
+ await cleanup(git, entry, repo, resolved.commit, opts);
242
+ return { dir, commit: resolved.commit, defaultBranch: resolved.defaultBranch, fetched, fetchedAt: await fetchedAt() };
243
+ }
244
+ async function hasCommit(git, repo, sha) {
245
+ try {
246
+ await git(["-C", repo, "cat-file", "-e", `${sha}^{commit}`]);
247
+ return true;
248
+ }
249
+ catch {
250
+ return false;
251
+ }
252
+ }
253
+ /** The refs as the last fetch left them in the bare repository, in `ls-remote`'s shape. */
254
+ async function cachedRefs(git, repo) {
255
+ const lines = [];
256
+ try {
257
+ lines.push(`ref: ${(await git(["-C", repo, "symbolic-ref", "HEAD"])).trim()}\tHEAD`);
258
+ }
259
+ catch {
260
+ // no recorded default branch
261
+ }
262
+ const listed = await git(["-C", repo, "for-each-ref", "--format=%(objectname)\t%(refname)\t%(*objectname)", "refs/heads", "refs/tags"]);
263
+ for (const line of listed.split(/\r?\n/)) {
264
+ const [sha, name, peeled] = line.split("\t");
265
+ if (!sha || !name)
266
+ continue;
267
+ lines.push(`${sha}\t${name}`);
268
+ if (peeled)
269
+ lines.push(`${peeled}\t${name}^{}`);
270
+ }
271
+ return parseLsRemote(lines.join("\n"));
272
+ }
273
+ async function cachedFallback(git, repo, haveRepo, ref, url, fetchedAt) {
274
+ if (!haveRepo)
275
+ return null;
276
+ try {
277
+ const { commit } = ref !== null && FULL_SHA.test(ref) ? { commit: ref } : resolveRef(await cachedRefs(git, repo), ref, url);
278
+ return (await hasCommit(git, repo, commit)) ? { commit, fetchedAt } : null;
279
+ }
280
+ catch {
281
+ return null;
282
+ }
283
+ }
284
+ /** One worktree per resolved commit; never written to after creation. A failure leaves no half-made tree. */
285
+ async function treeFor(git, entry, repo, commit, opts) {
286
+ const dir = path.join(entry, "trees", commit);
287
+ // Complete once Craftar wrote `<commit>.ok` under the lock, after `worktree add` returned. Git's own
288
+ // `.git` file comes before the checkout, so a run cut short — or one still checking out in another
289
+ // process — leaves a tree without the marker: it is removed and made again, never loaded half.
290
+ const marker = `${dir}.ok`;
291
+ const complete = async () => (await exists(marker)) && (await exists(path.join(dir, ".git")));
292
+ if (!(await complete())) {
293
+ await withLock(entry, opts, async () => {
294
+ if (await complete())
295
+ return;
296
+ if (await exists(dir)) {
297
+ await git(["-C", repo, "worktree", "remove", "--force", dir]).catch(() => { });
298
+ await fs.rm(dir, { recursive: true, force: true });
299
+ }
300
+ await git(["-C", repo, "worktree", "prune"]).catch(() => { });
301
+ await fs.mkdir(path.dirname(dir), { recursive: true });
302
+ try {
303
+ // The commit's bytes, the same on every platform (spec 13 §8): no host setting rewrites the
304
+ // checkout — not core.autocrlf, not core.eol (native CRLF on Git for Windows, applied to a
305
+ // Forge's `text=auto`), not a global attributes file. A Forge's own `eol=` still applies: that
306
+ // is the Forge author's choice.
307
+ await git(["-c", "core.autocrlf=false", "-c", "core.eol=lf", "-c", "core.attributesFile=", "-C", repo, "worktree", "add", "--quiet", "--detach", dir, commit]);
308
+ await fs.writeFile(marker, `${new Date().toISOString()}\n`);
309
+ }
310
+ catch (e) {
311
+ await git(["-C", repo, "worktree", "remove", "--force", dir]).catch(() => { });
312
+ await fs.rm(dir, { recursive: true, force: true });
313
+ await git(["-C", repo, "worktree", "prune"]).catch(() => { });
314
+ throw new Error(`cannot create the Forge tree ${dir}: ${e.message}`);
315
+ }
316
+ });
317
+ }
318
+ const used = `${dir}.used`;
319
+ const now = new Date();
320
+ await fs.writeFile(used, "").catch(() => { });
321
+ await fs.utimes(used, now, now).catch(() => { });
322
+ return dir;
323
+ }
324
+ /** Trees unused for `cleanupMs` go, except the one in use; `repo.git` is left to git. */
325
+ async function cleanup(git, entry, repo, inUse, opts) {
326
+ const trees = path.join(entry, "trees");
327
+ const limit = Date.now() - (opts.cleanupMs ?? 14 * 24 * 60 * 60 * 1000);
328
+ // When a tree was last used: its `.used` stamp, else its completion marker, else the directory — so a
329
+ // tree whose run died before stamping it still ages out.
330
+ const lastUse = async (commit) => {
331
+ for (const p of [`${commit}.used`, `${commit}.ok`, commit]) {
332
+ const st = await fs.stat(path.join(trees, p)).catch(() => null);
333
+ if (st)
334
+ return st.mtimeMs;
335
+ }
336
+ return null;
337
+ };
338
+ let removed = false;
339
+ for (const name of await fs.readdir(trees).catch(() => [])) {
340
+ if (name.includes(".") || name === inUse)
341
+ continue;
342
+ const used = await lastUse(name);
343
+ if (used === null || used >= limit)
344
+ continue;
345
+ await withLock(entry, opts, async () => {
346
+ // Again under the lock: a reader may have touched the stamp since (readers take no lock).
347
+ const again = await lastUse(name);
348
+ if (again === null || again >= limit)
349
+ return;
350
+ await git(["-C", repo, "worktree", "remove", "--force", path.join(trees, name)]).catch(() => { });
351
+ for (const p of [name, `${name}.used`, `${name}.ok`])
352
+ await fs.rm(path.join(trees, p), { recursive: true, force: true });
353
+ removed = true;
354
+ });
355
+ }
356
+ // A stamp or marker whose tree is gone (a reader stamped it as cleanup took the tree) goes too — under
357
+ // the lock, so it never races a rebuild between its `rm` of the tree and the `.ok` it then writes.
358
+ await withLock(entry, opts, async () => {
359
+ for (const name of await fs.readdir(trees).catch(() => [])) {
360
+ const m = /^(.+)\.(used|ok)$/.exec(name);
361
+ if (m && !(await exists(path.join(trees, m[1]))))
362
+ await fs.rm(path.join(trees, name), { force: true });
363
+ }
364
+ });
365
+ if (removed)
366
+ await git(["-C", repo, "worktree", "prune"]).catch(() => { });
367
+ }
368
+ /** The entry's lock: created exclusively, holding the PID and a timestamp; waited for, then refused; taken over when stale. */
369
+ async function withLock(entry, opts, body) {
370
+ const file = path.join(entry, "lock");
371
+ const waitMs = opts.waitMs ?? 60_000;
372
+ const pollMs = opts.pollMs ?? 200;
373
+ const staleMs = opts.staleMs ?? 10 * 60 * 1000;
374
+ const start = Date.now();
375
+ for (;;) {
376
+ try {
377
+ await fs.writeFile(file, `${process.pid} ${new Date().toISOString()}\n`, { flag: "wx" });
378
+ break;
379
+ }
380
+ catch (e) {
381
+ if (e.code !== "EEXIST")
382
+ throw e;
383
+ const stat = await fs.stat(file).catch(() => null);
384
+ if (stat && Date.now() - stat.mtimeMs > staleMs) {
385
+ await fs.rm(file, { force: true });
386
+ continue;
387
+ }
388
+ if (Date.now() - start >= waitMs) {
389
+ const held = (await fs.readFile(file, "utf8").catch(() => "")).split(/\s+/)[0] || "unknown";
390
+ throw new Error(`the Forge cache entry ${file} is busy (held by PID ${held})`);
391
+ }
392
+ await new Promise((r) => setTimeout(r, pollMs));
393
+ }
394
+ }
395
+ try {
396
+ return await body();
397
+ }
398
+ finally {
399
+ await fs.rm(file, { force: true });
400
+ }
401
+ }
402
+ //# sourceMappingURL=remote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.js","sourceRoot":"","sources":["../../src/core/remote.ts"],"names":[],"mappings":"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"}
package/dist/core/sync.js CHANGED
@@ -1,38 +1,121 @@
1
1
  import { promises as fs } from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
- import YAML from "yaml";
4
+ import { isDeepStrictEqual } from "node:util";
4
5
  import { loadForge, exists, listFiles, FORGE_MANIFEST } from "./forge.js";
5
6
  import { resolve, substitute, paramsFor, sectionKey, sectionsFor } from "./resolve.js";
6
7
  import { hashNormalized, stripBom, toLf } from "./text.js";
8
+ import { parseWorkspaceYaml } from "./workspace-yaml.js";
9
+ import { classifyForge, credentialFault, ensureTree, ForgeFetchError, NoCachedCopyError } from "./remote.js";
7
10
  import { deepMerge } from "./merge.js";
8
11
  import { canonicalValue, checkDeclaredOnce, expandSections, firstMarkerLine, markerLine, parseSections } from "./sections.js";
9
12
  import { placeholders, bodyFile, emittedFile } from "./extract.js";
10
- import { LockSchema, WorkspaceConfigSchema } from "../schema/index.js";
13
+ import { LOCK_SCHEMAS, LockSchema, WorkspaceConfigSchema } from "../schema/index.js";
11
14
  import { claudeCode } from "../emitters/claude-code.js";
12
15
  import { kiro } from "../emitters/kiro.js";
13
16
  import { agentsMd } from "../emitters/agents-md.js";
17
+ import { written } from "./capabilities.js";
14
18
  export const WORKSPACE_FILE = "craftar.yaml";
15
19
  export const LOCAL_FILE = "craftar.local.yaml";
16
20
  export const LOCK_FILE = "craftar.lock";
21
+ // Mapped, not Record<Target, Emitter>: an emitter in another target's slot does not compile (spec 18 §3.3).
17
22
  const EMITTERS = { "claude-code": claudeCode, kiro, "agents-md": agentsMd };
18
- export async function loadWorkspace(root) {
23
+ /**
24
+ * Runs one target's emitter on the walk of what the matrix says it writes. The walk warns about the
25
+ * rest, so an emitter that returns before finishing it would lose warnings: that fails the plan.
26
+ */
27
+ export async function emitFor(t, emitter, base) {
28
+ const aimed = written(base.resolution, t, base.warn);
29
+ const files = await emitter.emit({ ...base, aimed });
30
+ if (!aimed.finished)
31
+ throw new Error(`internal: the ${t} emitter returned before walking every ingredient aimed at it`);
32
+ return files;
33
+ }
34
+ export async function loadWorkspace(root, opts = {}) {
19
35
  root = path.resolve(root);
20
36
  const file = path.join(root, WORKSPACE_FILE);
21
37
  if (!(await exists(file)))
22
38
  throw new Error(`${WORKSPACE_FILE} not found in ${root} — run \`craftar import --workspace "${root}" --from claude-code --forge <dir> --profile <name> --write-config\` to create it`);
23
- const base = YAML.parse(await fs.readFile(file, "utf8")) ?? {};
39
+ const base = parseWorkspaceYaml(WORKSPACE_FILE, await fs.readFile(file, "utf8")) ?? {};
24
40
  const localFile = path.join(root, LOCAL_FILE);
25
41
  const hasLocal = await exists(localFile);
26
- const local = hasLocal ? YAML.parse(await fs.readFile(localFile, "utf8")) ?? {} : {};
42
+ const local = hasLocal ? parseWorkspaceYaml(LOCAL_FILE, await fs.readFile(localFile, "utf8")) ?? {} : {};
43
+ // Before anything can print the value: a credential in either file is refused by file and field (spec 13 §4.3).
44
+ for (const [name, raw] of [[WORKSPACE_FILE, base], [LOCAL_FILE, local]]) {
45
+ const value = raw !== null && typeof raw === "object" ? raw.forge : undefined;
46
+ if (typeof value === "string" && credentialFault(value))
47
+ throw new Error(`${name} › forge holds credentials in the URL — remove them and let git authenticate (credential helper, SSH agent; see README › Remote Forge)`);
48
+ }
49
+ const fromLocalFile = local !== null && typeof local === "object" && Object.hasOwn(local, "forge");
27
50
  const parsed = WorkspaceConfigSchema.safeParse(deepMerge(base, local));
28
51
  // Named, as a Forge file is: a wrong-shape `overrides.sections` key fails here (spec 11 §5.2).
29
52
  if (!parsed.success)
30
53
  throw new Error(`invalid ${WORKSPACE_FILE}${hasLocal ? ` (merged with ${LOCAL_FILE})` : ""}: ${parsed.error.message}`);
31
54
  const config = parsed.data;
32
- const forgeRoot = /^[a-z]+:\/\/|^git@/.test(config.forge) ? config.forge : path.resolve(root, config.forge);
55
+ const remote = classifyForge(config.forge) === "url";
56
+ const warnings = [];
57
+ if (fromLocalFile)
58
+ warnings.push(`Forge overridden by ${LOCAL_FILE} (${config.forge}) — do not commit ${LOCK_FILE} or the generated files`);
59
+ if (remote) {
60
+ if (opts.refuseRemote)
61
+ throw new Error(opts.refuseRemote(config.forge));
62
+ const { tree, origin, warning } = await remoteTree(config.forge, config.ref ?? null, opts, fromLocalFile);
63
+ if (warning)
64
+ warnings.push(warning);
65
+ return { root, config, forge: await loadForge(tree), origin, warnings };
66
+ }
67
+ const forgeRoot = path.resolve(root, config.forge);
33
68
  if (!(await exists(forgeRoot)))
34
- throw new Error(`Forge not found at ${forgeRoot} (remote Forges are not supported yet — clone it and point \`forge:\` at the path)`);
35
- return { root, config, forge: await loadForge(forgeRoot) };
69
+ throw new Error(`Forge not found at ${forgeRoot}`);
70
+ // A path means the working tree as it is; a ref beside it is ignored, said out loud (Ruling 9).
71
+ if (config.ref !== undefined)
72
+ warnings.unshift(`ref "${config.ref}" is ignored: the Forge is a path (${config.forge}), read as its working tree`);
73
+ const origin = { kind: "path", source: config.forge, ref: null, defaultBranch: null, fetched: false, fromLocalFile };
74
+ return { root, config, forge: await loadForge(forgeRoot), origin, warnings };
75
+ }
76
+ const short = (sha) => sha.slice(0, 8);
77
+ /** Fetch (or not) by the rule of `opts.mode`, and say what was used (spec 13 §4.1, §4.8). */
78
+ async function remoteTree(url, ref, opts, fromLocalFile) {
79
+ const home = opts.home ?? path.join(os.homedir(), ".craftar");
80
+ const mode = opts.mode ?? "read";
81
+ const offline = opts.offline || mode === "no-fetch";
82
+ const origin = (t) => ({
83
+ kind: "remote",
84
+ source: url,
85
+ ref,
86
+ defaultBranch: t.defaultBranch,
87
+ fetched: t.fetched,
88
+ ...(t.fetchedAt ? { fetchedAt: t.fetchedAt } : {}),
89
+ fromLocalFile,
90
+ });
91
+ const used = (t) => `the cached copy at ${short(t.commit)}, fetched ${t.fetchedAt ?? "never"}`;
92
+ if (offline) {
93
+ let t;
94
+ try {
95
+ t = await ensureTree(url, ref, { home, offline: true });
96
+ }
97
+ catch (e) {
98
+ if (!(e instanceof NoCachedCopyError))
99
+ throw e;
100
+ // `targets` (no-fetch) takes no --offline: point at a command that fetches.
101
+ throw new Error(`${e.message} — ${opts.offline ? "run without --offline once to fetch it" : "run craftar status once to fetch it"}`);
102
+ }
103
+ return { tree: t.dir, origin: origin(t), warning: opts.offline ? `Forge ${url} not fetched (--offline) — using ${used(t)}` : null };
104
+ }
105
+ try {
106
+ const t = await ensureTree(url, ref, { home });
107
+ return { tree: t.dir, origin: origin(t), warning: null };
108
+ }
109
+ catch (e) {
110
+ if (!(e instanceof ForgeFetchError))
111
+ throw e;
112
+ if (!e.cached)
113
+ throw new Error(`cannot fetch the Forge ${url}: ${e.gitMessage} — ${e.haveCopy ? `and the cached copy cannot resolve ${ref ?? "the default branch"}` : "and there is no cached copy to fall back on"}`);
114
+ if (mode === "sync")
115
+ throw new Error(`cannot fetch the Forge ${url}: ${e.gitMessage} — run with --offline to use the cached copy (${short(e.cached.commit)} fetched ${e.cached.fetchedAt ?? "never"})`);
116
+ const t = await ensureTree(url, ref, { home, offline: true });
117
+ return { tree: t.dir, origin: origin(t), warning: `Forge ${url} not fetched (${e.gitMessage}) — using ${used(t)}` };
118
+ }
36
119
  }
37
120
  /**
38
121
  * The Forge and (optionally) the workspace a command operates on.
@@ -43,13 +126,17 @@ export async function resolveForgeSource(opts) {
43
126
  if (opts.forge !== undefined && opts.workspace !== undefined) {
44
127
  throw new Error("pass either --forge or --workspace, not both — two sources for one Forge");
45
128
  }
46
- if (opts.forge !== undefined)
129
+ if (opts.forge !== undefined) {
130
+ // A URL is not a directory to load: a remote Forge is read through a workspace that names it (spec 13 §4.4).
131
+ if (classifyForge(opts.forge) === "url")
132
+ throw new Error("--forge takes a directory; to read a remote Forge, run inside a workspace that names it");
47
133
  return { forge: await loadForge(path.resolve(opts.forge)) };
134
+ }
48
135
  const root = path.resolve(opts.workspace ?? ".");
49
136
  if (!(await exists(path.join(root, WORKSPACE_FILE)))) {
50
137
  throw new Error(`no ${WORKSPACE_FILE} in ${root} — run this inside a workspace, pass --workspace <dir>, or point at the Forge with --forge <dir>`);
51
138
  }
52
- const workspace = await loadWorkspace(root);
139
+ const workspace = await loadWorkspace(root, opts);
53
140
  return { forge: workspace.forge, workspace };
54
141
  }
55
142
  /**
@@ -172,7 +259,7 @@ function guardOutput(ing, file, out, p, resolution, params) {
172
259
  }
173
260
  export async function plan(ws) {
174
261
  const resolution = resolve(ws.forge, ws.config);
175
- const warnings = [...resolution.warnings];
262
+ const warnings = [...ws.warnings, ...resolution.warnings];
176
263
  if (resolution.targets.length === 0) {
177
264
  warnings.push("no targets resolved — nothing will be emitted and every file in craftar.lock becomes an orphan (an empty list in craftar.local.yaml replaces the workspace's)");
178
265
  }
@@ -225,7 +312,7 @@ export async function plan(ws) {
225
312
  warnings.push(`target "${t}" has no emitter yet`);
226
313
  continue;
227
314
  }
228
- files.push(...(await em.emit(ctx)));
315
+ files.push(...(await emitFor(t, em, ctx)));
229
316
  }
230
317
  for (const [key, refs] of [...missingParams].sort(([a], [b]) => a.localeCompare(b))) {
231
318
  warnings.push(`param "${key}" has no value in any layer — left verbatim (${[...refs].sort().join(", ")})`);
@@ -253,7 +340,11 @@ export async function readLock(root) {
253
340
  const f = path.join(root, LOCK_FILE);
254
341
  if (!(await exists(f)))
255
342
  return null;
256
- return LockSchema.parse(JSON.parse(await fs.readFile(f, "utf8")));
343
+ const raw = JSON.parse(await fs.readFile(f, "utf8"));
344
+ // A lock a later craftar wrote is refused by name, not with a zod dump (spec 13 §4.6).
345
+ if (raw !== null && typeof raw === "object" && Object.hasOwn(raw, "schema") && !LOCK_SCHEMAS.includes(raw.schema))
346
+ throw new Error(`${LOCK_FILE} declares schema ${JSON.stringify(raw.schema)}, which this craftar does not read — upgrade craftar`);
347
+ return LockSchema.parse(raw);
257
348
  }
258
349
  export async function writeLock(root, lock) {
259
350
  await fs.writeFile(path.join(root, LOCK_FILE), JSON.stringify(lock, null, 2) + "\n", "utf8");
@@ -353,14 +444,22 @@ export async function apply(ws, p, statuses, opts = {}) {
353
444
  break;
354
445
  }
355
446
  }
356
- const lock = {
357
- schema: 1,
358
- forge: { source: ws.config.forge, commit: ws.forge.commit },
447
+ // Schema 2, in this key order (spec 13 §5.2): what the sync used, not only what it wrote.
448
+ const built = {
449
+ schema: 2,
450
+ forge: { source: ws.config.forge, ref: ws.origin.ref, commit: ws.forge.commit },
359
451
  profile: ws.config.profile,
452
+ recipes: p.resolution.recipes,
453
+ targets: p.resolution.targets,
360
454
  generatedAt: new Date().toISOString(),
361
455
  files: entries.sort((a, b) => a.path.localeCompare(b.path)),
362
456
  };
363
- if (!opts.dryRun)
457
+ // Rewritten only when its content changes: generatedAt is when it last did (Ruling 10). A schema 1
458
+ // lock always differs, so the first sync with this craftar upgrades it.
459
+ const onDisk = opts.dryRun ? null : await readLock(ws.root);
460
+ const unchanged = onDisk !== null && isDeepStrictEqual({ ...onDisk, generatedAt: null }, { ...built, generatedAt: null });
461
+ const lock = unchanged ? onDisk : built;
462
+ if (!opts.dryRun && !unchanged)
364
463
  await writeLock(ws.root, lock);
365
464
  return { written, removed, skipped, lock };
366
465
  }