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.
- package/README.md +34 -9
- package/dist/cli.js +132 -26
- package/dist/cli.js.map +1 -1
- package/dist/core/capabilities.js +58 -1
- package/dist/core/capabilities.js.map +1 -1
- package/dist/core/remote.js +402 -0
- package/dist/core/remote.js.map +1 -0
- package/dist/core/sync.js +116 -17
- package/dist/core/sync.js.map +1 -1
- package/dist/core/workspace-yaml.js +18 -0
- package/dist/core/workspace-yaml.js.map +1 -0
- package/dist/emitters/agents-md.js +17 -15
- package/dist/emitters/agents-md.js.map +1 -1
- package/dist/emitters/claude-code.js +12 -12
- package/dist/emitters/claude-code.js.map +1 -1
- package/dist/emitters/kiro.js +11 -9
- package/dist/emitters/kiro.js.map +1 -1
- package/dist/emitters/shared.js +4 -3
- package/dist/emitters/shared.js.map +1 -1
- package/dist/importers/claude-code.js +19 -6
- package/dist/importers/claude-code.js.map +1 -1
- package/dist/importers/decide.js +3 -3
- package/dist/importers/decide.js.map +1 -1
- package/dist/schema/index.js +27 -2
- package/dist/schema/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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 ?
|
|
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
|
|
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}
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
}
|