craftar 0.28.2 → 0.30.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,164 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { EXAMPLE_SETTINGS } from "../emitters/shared.js";
4
+ import { listTargets } from "./capabilities.js";
5
+ import { diffOps } from "./diff.js";
6
+ import { mcpTagged } from "./drift.js";
7
+ import { FULL_SHA } from "./remote.js";
8
+ import { resolve } from "./resolve.js";
9
+ import { errorText } from "./schema-fault.js";
10
+ import { loadForgeFor, unsetDeclared } from "./sync.js";
11
+ import { hashNormalized, stripBom, toLf } from "./text.js";
12
+ /*
13
+ * What the reading commands answer, as data (spec 35 §5.1): one builder per shape, called by the CLI, by
14
+ * `craftar mcp` and by anything else that shows the same thing. Nothing here prints, and none of them selects,
15
+ * normalizes or refuses a path — each caller keeps its own rule.
16
+ */
17
+ /** The one test of which states `diff` shows — the states `sync --check` refuses. */
18
+ export function diffShows(state) {
19
+ return state !== "unchanged" && state !== "adopt";
20
+ }
21
+ /** Spec 27 Ruling 4: the on-disk content of the example file is never shown. No indentation: the caller's. */
22
+ export const EXAMPLE_CONTENT_HIDDEN = "content not shown: an example file may hold a value typed by hand";
23
+ /** Spec 35 §4.5: what `craftar mcp` says in place of the disk side of a hand-touched MCP file. */
24
+ export const MCP_CONTENT_HIDDEN = "content not shown: an MCP configuration file may hold a value typed by hand";
25
+ /** A path as the user may type it, to the spelling `status` prints: forward slashes, no leading `./`. */
26
+ export const statusPath = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
27
+ /** `explain`'s refusal: the Forge does not produce this path for the profile. */
28
+ export const notProduced = (p, profile) => `${p} is not produced by the Forge for profile ${profile}`;
29
+ /** Why sync leaves a file alone, for the three states it skips. */
30
+ export function explainSkip(s) {
31
+ switch (s.state) {
32
+ case "drift":
33
+ return "hand-edited since last sync — `craftar drift show <path>`, then `craftar drift discard` or `craftar drift promote`";
34
+ case "collision":
35
+ return "exists but was never generated by craftar and differs from the Forge — rename it or import it";
36
+ case "orphan-drift":
37
+ return "no longer produced by the Forge but hand-edited — kept; `craftar drift discard <path>` removes it, or delete it yourself";
38
+ default:
39
+ return "";
40
+ }
41
+ }
42
+ /** A file's text as a diff compares it: LF, no BOM; null when it cannot be read. */
43
+ export async function readText(p) {
44
+ try {
45
+ return toLf(stripBom(await fs.readFile(p, "utf8")));
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ export function statusReport(ws, p, lock, statuses) {
52
+ const o = ws.origin;
53
+ return {
54
+ forge: {
55
+ kind: o.kind,
56
+ source: o.source,
57
+ ref: o.ref,
58
+ defaultBranch: o.defaultBranch,
59
+ commit: ws.forge.commit,
60
+ lockCommit: lock?.forge.commit ?? null,
61
+ fetched: o.fetched,
62
+ },
63
+ // The planned buffer never leaves the process: dropped here, once, for every reader of the document.
64
+ statuses: statuses.map(({ planned, ...s }) => s),
65
+ warnings: p.warnings,
66
+ unsetParams: unsetDeclared(p),
67
+ };
68
+ }
69
+ /**
70
+ * What `diff` shows for each row in a state `sync --check` refuses (spec 19 §3.2: `unchanged` and `adopt` are
71
+ * skipped). It never renders: the caller owns the header lines and the paint. `withholdMcp` widens the one
72
+ * withholding of spec 27 Ruling 4 — the example file — to every file tagged `mcp/*`, for a reader that is a model
73
+ * (spec 35 §4.5). The disk is read at the row's path, never at one a caller typed.
74
+ */
75
+ export async function diffReport(ws, rows, opts) {
76
+ const out = [];
77
+ for (const s of rows) {
78
+ if (!diffShows(s.state))
79
+ continue;
80
+ const noted = (note) => ({ path: s.path, state: s.state, added: null, removed: null, note, disk: null, forge: null });
81
+ if (s.state === "orphan-drift") {
82
+ // No body: sync keeps this file
83
+ out.push(noted(explainSkip(s)));
84
+ continue;
85
+ }
86
+ // Only where a hand has touched the file: in `update` and `orphan` the disk side is what craftar wrote.
87
+ if (s.state === "drift" || s.state === "collision") {
88
+ if (opts.withholdMcp && mcpTagged(s)) {
89
+ out.push(noted(MCP_CONTENT_HIDDEN));
90
+ continue;
91
+ }
92
+ if (s.path === EXAMPLE_SETTINGS) {
93
+ out.push(noted(EXAMPLE_CONTENT_HIDDEN));
94
+ continue;
95
+ }
96
+ }
97
+ const disk = (await readText(path.join(ws.root, s.path))) ?? "";
98
+ // An orphan is the removal of the whole file
99
+ const forge = s.state === "orphan" || !s.planned ? "" : toLf(stripBom(s.planned.content.toString("utf8")));
100
+ const ops = diffOps(disk, forge);
101
+ out.push({
102
+ path: s.path,
103
+ state: s.state,
104
+ added: ops.filter((o) => o.kind === "add").length,
105
+ removed: ops.filter((o) => o.kind === "del").length,
106
+ note: null,
107
+ disk,
108
+ forge,
109
+ });
110
+ }
111
+ return out;
112
+ }
113
+ /** The planned file at `file`, compared as given (the caller normalizes); null when the Forge does not produce it. */
114
+ export function explainFile(ws, p, file) {
115
+ const f = p.files.find((x) => x.path === file);
116
+ if (!f)
117
+ return null;
118
+ // AGENTS.md (rule/*) and the MCP files (mcp/*) are not one ingredient: nothing resolved has their ref.
119
+ const ing = p.resolution.ingredients.find((i) => i.ref === f.ingredient);
120
+ return {
121
+ path: f.path,
122
+ target: f.target,
123
+ ingredient: f.ingredient,
124
+ origin: ing?.meta.origin ? { workspace: ing.meta.origin.workspace, path: ing.meta.origin.path } : null,
125
+ via: ing ? [...ing.via] : null,
126
+ sections: (p.sections.get(f.ingredient) ?? []).map((x) => ({ name: x.name, layer: x.layer })),
127
+ params: (p.params.get(f.ingredient) ?? []).map((x) => ({ key: x.key, layer: x.layer })),
128
+ profile: ws.config.profile,
129
+ recipes: [...p.resolution.recipes],
130
+ hash: hashNormalized(f.content),
131
+ };
132
+ }
133
+ /**
134
+ * The capability matrix with the targets a workspace uses marked. `input` is the configuration to load from
135
+ * (never fetched: `no-fetch`), a reason not to load — worded as a failed load is — or null for no workspace at
136
+ * all: the matrix unmarked, no warning. It never throws for a workspace that does not load or resolve.
137
+ */
138
+ export async function targetsReport(root, opts, input) {
139
+ if (input === null)
140
+ return { ...listTargets(null), warnings: [] };
141
+ const notShown = (why) => ({ ...listTargets(null), warnings: [`targets in use not shown: ${why}`] });
142
+ if ("reason" in input)
143
+ return notShown(input.reason);
144
+ try {
145
+ const ws = await loadForgeFor(root, input.merged, { ...opts, mode: "no-fetch" });
146
+ return { ...listTargets(resolve(ws.forge, ws.config).targets), warnings: [] };
147
+ }
148
+ catch (e) {
149
+ // Load or resolve failed — a warning, not an error
150
+ return notShown(errorText(e));
151
+ }
152
+ }
153
+ /**
154
+ * For a reader started without `--fetch` (`craftar mcp`, spec 35 §4.4): the line that says a remote Forge was read
155
+ * from its cached copy, so an answer for a Forge that has moved is never silent. Null when the caller fetches (the
156
+ * fallback warning is then the signal), for a path Forge, and for a full-SHA `ref`, which cannot go stale.
157
+ */
158
+ export function cachedCopyLine(ws, fetch) {
159
+ const o = ws.origin;
160
+ if (fetch || o.kind !== "remote" || (o.ref !== null && FULL_SHA.test(o.ref)))
161
+ return null;
162
+ return `Forge ${o.source} read from the copy cached ${o.fetchedAt ?? "(time unknown)"} (commit ${ws.forge.commit?.slice(0, 8) ?? "no git"}) — craftar mcp was started without --fetch`;
163
+ }
164
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/core/report.ts"],"names":[],"mappings":"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"}
package/dist/mcp.js ADDED
@@ -0,0 +1,87 @@
1
+ import { handleLine } from "./core/mcp-protocol.js";
2
+ /*
3
+ * The stdio transport of `craftar mcp` (spec 35 §4.3): one JSON-RPC message per line in, one per line out. The
4
+ * only place that knows the wire — the protocol itself is `src/core/mcp-protocol.ts`. It splits lines itself
5
+ * (the cap needs it, and `node:readline` stays `src/prompt.ts`'s alone), touches neither `process` nor `console`,
6
+ * and writes nothing but answers.
7
+ */
8
+ /** A line longer than this is discarded whole: one line cannot hold the server's memory. */
9
+ export const MAX_LINE_BYTES = 1024 * 1024;
10
+ /**
11
+ * Lines in, lines out, one call at a time: a line is not handled before the previous one was answered.
12
+ * Resolves when `input` has ended and the last answer was written.
13
+ */
14
+ export function serve(input, output, ctx) {
15
+ const state = { initialized: false };
16
+ let queue = Promise.resolve();
17
+ // The bytes of the line being read: decoded only once it is whole, so a character split across chunks survives.
18
+ let parts = [];
19
+ let size = 0;
20
+ // Inside a line that already passed the cap: its bytes are dropped up to the newline, never buffered.
21
+ let skipping = false;
22
+ const line = (bytes) => {
23
+ const end = bytes.length > 0 && bytes[bytes.length - 1] === 0x0d ? bytes.length - 1 : bytes.length;
24
+ // The cap is on the line itself: a CR before the newline is not counted against it
25
+ if (end === 0 || end > MAX_LINE_BYTES)
26
+ return;
27
+ const text = bytes.toString("utf8", 0, end);
28
+ queue = queue.then(async () => {
29
+ // A tool that fails is an answer. Anything else that throws — the handling itself, or a write to a reader
30
+ // that went away — drops this line only: the queue goes on, and nothing is said on any stream.
31
+ try {
32
+ const answer = await handleLine(text, ctx, state);
33
+ if (answer !== null)
34
+ output.write(answer + "\n");
35
+ }
36
+ catch {
37
+ /* dropped */
38
+ }
39
+ });
40
+ };
41
+ const feed = (chunk) => {
42
+ let from = 0;
43
+ for (;;) {
44
+ const newline = chunk.indexOf(0x0a, from);
45
+ const piece = chunk.subarray(from, newline === -1 ? chunk.length : newline);
46
+ if (!skipping) {
47
+ // One byte of slack while buffering, for that CR; `line()` holds the cap itself
48
+ if (size + piece.length > MAX_LINE_BYTES + 1) {
49
+ skipping = true;
50
+ parts = [];
51
+ size = 0;
52
+ }
53
+ else {
54
+ parts.push(piece);
55
+ size += piece.length;
56
+ }
57
+ }
58
+ if (newline === -1)
59
+ return;
60
+ if (!skipping)
61
+ line(Buffer.concat(parts));
62
+ skipping = false;
63
+ parts = [];
64
+ size = 0;
65
+ from = newline + 1;
66
+ }
67
+ };
68
+ return new Promise((resolve) => {
69
+ let over = false;
70
+ // The input is over once: at its end, or when it fails or closes without one (a pipe that resets). What was
71
+ // already read is still answered; a stream error never reaches the process as an uncaught exception.
72
+ const finish = (ended) => {
73
+ if (over)
74
+ return;
75
+ over = true;
76
+ // A last line with no newline is still a line — only when the input really ended
77
+ if (ended && !skipping && size > 0)
78
+ line(Buffer.concat(parts));
79
+ void queue.then(resolve);
80
+ };
81
+ input.on("data", (chunk) => feed(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk));
82
+ input.on("end", () => finish(true));
83
+ input.on("error", () => finish(false));
84
+ input.on("close", () => finish(false));
85
+ });
86
+ }
87
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkC,MAAM,wBAAwB,CAAC;AAEpF;;;;;GAKG;AAEH,4FAA4F;AAC5F,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAAC;AAE1C;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,KAA4B,EAAE,MAA6B,EAAE,GAAe;IAChG,MAAM,KAAK,GAAa,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IAC/C,IAAI,KAAK,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7C,gHAAgH;IAChH,IAAI,KAAK,GAAa,EAAE,CAAC;IACzB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,sGAAsG;IACtG,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;QACnG,mFAAmF;QACnF,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,GAAG,cAAc;YAAE,OAAO;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;QAC5C,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YAC5B,0GAA0G;YAC1G,+FAA+F;YAC/F,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;gBAClD,IAAI,MAAM,KAAK,IAAI;oBAAE,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;YACnD,CAAC;YAAC,MAAM,CAAC;gBACP,aAAa;YACf,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,EAAE;QAC7B,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,SAAS,CAAC;YACR,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YAC5E,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,gFAAgF;gBAChF,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,cAAc,GAAG,CAAC,EAAE,CAAC;oBAC7C,QAAQ,GAAG,IAAI,CAAC;oBAChB,KAAK,GAAG,EAAE,CAAC;oBACX,IAAI,GAAG,CAAC,CAAC;gBACX,CAAC;qBAAM,CAAC;oBACN,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBAClB,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC;gBACvB,CAAC;YACH,CAAC;YACD,IAAI,OAAO,KAAK,CAAC,CAAC;gBAAE,OAAO;YAC3B,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC1C,QAAQ,GAAG,KAAK,CAAC;YACjB,KAAK,GAAG,EAAE,CAAC;YACX,IAAI,GAAG,CAAC,CAAC;YACT,IAAI,GAAG,OAAO,GAAG,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,IAAI,GAAG,KAAK,CAAC;QACjB,4GAA4G;QAC5G,qGAAqG;QACrG,MAAM,MAAM,GAAG,CAAC,KAAc,EAAE,EAAE;YAChC,IAAI,IAAI;gBAAE,OAAO;YACjB,IAAI,GAAG,IAAI,CAAC;YACZ,iFAAiF;YACjF,IAAI,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,GAAG,CAAC;gBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC/D,KAAK,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC,CAAC;QACF,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAsB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACnH,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QACpC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACvC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,393 @@
1
+ /**
2
+ * The page of `craftar ui` (spec 36 §4.7, §7.4): one document, one script, one stylesheet, hand-written, as three
3
+ * string constants — `dist/` holds only what `tsc` emits. The server sends the document with a CSP that allows
4
+ * no inline script and no inline style, and the script puts every value it receives into the document as text
5
+ * (`textContent`), never as markup. `test/ui-page.test.ts` reads these sources; what the page does in a browser
6
+ * is a by-hand run.
7
+ */
8
+ import { diffShows } from "../core/report.js";
9
+ export const PAGE_HTML = `<!doctype html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1">
14
+ <title>craftar</title>
15
+ <link rel="stylesheet" href="/app.css">
16
+ </head>
17
+ <body>
18
+ <main id="app"></main>
19
+ <script src="/app.js"></script>
20
+ </body>
21
+ </html>
22
+ `;
23
+ export const PAGE_CSS = `:root { color-scheme: light dark; --fg: #1b1f24; --bg: #ffffff; --dim: #5c6670; --line: #d5dae0; --soft: #f3f5f7; --ok: #1a7f37; --warn: #9a6700; --bad: #b42318; --add: #1a7f37; --del: #b42318; }
24
+ @media (prefers-color-scheme: dark) { :root { --fg: #e6e9ec; --bg: #0f1317; --dim: #9aa5b1; --line: #2b333b; --soft: #171c21; --ok: #4ac26b; --warn: #d4a72c; --bad: #ff7b72; --add: #4ac26b; --del: #ff7b72; } }
25
+ * { box-sizing: border-box; }
26
+ body { margin: 0; font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--fg); background: var(--bg); }
27
+ main { max-width: 1200px; margin: 0 auto; padding: 16px 20px 48px; }
28
+ h1 { font-size: 18px; margin: 0 0 4px; }
29
+ h2 { font-size: 15px; margin: 24px 0 8px; }
30
+ h3 { font-size: 13px; margin: 16px 0 6px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim); }
31
+ p { margin: 4px 0; }
32
+ .dim { color: var(--dim); }
33
+ .mono, code, pre, td.path { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 12.5px; }
34
+ .bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 12px; }
35
+ button { font: inherit; color: inherit; background: var(--soft); border: 1px solid var(--line); border-radius: 6px; padding: 3px 10px; cursor: pointer; }
36
+ button:hover { border-color: var(--dim); }
37
+ button.link { background: none; border: 0; padding: 0; color: inherit; text-decoration: underline; font-weight: 600; }
38
+ button.on { border-color: var(--fg); }
39
+ .scroll { overflow-x: auto; }
40
+ table { border-collapse: collapse; width: 100%; }
41
+ th, td { text-align: left; vertical-align: top; padding: 5px 10px 5px 0; border-bottom: 1px solid var(--line); }
42
+ th { font-weight: 600; color: var(--dim); white-space: nowrap; }
43
+ td.path { overflow-wrap: anywhere; }
44
+ .state { font-weight: 600; white-space: nowrap; }
45
+ .s-up-to-date, .s-unchanged { color: var(--ok); }
46
+ .s-outdated, .s-update, .s-new, .s-adopt, .s-orphan, .s-no-lock { color: var(--warn); }
47
+ .s-drift, .s-collision, .s-orphan-drift, .s-error, .s-missing { color: var(--bad); }
48
+ .note { border-left: 3px solid var(--line); padding: 2px 10px; margin: 8px 0; }
49
+ .note.warn { border-color: var(--warn); }
50
+ .note.bad { border-color: var(--bad); }
51
+ .detail { background: var(--soft); border: 1px solid var(--line); border-radius: 6px; padding: 8px 12px; margin: 6px 0 10px; }
52
+ pre { margin: 6px 0 0; overflow-x: auto; white-space: pre; }
53
+ pre .add { color: var(--add); }
54
+ pre .del { color: var(--del); }
55
+ dl { display: grid; grid-template-columns: max-content 1fr; gap: 2px 14px; margin: 0; }
56
+ dt { color: var(--dim); }
57
+ dd { margin: 0; overflow-wrap: anywhere; }
58
+ ul { margin: 4px 0; padding-left: 18px; }
59
+ code.cmd { background: var(--soft); border: 1px solid var(--line); border-radius: 4px; padding: 1px 6px; user-select: all; }
60
+ `;
61
+ /**
62
+ * How the page lists each file state — a `Record` over every `FileState`, so a new state does not compile until it is placed here,
63
+ * as `FILE_STATE_ORDER_MAP` does for the CLI: `order` on the page, `syncActs` (sync writes or removes it without a flag, so the page
64
+ * offers the command), and `diff` (the core's `diffShows`).
65
+ */
66
+ export const PAGE_STATES = {
67
+ new: { order: 0, syncActs: true, diff: diffShows("new") },
68
+ update: { order: 1, syncActs: true, diff: diffShows("update") },
69
+ adopt: { order: 2, syncActs: true, diff: diffShows("adopt") },
70
+ drift: { order: 3, syncActs: false, diff: diffShows("drift") },
71
+ collision: { order: 4, syncActs: false, diff: diffShows("collision") },
72
+ orphan: { order: 5, syncActs: true, diff: diffShows("orphan") },
73
+ "orphan-drift": { order: 6, syncActs: false, diff: diffShows("orphan-drift") },
74
+ unchanged: { order: 7, syncActs: false, diff: diffShows("unchanged") },
75
+ };
76
+ export const PAGE_JS = String.raw `"use strict";
77
+ (function () {
78
+ var NO_TOKEN = "open the address printed by craftar ui — this tab has no token";
79
+ var KEY = "craftar-ui-token";
80
+ var MAX_DIFF_LINES = 2000;
81
+ var _PS = ${JSON.stringify(PAGE_STATES)};
82
+ var STATES = Object.keys(_PS).sort(function(a, b) { return _PS[a].order - _PS[b].order; });
83
+ /* The states sync acts on without a flag. */
84
+ var SYNC_ACTS = ${JSON.stringify(Object.fromEntries(Object.entries(PAGE_STATES).filter(([, v]) => v.syncActs).map(([k]) => [k, true])))};
85
+ var HAS_DIFF = ${JSON.stringify(Object.fromEntries(Object.entries(PAGE_STATES).map(([k, v]) => [k, v.diff])))};
86
+ var app = document.getElementById("app");
87
+ var table = null;
88
+
89
+ var fromHash = location.hash.slice(1);
90
+ var token = fromHash;
91
+ try {
92
+ if (fromHash) sessionStorage.setItem(KEY, fromHash);
93
+ else token = sessionStorage.getItem(KEY) || "";
94
+ } catch (e) {
95
+ /* storage refused: the token lives for this page load only */
96
+ }
97
+ if (fromHash) history.replaceState(null, "", location.pathname);
98
+
99
+ function el(tag, className, text) {
100
+ var node = document.createElement(tag);
101
+ if (className) node.className = className;
102
+ if (text !== undefined && text !== null) node.textContent = String(text);
103
+ return node;
104
+ }
105
+ function add(parent) {
106
+ for (var i = 1; i < arguments.length; i++) if (arguments[i]) parent.appendChild(arguments[i]);
107
+ return parent;
108
+ }
109
+ function clear(node) {
110
+ while (node.firstChild) node.removeChild(node.firstChild);
111
+ return node;
112
+ }
113
+ function button(label, onClick, className) {
114
+ var b = el("button", className || "", label);
115
+ b.type = "button";
116
+ b.addEventListener("click", onClick);
117
+ return b;
118
+ }
119
+ function list(items, className) {
120
+ var ul = el("ul", className || "");
121
+ items.forEach(function (item) { add(ul, el("li", "", item)); });
122
+ return ul;
123
+ }
124
+ function row(tag, cells) {
125
+ var tr = el("tr");
126
+ cells.forEach(function (cell) {
127
+ var td = el(tag);
128
+ if (cell && cell.nodeType) add(td, cell);
129
+ else td.textContent = cell === null || cell === undefined ? "" : String(cell);
130
+ add(tr, td);
131
+ });
132
+ return tr;
133
+ }
134
+ function grid(head, rows) {
135
+ var t = el("table");
136
+ add(t, add(el("thead"), row("th", head)));
137
+ var body = el("tbody");
138
+ rows.forEach(function (cells) { add(body, row("td", cells)); });
139
+ return add(el("div", "scroll"), add(t, body));
140
+ }
141
+ function note(text, kind) {
142
+ return el("p", "note" + (kind ? " " + kind : ""), text);
143
+ }
144
+ function short(commit) {
145
+ return commit ? commit.slice(0, 7) : "no git";
146
+ }
147
+ /* A path as one word of a command line: as it is when plain; single-quoted for a POSIX path (nothing expands
148
+ inside single quotes); double-quoted, only the quote escaped, otherwise. */
149
+ function shellWord(text) {
150
+ if (text.charAt(0) === "/") return /^[A-Za-z0-9_.,:=@%+\/-]+$/.test(text) ? text : "'" + text.replace(/'/g, "'\\''") + "'";
151
+ if (/^[A-Za-z0-9_.,:=@%+\/\\-]+$/.test(text)) return text;
152
+ return '"' + text.replace(/"/g, '\\"') + '"';
153
+ }
154
+ function forgeLine(forge) {
155
+ return forge.source + (forge.ref ? " (ref " + forge.ref + ")" : "") + " @ " + short(forge.commit);
156
+ }
157
+
158
+ function api(path) {
159
+ return fetch(path, { headers: { Authorization: "Bearer " + token }, cache: "no-store" }).then(function (res) {
160
+ return res.json().then(
161
+ function (body) { return { status: res.status, body: body }; },
162
+ function () { return { status: res.status, body: null }; }
163
+ );
164
+ });
165
+ }
166
+
167
+ /* What the page says for an answer that is not 200. asked: the file's path when the request named one. */
168
+ function problem(into, answer, asked) {
169
+ clear(into);
170
+ if (answer.status === 403) return add(into, note(NO_TOKEN, "bad"));
171
+ if (answer.status === 404) {
172
+ if (asked !== undefined) return add(into, note("craftar does not manage " + asked, "warn"));
173
+ return add(into, note("this workspace is no longer registered", "warn"), button("Refresh", showWorkspaces));
174
+ }
175
+ if (answer.status === 409 && answer.body) {
176
+ add(into, note(String(answer.body.error), "bad"));
177
+ if (answer.body.warnings && answer.body.warnings.length) add(into, list(answer.body.warnings, "dim"));
178
+ return into;
179
+ }
180
+ if (answer.status === 400 && answer.body) return add(into, note(String(answer.body.error), "bad"));
181
+ if (answer.status === 500) return add(into, note("craftar ui failed — see its terminal", "bad"));
182
+ return add(into, note("unexpected answer: " + answer.status, "bad"));
183
+ }
184
+ function unreachable(into) {
185
+ clear(into);
186
+ return add(into, note("craftar ui is not answering — it may have been stopped in its terminal", "bad"));
187
+ }
188
+ function load(path, into, asked, render) {
189
+ add(clear(into), el("p", "dim", "loading…"));
190
+ api(path).then(
191
+ function (answer) {
192
+ if (answer.status !== 200 || answer.body === null) return problem(into, answer, asked);
193
+ clear(into);
194
+ render(answer.body);
195
+ },
196
+ function () { unreachable(into); }
197
+ );
198
+ }
199
+
200
+ /* View 1: the table of craftar workspaces. */
201
+ function showWorkspaces() {
202
+ clear(app);
203
+ var head = add(el("div"), el("h1", "", "craftar — workspaces"));
204
+ var body = el("div");
205
+ add(app, head, add(el("div", "bar"), button("Refresh", showWorkspaces)), body);
206
+ if (!token) return add(clear(body), note(NO_TOKEN, "bad"));
207
+ load("/api/workspaces", body, undefined, function (doc) {
208
+ table = doc;
209
+ add(head, el("p", "dim", "craftar " + doc.version + " · registry " + doc.registry));
210
+ if (!doc.workspaces.length) {
211
+ add(body, note("no workspace is registered on this machine yet — run craftar sync in one"));
212
+ } else {
213
+ add(body, grid(
214
+ ["workspace", "path", "profile", "stack", "targets", "forge", "last sync", "status"],
215
+ doc.workspaces.map(function (w) {
216
+ var stack = Object.keys(w.stack || {}).map(function (k) { return k + " " + w.stack[k]; }).join(", ");
217
+ var state = el("span", "state s-" + w.status, w.status.replace(/-/g, " ") + (w.forgeMoved ? " · forge moved" : ""));
218
+ var path = el("span", "mono", w.path);
219
+ return [button(w.name, function () { showWorkspace(w, "files"); }, "link"), path, w.profile, stack, w.targets.join(", "), forgeLine(w.forge), w.lastSync, state];
220
+ })
221
+ ));
222
+ if (doc.workspaces.some(function (w) { return w.forge.kind === "remote"; })) add(body, el("p", "dim", "status read offline from the Forge cache"));
223
+ }
224
+ if (doc.warnings.length) add(body, el("h3", "", "warnings"), list(doc.warnings));
225
+ });
226
+ }
227
+
228
+ function wordLayer(layer, profile) {
229
+ if (layer === "profile") return "profile " + profile;
230
+ if (typeof layer === "string") return layer;
231
+ if (layer && layer.recipe !== undefined) return "recipe " + layer.recipe;
232
+ if (layer && layer.profile !== undefined) return "profile " + layer.profile;
233
+ return "";
234
+ }
235
+
236
+ function frame(w, tab) {
237
+ clear(app);
238
+ var body = el("div");
239
+ add(
240
+ app,
241
+ add(el("div", "bar"), button("← Workspaces", showWorkspaces)),
242
+ el("h1", "", w.name),
243
+ el("p", "mono dim", w.path),
244
+ add(
245
+ el("div", "bar"),
246
+ button("Files", function () { showWorkspace(w, "files"); }, tab === "files" ? "on" : ""),
247
+ button("Its Forge", function () { showWorkspace(w, "forge"); }, tab === "forge" ? "on" : ""),
248
+ button("Refresh", function () { showWorkspace(w, tab); })
249
+ ),
250
+ body
251
+ );
252
+ return body;
253
+ }
254
+ function cachedLine(into, kind) {
255
+ if (kind === "remote") add(into, el("p", "dim", "read from the cached copy of the Forge — not fetched"));
256
+ }
257
+
258
+ function showWorkspace(w, tab) {
259
+ var body = frame(w, tab);
260
+ if (!token) return add(body, note(NO_TOKEN, "bad"));
261
+ if (tab === "forge") return showForge(w, body);
262
+ load("/api/workspaces/" + w.id, body, undefined, function (doc) {
263
+ cachedLine(body, doc.forge.kind);
264
+ var facts = el("dl");
265
+ [
266
+ ["profile", doc.profile],
267
+ ["recipes", doc.recipes.join(", ")],
268
+ ["targets", doc.targets.join(", ")],
269
+ ["forge", forgeLine(doc.forge)],
270
+ ["lock", doc.forge.lockCommit ? "generated from " + short(doc.forge.lockCommit) : "records no commit"]
271
+ ].forEach(function (pair) { add(facts, el("dt", "", pair[0]), el("dd", "", pair[1])); });
272
+ add(body, facts);
273
+ if (doc.unsetParams.length) {
274
+ add(body, note(doc.refused, "bad"));
275
+ add(body, list(doc.unsetParams.map(function (u) {
276
+ return u.key + " — declared by " + u.declaredBy.join(", ") + "; cited by " + u.citedBy.join(", ");
277
+ })));
278
+ }
279
+ if (doc.warnings.length) add(body, el("h3", "", "warnings"), list(doc.warnings));
280
+ if (!doc.statuses.length) add(body, note("the Forge produces no file for this workspace"));
281
+ STATES.concat(doc.statuses.map(function (s) { return s.state; }).filter(function (s, i, all) { return STATES.indexOf(s) === -1 && all.indexOf(s) === i; })).forEach(function (state) {
282
+ var files = doc.statuses.filter(function (s) { return s.state === state; });
283
+ if (!files.length) return;
284
+ var title = el("h2", "", "");
285
+ add(title, el("span", "state s-" + state, state), document.createTextNode(" · " + files.length));
286
+ add(body, title);
287
+ if (SYNC_ACTS[state] && !doc.unsetParams.length) {
288
+ add(body, add(el("p", "dim", "to apply, run in a terminal: "), el("code", "cmd", "craftar sync --workspace " + shellWord(doc.path))));
289
+ }
290
+ files.forEach(function (f) { add(body, fileRow(w, doc, f)); });
291
+ });
292
+ });
293
+ }
294
+
295
+ function fileRow(w, doc, f) {
296
+ var box = el("div");
297
+ var detail = el("div", "detail");
298
+ detail.hidden = true;
299
+ var bar = add(el("div", "bar"), el("span", "mono", f.path), el("span", "dim", [f.target, f.ingredient].filter(Boolean).join(" · ")));
300
+ var query = "?path=" + encodeURIComponent(f.path);
301
+ if (HAS_DIFF[f.state] !== false) {
302
+ add(bar, button("diff", function () {
303
+ detail.hidden = false;
304
+ load("/api/workspaces/" + w.id + "/diff" + query, detail, f.path, function (d) { showDiff(detail, d); });
305
+ }));
306
+ }
307
+ if (f.state !== "orphan" && f.state !== "orphan-drift") {
308
+ add(bar, button("explain", function () {
309
+ detail.hidden = false;
310
+ load("/api/workspaces/" + w.id + "/explain" + query, detail, f.path, function (x) { showExplain(detail, x); });
311
+ }));
312
+ }
313
+ add(bar, button("close", function () { detail.hidden = true; clear(detail); }));
314
+ return add(box, bar, detail);
315
+ }
316
+
317
+ function showDiff(into, d) {
318
+ if (d.from) add(into, el("p", "mono dim", "--- " + d.from));
319
+ if (d.to) add(into, el("p", "mono dim", "+++ " + d.to));
320
+ if (d.note) add(into, note(d.note, d.hidden ? "warn" : ""));
321
+ if (d.diff === null) {
322
+ if (!d.note) add(into, el("p", "dim", "nothing to show: sync leaves this file as it is"));
323
+ return;
324
+ }
325
+ var lines = d.diff.split("\n");
326
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
327
+ var pre = el("pre");
328
+ lines.slice(0, MAX_DIFF_LINES).forEach(function (line) {
329
+ var kind = line.charAt(0) === "+" ? "add" : line.charAt(0) === "-" ? "del" : "";
330
+ add(pre, el("span", kind, line), document.createTextNode("\n"));
331
+ });
332
+ add(into, pre);
333
+ if (lines.length > MAX_DIFF_LINES) add(into, el("p", "dim", "… " + (lines.length - MAX_DIFF_LINES) + " more lines"));
334
+ }
335
+
336
+ function showExplain(into, x) {
337
+ var dl = el("dl");
338
+ var pairs = [
339
+ ["ingredient", x.ingredient],
340
+ ["target", x.target],
341
+ ["brought by", x.via === null ? "no single recipe" : x.via.join(", ")],
342
+ ["imported from", x.origin ? x.origin.workspace + " · " + x.origin.path : "not imported"],
343
+ ["profile", x.profile],
344
+ ["recipes", x.recipes.join(", ")],
345
+ ["hash", x.hash]
346
+ ];
347
+ x.sections.forEach(function (s) { pairs.push(["section " + s.name, wordLayer(s.layer, x.profile)]); });
348
+ x.params.forEach(function (p) { pairs.push(["param " + p.key, wordLayer(p.layer, x.profile)]); });
349
+ pairs.forEach(function (pair) { add(dl, el("dt", "", pair[0]), el("dd", "mono", pair[1])); });
350
+ add(into, dl);
351
+ }
352
+
353
+ /* View 3: the Forge of one workspace — its recipes, then its ingredients. */
354
+ function showForge(w, body) {
355
+ cachedLine(body, w.forge.kind);
356
+ var recipes = el("div");
357
+ var ingredients = el("div");
358
+ add(body, el("h2", "", "Recipes"), recipes, el("h2", "", "Ingredients"), ingredients);
359
+ load("/api/workspaces/" + w.id + "/recipes", recipes, undefined, function (doc) {
360
+ add(recipes, el("p", "dim", "forge " + doc.forge.name + " @ " + short(doc.forge.commit)));
361
+ add(recipes, grid(
362
+ ["recipe", "slot", "parents", "profiles", "registered workspaces", "here"],
363
+ doc.recipes.map(function (r) {
364
+ var users = !table || w.forge.key === null ? [] : table.workspaces.filter(function (o) {
365
+ return o.forge.key !== null && o.forge.key === w.forge.key && o.recipes.indexOf(r.name) !== -1;
366
+ }).map(function (o) { return o.name; });
367
+ var here = r.removedByWorkspace ? "removed by this workspace" : r.inUse ? "in use (" + r.inUse.by.join(", ") + ")" : "";
368
+ return [r.name, r.slot, r.extends.join(", "), r.profiles.join(", "), users.join(", "), here];
369
+ })
370
+ ));
371
+ if (doc.warnings.length) add(recipes, el("h3", "", "warnings"), list(doc.warnings));
372
+ var said = doc.warnings;
373
+ load("/api/workspaces/" + w.id + "/ingredients", ingredients, undefined, function (ing) {
374
+ add(ingredients, grid(
375
+ ["ingredient", "type", "written as", "recipes", "targets", "here"],
376
+ ing.ingredients.map(function (i) {
377
+ var here = i.disabled ? "disabled by this workspace" : i.inUse ? "in use" : "";
378
+ return [i.ref, i.type, i.outputName, i.recipes.join(", "), i.targets === "*" ? "every target" : i.targets.join(", "), here];
379
+ })
380
+ ));
381
+ var more = ing.warnings.filter(function (w) { return said.indexOf(w) === -1; });
382
+ if (more.length) add(ingredients, el("h3", "", "warnings"), list(more));
383
+ if (ing.missing.length) {
384
+ add(ingredients, el("h3", "", "named by a recipe, not in the Forge"), list(ing.missing.map(function (m) { return m.ref + " — " + m.recipes.join(", "); })));
385
+ }
386
+ });
387
+ });
388
+ }
389
+
390
+ showWorkspaces();
391
+ })();
392
+ `;
393
+ //# sourceMappingURL=page.js.map