@domandigital/craft 0.14.0 → 0.16.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/CHANGELOG.md +25 -0
- package/CHARACTER.md +247 -28
- package/README.md +19 -1
- package/art-direction.schema.json +428 -25
- package/dist/{audit-6WHR7ZTV.js → audit-UJCUWORL.js} +2 -2
- package/dist/audit.cjs +5 -0
- package/dist/audit.js +5 -0
- package/dist/{chunk-GQVEYTBY.js → chunk-B2MJLGF6.js} +14 -3
- package/dist/{chunk-PBK6SYH5.js → chunk-CVG5GSMN.js} +46 -11
- package/dist/{chunk-XVBR2UPH.js → chunk-LWVGIJ5A.js} +175 -37
- package/dist/{chunk-3OTKMKJQ.js → chunk-LXDNIJFG.js} +36 -15
- package/dist/{cli-WG3UJNUF.js → chunk-NTIPJ5U7.js} +12 -172
- package/dist/chunk-OQXTQQAU.js +27 -0
- package/dist/{chunk-7T5WR74C.js → chunk-TQKDCA4V.js} +4 -4
- package/dist/chunk-WAV6AKR4.js +711 -0
- package/dist/{chunk-Q7ILL2HV.js → chunk-YK2FFL6Z.js} +1 -1
- package/dist/cli-3GQROFCB.js +241 -0
- package/dist/{cli-WSHPSWVU.js → cli-52M62QBN.js} +6 -5
- package/dist/{cli-Z2IQG3LC.js → cli-AVYNHBFJ.js} +12 -5
- package/dist/cli-CWORJYOU.js +190 -0
- package/dist/{cli-O3CDSONO.js → cli-K4LSRYBE.js} +86 -20
- package/dist/{cli-3CGUKXY4.js → cli-PEATEW57.js} +6 -4
- package/dist/cli.js +2 -2
- package/dist/generate.js +6 -0
- package/dist/index.cjs +1142 -208
- package/dist/index.d.cts +603 -57
- package/dist/index.d.ts +603 -57
- package/dist/index.js +1084 -208
- package/package.json +1 -1
- package/dist/chunk-OOZOEWGD.js +0 -295
|
@@ -1,15 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compareDirections,
|
|
3
|
+
compareToDeclared,
|
|
4
|
+
formatDirectionMatches,
|
|
5
|
+
summariseDirection,
|
|
6
|
+
validateDirection
|
|
7
|
+
} from "./chunk-WAV6AKR4.js";
|
|
1
8
|
import {
|
|
2
9
|
loadNull
|
|
3
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-LWVGIJ5A.js";
|
|
4
11
|
import {
|
|
5
12
|
parseFlags,
|
|
6
13
|
toJson
|
|
7
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-LXDNIJFG.js";
|
|
8
15
|
import {
|
|
9
16
|
readSnapshot
|
|
10
17
|
} from "./chunk-WUACK2HX.js";
|
|
11
18
|
import {
|
|
12
19
|
COMPONENT_ROLES,
|
|
20
|
+
SHARED_PART,
|
|
13
21
|
auditSnapshot,
|
|
14
22
|
closestComponents,
|
|
15
23
|
estateComponentPairs,
|
|
@@ -18,7 +26,7 @@ import {
|
|
|
18
26
|
groundName,
|
|
19
27
|
hueName,
|
|
20
28
|
shapeName
|
|
21
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-B2MJLGF6.js";
|
|
22
30
|
|
|
23
31
|
// src/estate/cli.ts
|
|
24
32
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -37,7 +45,7 @@ function addSite(register, site) {
|
|
|
37
45
|
}
|
|
38
46
|
function sharedParts(a, b, d = fingerprintDistance(a, b)) {
|
|
39
47
|
const out = [];
|
|
40
|
-
const close = (k) => d.parts[k] <=
|
|
48
|
+
const close = (k) => d.parts[k] <= SHARED_PART;
|
|
41
49
|
if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
|
|
42
50
|
if (close("type")) {
|
|
43
51
|
if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
|
|
@@ -52,14 +60,14 @@ function sharedParts(a, b, d = fingerprintDistance(a, b)) {
|
|
|
52
60
|
}
|
|
53
61
|
function compareToEstate(target, register, options = {}) {
|
|
54
62
|
const at = options.siblingAt ?? SIBLING_AT;
|
|
55
|
-
return register.sites.filter((s) => s.id !== options.exclude).map((s) => {
|
|
63
|
+
return register.sites.filter((s) => s.id !== options.exclude && s.fingerprint).map((s) => {
|
|
56
64
|
const d = fingerprintDistance(target, s.fingerprint);
|
|
57
65
|
return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target, s.fingerprint, d) };
|
|
58
66
|
}).sort((a, b) => a.distance - b.distance);
|
|
59
67
|
}
|
|
60
68
|
function estatePairs(register, siblingAt = SIBLING_AT) {
|
|
61
69
|
const out = [];
|
|
62
|
-
const s = register.sites;
|
|
70
|
+
const s = register.sites.filter((site) => site.fingerprint);
|
|
63
71
|
for (let i = 0; i < s.length; i += 1) {
|
|
64
72
|
for (let j = i + 1; j < s.length; j += 1) {
|
|
65
73
|
const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
|
|
@@ -81,7 +89,7 @@ function siblingLine(briefs) {
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
// src/estate/cli.ts
|
|
84
|
-
var USAGE = "usage: craft estate add <url | snapshot.json> --id <id> [--client <name>] | craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--component <role>] [--json] [--strict]";
|
|
92
|
+
var USAGE = "usage: craft estate add [<url | snapshot.json>] --id <id> [--client <name>] [--direction <art-direction.json>] | craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--component <role>] [--directions] [--json] [--strict]";
|
|
85
93
|
function loadEstate(path) {
|
|
86
94
|
if (!existsSync(path)) return emptyEstate();
|
|
87
95
|
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -95,7 +103,7 @@ async function snapshotOf(target, cwd) {
|
|
|
95
103
|
return snap;
|
|
96
104
|
}
|
|
97
105
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
|
|
98
|
-
const { snapshotUrl } = await import("./audit-
|
|
106
|
+
const { snapshotUrl } = await import("./audit-UJCUWORL.js");
|
|
99
107
|
return snapshotUrl(target);
|
|
100
108
|
}
|
|
101
109
|
var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
|
|
@@ -115,7 +123,7 @@ function componentRole(name) {
|
|
|
115
123
|
if (!COMPONENT_ROLES.includes(name)) throw new Error(`--component takes one of ${COMPONENT_ROLES.join(", ")}`);
|
|
116
124
|
return name;
|
|
117
125
|
}
|
|
118
|
-
var withRole = (register, role) => register.sites.filter((s) => (s.fingerprint
|
|
126
|
+
var withRole = (register, role) => register.sites.filter((s) => (s.fingerprint?.sections ?? []).some((x) => x.role === role)).length;
|
|
119
127
|
function formatComponentPairs(role, pairs, measured, sites) {
|
|
120
128
|
const lines = [`craft estate: ${role} components, ${measured} of ${sites} sites have one, closest pairs first (no sibling line yet: ranked, not judged)`];
|
|
121
129
|
for (const p of pairs) lines.push(` ${`${p.a} + ${p.b}`.padEnd(34)} ${p.distance.toFixed(2)}${p.shared.length ? ` shares ${p.shared.join(", ")}` : ""}`);
|
|
@@ -134,6 +142,19 @@ async function runEstate(args, io) {
|
|
|
134
142
|
const file = resolve(io.cwd, flags.estate ?? "estate.json");
|
|
135
143
|
const register = loadEstate(file);
|
|
136
144
|
const at = flags.null ? siblingLine(flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd).runs.map((r) => r.fingerprint))) : SIBLING_AT;
|
|
145
|
+
const directionFile = flags.direction ? JSON.parse(readFileSync(resolve(io.cwd, flags.direction), "utf8")) : void 0;
|
|
146
|
+
if (directionFile && !validateDirection(directionFile).valid) throw new Error("the direction has errors: run craft direction validate before registering it");
|
|
147
|
+
const declared = () => summariseDirection(directionFile);
|
|
148
|
+
if (sub === "add" && !target && flags.id && flags.direction) {
|
|
149
|
+
const existing = register.sites.find((s) => s.id === flags.id);
|
|
150
|
+
const direction = declared();
|
|
151
|
+
const next = addSite(register, { ...existing ?? { id: flags.id, client: flags.client ?? directionFile.client, url: "", tells: [], addedAt: (/* @__PURE__ */ new Date()).toISOString() }, direction });
|
|
152
|
+
writeFileSync(file, `${JSON.stringify(next, null, 2)}
|
|
153
|
+
`);
|
|
154
|
+
io.out(`craft estate: ${flags.id} direction recorded.`);
|
|
155
|
+
io.out(formatDirectionMatches(flags.id, compareToDeclared(direction, next.sites, flags.id)));
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
137
158
|
if (sub === "add") {
|
|
138
159
|
if (!target || !flags.id) throw new Error(USAGE);
|
|
139
160
|
const snap = await snapshotOf(target, io.cwd);
|
|
@@ -144,7 +165,8 @@ async function runEstate(args, io) {
|
|
|
144
165
|
url: snap.url,
|
|
145
166
|
fingerprint: fp,
|
|
146
167
|
tells: [...new Set(auditSnapshot(snap).findings.map((f) => f.tell))].sort(),
|
|
147
|
-
addedAt: snap.capturedAt
|
|
168
|
+
addedAt: snap.capturedAt,
|
|
169
|
+
...flags.direction ? { direction: declared() } : register.sites.find((s) => s.id === flags.id)?.direction ? { direction: register.sites.find((s) => s.id === flags.id).direction } : {}
|
|
148
170
|
});
|
|
149
171
|
writeFileSync(file, `${JSON.stringify(next, null, 2)}
|
|
150
172
|
`);
|
|
@@ -152,6 +174,17 @@ async function runEstate(args, io) {
|
|
|
152
174
|
io.out(formatMatches(flags.id, compareToEstate(fp, next, { exclude: flags.id, siblingAt: at }).slice(0, 3), at));
|
|
153
175
|
return 0;
|
|
154
176
|
}
|
|
177
|
+
if (sub === "compare" && flags.directions) {
|
|
178
|
+
const withDirection = register.sites.filter((s) => s.direction);
|
|
179
|
+
const pairs = withDirection.flatMap((a, i) => withDirection.slice(i + 1).map((b) => ({ a: a.id, b: b.id, ...compareDirections(a.direction, b.direction) })));
|
|
180
|
+
pairs.sort((x, y) => y.shared.length - x.shared.length);
|
|
181
|
+
if (flags.json) io.out(toJson({ sites: withDirection.length, pairs }));
|
|
182
|
+
else {
|
|
183
|
+
io.out(`craft estate: ${withDirection.length} of ${register.sites.length} sites have a declared direction, ${pairs.filter((p) => p.flagged).length} pair(s) worth a look (ranked, not judged)`);
|
|
184
|
+
for (const p of pairs) io.out(` ${`${p.a} + ${p.b}`.padEnd(34)} ${p.flagged ? "LOOK " : " "}${p.shared.length ? `shares ${p.shared.join(", ")}` : "nothing shared"}`);
|
|
185
|
+
}
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
155
188
|
if (sub === "compare" && flags.component) {
|
|
156
189
|
const role = componentRole(flags.component);
|
|
157
190
|
if (!target) {
|
|
@@ -161,9 +194,10 @@ async function runEstate(args, io) {
|
|
|
161
194
|
return 0;
|
|
162
195
|
}
|
|
163
196
|
const known = register.sites.find((s) => s.id === target);
|
|
197
|
+
if (known && !known.fingerprint) throw new Error(`${target} has a declared direction only; add a snapshot to compare rendered components`);
|
|
164
198
|
const fp = known ? known.fingerprint : fingerprint(await snapshotOf(target, io.cwd));
|
|
165
199
|
if (!(fp.sections ?? []).some((x) => x.role === role)) throw new Error(`${known?.id ?? target} has no ${role} section measured (roles need snapshot version 2)`);
|
|
166
|
-
const pairs = register.sites.filter((s) => s.id !== known?.id).flatMap((s) => {
|
|
200
|
+
const pairs = register.sites.filter((s) => s.id !== known?.id && s.fingerprint).flatMap((s) => {
|
|
167
201
|
const c = closestComponents(fp, s.fingerprint, role);
|
|
168
202
|
return c ? [{ a: known?.id ?? target, b: s.id, ...c }] : [];
|
|
169
203
|
}).sort((x, y) => x.distance - y.distance);
|
|
@@ -177,6 +211,7 @@ async function runEstate(args, io) {
|
|
|
177
211
|
return flags.strict && pairs.some((p) => p.sibling) ? 1 : 0;
|
|
178
212
|
}
|
|
179
213
|
const known = register.sites.find((s) => s.id === target);
|
|
214
|
+
if (known && !known.fingerprint) throw new Error(`${target} has a declared direction only; add a snapshot to compare rendered sites`);
|
|
180
215
|
const fp = known ? known.fingerprint : fingerprint(await snapshotOf(target, io.cwd));
|
|
181
216
|
const matches = compareToEstate(fp, register, { exclude: known?.id, siblingAt: at });
|
|
182
217
|
io.out(flags.json ? toJson({ siblingAt: at, matches }) : formatMatches(known?.id ?? target, matches, at));
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generate
|
|
3
|
+
} from "./chunk-OQXTQQAU.js";
|
|
1
4
|
import {
|
|
2
5
|
parseFlags,
|
|
3
6
|
toJson
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-LXDNIJFG.js";
|
|
5
8
|
import {
|
|
6
9
|
readSnapshot
|
|
7
10
|
} from "./chunk-WUACK2HX.js";
|
|
8
11
|
import {
|
|
9
12
|
CATALOGUE_VERSION,
|
|
13
|
+
MIN_RUNS,
|
|
10
14
|
NULL_VERSION,
|
|
11
15
|
auditSnapshot,
|
|
12
16
|
checkCopy,
|
|
@@ -17,34 +21,20 @@ import {
|
|
|
17
21
|
hueSwatch,
|
|
18
22
|
nullPrompt,
|
|
19
23
|
scanSource
|
|
20
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-B2MJLGF6.js";
|
|
21
25
|
|
|
22
26
|
// src/null/cli.ts
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { join, relative, resolve } from "path";
|
|
27
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
28
|
+
import { createServer } from "http";
|
|
29
|
+
import { extname, join, relative, resolve, sep } from "path";
|
|
27
30
|
import { pathToFileURL } from "url";
|
|
28
31
|
var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
+
var NULL_USAGE = [
|
|
33
|
+
'usage: craft null build --brief "<text>" --out <dir> [--runs 20] [--parallel 4] [--model <name>]',
|
|
34
|
+
' craft null import <dir> --builder <name> --brief "<text>" [--out <dir>] [--model <name>]',
|
|
35
|
+
' craft null prompt --brief "<text>"'
|
|
36
|
+
].join("\n");
|
|
29
37
|
var pad = (n) => String(n).padStart(2, "0");
|
|
30
|
-
function generate(prompt, model) {
|
|
31
|
-
const bin = process.env.CRAFT_CLAUDE ?? "claude";
|
|
32
|
-
const args = ["-p", "--tools", "", "--no-session-persistence", "--disable-slash-commands", "--setting-sources", "", "--strict-mcp-config", ...model ? ["--model", model] : [], prompt];
|
|
33
|
-
const cwd = mkdtempSync(join(tmpdir(), "craft-null-"));
|
|
34
|
-
return new Promise((done, fail) => {
|
|
35
|
-
const child = spawn(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
36
|
-
let out = "";
|
|
37
|
-
let err = "";
|
|
38
|
-
child.stdout.on("data", (d) => out += d.toString());
|
|
39
|
-
child.stderr.on("data", (d) => err += d.toString());
|
|
40
|
-
child.on("error", (e) => fail(new Error(`${bin}: ${e.message}. Install Claude Code, or set CRAFT_CLAUDE.`)));
|
|
41
|
-
child.on("close", (code) => {
|
|
42
|
-
rmSync(cwd, { recursive: true, force: true });
|
|
43
|
-
if (code === 0) done(out);
|
|
44
|
-
else fail(new Error(`${bin} exited ${code}: ${(err || out).trim().slice(0, 200)}`));
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
38
|
async function pool(items, size, work) {
|
|
49
39
|
const queue = [...items];
|
|
50
40
|
await Promise.all(Array.from({ length: Math.max(1, Math.min(size, queue.length)) }, async () => {
|
|
@@ -55,6 +45,15 @@ function nullPath(target, cwd) {
|
|
|
55
45
|
const abs = resolve(cwd, target);
|
|
56
46
|
return existsSync(abs) && statSync(abs).isDirectory() ? join(abs, "null.json") : abs;
|
|
57
47
|
}
|
|
48
|
+
function expandNullTargets(targets, cwd) {
|
|
49
|
+
return targets.flatMap((t) => {
|
|
50
|
+
const abs = resolve(cwd, t);
|
|
51
|
+
if (existsSync(abs) && statSync(abs).isDirectory() && !existsSync(join(abs, "null.json"))) {
|
|
52
|
+
return readdirSync(abs).sort().map((d) => join(abs, d, "null.json")).filter((p) => existsSync(p));
|
|
53
|
+
}
|
|
54
|
+
return [t];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
58
57
|
function loadNull(target, cwd) {
|
|
59
58
|
const path = nullPath(target, cwd);
|
|
60
59
|
if (!existsSync(path)) throw new Error(`no null model at ${target}. Build one with craft null build.`);
|
|
@@ -62,10 +61,11 @@ function loadNull(target, cwd) {
|
|
|
62
61
|
if (model.version !== NULL_VERSION) throw new Error(`${target} is null model version ${String(model.version)}; this craft reads ${NULL_VERSION}`);
|
|
63
62
|
return model;
|
|
64
63
|
}
|
|
65
|
-
function measureRun(id, html, snapshot) {
|
|
64
|
+
function measureRun(id, html, snapshot, styles = []) {
|
|
66
65
|
const file = { path: `${id}.html`, text: html };
|
|
67
|
-
const tells = new Set([...auditSnapshot(snapshot).findings, ...scanSource([file]).findings].map((f) => f.tell));
|
|
68
|
-
|
|
66
|
+
const tells = new Set([...auditSnapshot(snapshot).findings, ...scanSource([file, ...styles]).findings].map((f) => f.tell));
|
|
67
|
+
let copy = [...new Set(extractStrings(file).filter((b) => !b.literal).map((b) => b.text.replace(/\s+/g, " ").trim()).filter(Boolean))];
|
|
68
|
+
if (copy.length === 0) copy = [...new Set(snapshot.sections.flatMap((s) => s.text ?? []).map((t) => t.replace(/\s+/g, " ").trim()).filter(Boolean))];
|
|
69
69
|
return { id, fingerprint: fingerprint(snapshot), tells: [...tells].sort(), copy };
|
|
70
70
|
}
|
|
71
71
|
async function build(flags, io) {
|
|
@@ -115,7 +115,7 @@ async function build(flags, io) {
|
|
|
115
115
|
const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
|
|
116
116
|
if (toShoot.length) {
|
|
117
117
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
|
|
118
|
-
const { snapshotUrls } = await import("./audit-
|
|
118
|
+
const { snapshotUrls } = await import("./audit-UJCUWORL.js");
|
|
119
119
|
const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
|
|
120
120
|
results.forEach((r, i) => {
|
|
121
121
|
const id = toShoot[i];
|
|
@@ -144,6 +144,142 @@ async function build(flags, io) {
|
|
|
144
144
|
}
|
|
145
145
|
return 0;
|
|
146
146
|
}
|
|
147
|
+
function findImportPages(dir) {
|
|
148
|
+
const pages = [];
|
|
149
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
150
|
+
if (e.isFile() && /\.html?$/i.test(e.name)) pages.push({ id: e.name.replace(/\.html?$/i, ""), root: dir, entry: e.name });
|
|
151
|
+
else if (e.isDirectory() && existsSync(join(dir, e.name, "index.html"))) pages.push({ id: e.name, root: join(dir, e.name), entry: "index.html" });
|
|
152
|
+
}
|
|
153
|
+
const ids = pages.map((p) => p.id);
|
|
154
|
+
const twice = ids.find((id, i) => ids.indexOf(id) !== i);
|
|
155
|
+
if (twice) throw new Error(`"${twice}" is both a file and a folder; rename one`);
|
|
156
|
+
return pages.sort((a, b) => a.id.localeCompare(b.id));
|
|
157
|
+
}
|
|
158
|
+
var TYPES = {
|
|
159
|
+
".html": "text/html",
|
|
160
|
+
".htm": "text/html",
|
|
161
|
+
".css": "text/css",
|
|
162
|
+
".js": "text/javascript",
|
|
163
|
+
".mjs": "text/javascript",
|
|
164
|
+
".json": "application/json",
|
|
165
|
+
".svg": "image/svg+xml",
|
|
166
|
+
".png": "image/png",
|
|
167
|
+
".jpg": "image/jpeg",
|
|
168
|
+
".jpeg": "image/jpeg",
|
|
169
|
+
".webp": "image/webp",
|
|
170
|
+
".avif": "image/avif",
|
|
171
|
+
".gif": "image/gif",
|
|
172
|
+
".ico": "image/x-icon",
|
|
173
|
+
".woff": "font/woff",
|
|
174
|
+
".woff2": "font/woff2",
|
|
175
|
+
".ttf": "font/ttf",
|
|
176
|
+
".otf": "font/otf"
|
|
177
|
+
};
|
|
178
|
+
function serveDir(root) {
|
|
179
|
+
const server = createServer((req, res) => {
|
|
180
|
+
let file;
|
|
181
|
+
try {
|
|
182
|
+
file = resolve(root, `.${decodeURIComponent(new URL(req.url ?? "/", "http://local").pathname)}`);
|
|
183
|
+
} catch {
|
|
184
|
+
res.statusCode = 400;
|
|
185
|
+
return void res.end();
|
|
186
|
+
}
|
|
187
|
+
if (file !== root && !file.startsWith(root + sep)) {
|
|
188
|
+
res.statusCode = 403;
|
|
189
|
+
return void res.end();
|
|
190
|
+
}
|
|
191
|
+
if (existsSync(file) && statSync(file).isDirectory()) file = join(file, "index.html");
|
|
192
|
+
if (!existsSync(file)) {
|
|
193
|
+
res.statusCode = 404;
|
|
194
|
+
return void res.end();
|
|
195
|
+
}
|
|
196
|
+
res.setHeader("content-type", TYPES[extname(file).toLowerCase()] ?? "application/octet-stream");
|
|
197
|
+
res.end(readFileSync(file));
|
|
198
|
+
});
|
|
199
|
+
return new Promise((done, fail) => {
|
|
200
|
+
server.once("error", fail);
|
|
201
|
+
server.listen(
|
|
202
|
+
0,
|
|
203
|
+
"127.0.0.1",
|
|
204
|
+
() => done({
|
|
205
|
+
base: `http://127.0.0.1:${server.address().port}`,
|
|
206
|
+
close: () => new Promise((closed) => server.close(() => closed()))
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function stylesheets(root, id) {
|
|
212
|
+
const found = [];
|
|
213
|
+
const walk = (dir) => {
|
|
214
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
215
|
+
const path = join(dir, e.name);
|
|
216
|
+
if (e.isDirectory() && e.name !== "node_modules" && !e.name.startsWith(".")) walk(path);
|
|
217
|
+
else if (e.isFile() && e.name.endsWith(".css") && statSync(path).size <= 1e6) found.push({ path: `${id}/${relative(root, path)}`, text: readFileSync(path, "utf8") });
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
walk(root);
|
|
221
|
+
return found;
|
|
222
|
+
}
|
|
223
|
+
async function importPages(flags, io) {
|
|
224
|
+
const usage = 'usage: craft null import <dir> --builder <name> --brief "<who, where, what they do>" [--out <dir>] [--model <name>]';
|
|
225
|
+
const from = flags.positional[1];
|
|
226
|
+
const brief = flags.brief?.trim();
|
|
227
|
+
if (!from || flags.positional.length > 2 || !flags.builder || !brief || brief.split(/\s+/).length < 5) throw new Error(usage);
|
|
228
|
+
const dir = resolve(io.cwd, from);
|
|
229
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new Error(`no directory at ${from}`);
|
|
230
|
+
const pages = findImportPages(dir);
|
|
231
|
+
if (pages.length < MIN_RUNS) throw new Error(`${from} holds ${pages.length} page${pages.length === 1 ? "" : "s"}; a null model needs at least ${MIN_RUNS}`);
|
|
232
|
+
const out = resolve(io.cwd, flags.out ?? from);
|
|
233
|
+
const shots = join(out, "snapshots");
|
|
234
|
+
mkdirSync(shots, { recursive: true });
|
|
235
|
+
const failed = [];
|
|
236
|
+
const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
|
|
237
|
+
if (toShoot.length) {
|
|
238
|
+
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
|
|
239
|
+
const { snapshotUrls } = await import("./audit-UJCUWORL.js");
|
|
240
|
+
const servers = /* @__PURE__ */ new Map();
|
|
241
|
+
try {
|
|
242
|
+
for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
|
|
243
|
+
const urls = toShoot.map((p) => `${servers.get(p.root)?.base}/${p.entry === "index.html" ? "" : encodeURIComponent(p.entry)}`);
|
|
244
|
+
const results = await snapshotUrls(urls);
|
|
245
|
+
results.forEach((r, i) => {
|
|
246
|
+
const p = toShoot[i];
|
|
247
|
+
if (r.snapshot) writeFileSync(join(shots, `${p.id}.json`), `${JSON.stringify({ ...r.snapshot, url: relative(out, join(p.root, p.entry)) }, null, 2)}
|
|
248
|
+
`);
|
|
249
|
+
else failed.push(`${p.id}: the page would not render: ${r.error}`);
|
|
250
|
+
});
|
|
251
|
+
} finally {
|
|
252
|
+
await Promise.all([...servers.values()].map((s) => s.close()));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const measured = pages.filter((p) => existsSync(join(shots, `${p.id}.json`))).map(
|
|
256
|
+
(p) => measureRun(
|
|
257
|
+
p.id,
|
|
258
|
+
readFileSync(join(p.root, p.entry), "utf8"),
|
|
259
|
+
readSnapshot(readJson(join(shots, `${p.id}.json`)), `${p.id}.json`),
|
|
260
|
+
p.root === dir ? [] : stylesheets(p.root, p.id)
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
if (measured.length < MIN_RUNS) {
|
|
264
|
+
for (const f of failed) io.err(` ${f}`);
|
|
265
|
+
throw new Error(`only ${measured.length} of ${pages.length} pages rendered; a null model needs at least ${MIN_RUNS}`);
|
|
266
|
+
}
|
|
267
|
+
const model = {
|
|
268
|
+
version: NULL_VERSION,
|
|
269
|
+
brief,
|
|
270
|
+
prompt: nullPrompt(brief),
|
|
271
|
+
model: flags.model ?? "unknown",
|
|
272
|
+
builder: flags.builder,
|
|
273
|
+
catalogueVersion: CATALOGUE_VERSION,
|
|
274
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
275
|
+
runs: measured
|
|
276
|
+
};
|
|
277
|
+
writeFileSync(join(out, "null.json"), `${JSON.stringify(model, null, 2)}
|
|
278
|
+
`);
|
|
279
|
+
io.out(`craft null: ${measured.length} of ${pages.length} pages from ${flags.builder} in ${relative(io.cwd, join(out, "null.json")) || "null.json"}`);
|
|
280
|
+
for (const f of failed) io.err(` ${f}`);
|
|
281
|
+
return measured.length < pages.length ? 1 : 0;
|
|
282
|
+
}
|
|
147
283
|
function copyTell(phrase) {
|
|
148
284
|
const text = `We found that ${phrase} was there.`;
|
|
149
285
|
const hit = checkCopy([{ path: "phrase.md", text }]).findings[0];
|
|
@@ -179,7 +315,13 @@ async function runNull(args, io) {
|
|
|
179
315
|
try {
|
|
180
316
|
const flags = parseFlags(args);
|
|
181
317
|
if (typeof flags === "string") throw new Error(flags);
|
|
182
|
-
if (flags.positional[0]
|
|
318
|
+
if (flags.positional[0] === "import") return await importPages(flags, io);
|
|
319
|
+
if (flags.positional[0] === "prompt") {
|
|
320
|
+
if (!flags.brief || flags.brief.trim().split(/\s+/).length < 5) throw new Error('usage: craft null prompt --brief "<who, where, what they do>"');
|
|
321
|
+
io.out(nullPrompt(flags.brief));
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
if (flags.positional[0] !== "build") throw new Error(NULL_USAGE);
|
|
183
325
|
return await build(flags, io);
|
|
184
326
|
} catch (error) {
|
|
185
327
|
io.err(`craft: ${error.message}`);
|
|
@@ -191,14 +333,7 @@ function runHarvest(args, io) {
|
|
|
191
333
|
if (typeof flags === "string") throw new Error(flags);
|
|
192
334
|
const targets = flags.positional.slice(1);
|
|
193
335
|
if (targets.length === 0) throw new Error("usage: craft tells harvest <null.json | dir>... [--share 0.25] [--json]");
|
|
194
|
-
const
|
|
195
|
-
const abs = resolve(io.cwd, t);
|
|
196
|
-
if (existsSync(abs) && statSync(abs).isDirectory() && !existsSync(join(abs, "null.json"))) {
|
|
197
|
-
return readdirSync(abs).map((d) => join(abs, d, "null.json")).filter((p) => existsSync(p));
|
|
198
|
-
}
|
|
199
|
-
return [t];
|
|
200
|
-
});
|
|
201
|
-
const models = paths.map((p) => loadNull(p, io.cwd));
|
|
336
|
+
const models = expandNullTargets(targets, io.cwd).map((p) => loadNull(p, io.cwd));
|
|
202
337
|
const minShare = flags.share === void 0 ? void 0 : Number(flags.share);
|
|
203
338
|
if (minShare !== void 0 && !(minShare > 0 && minShare <= 1)) throw new Error("--share needs a number above 0 and at most 1");
|
|
204
339
|
const candidates = harvest(models, { minShare, copyTell });
|
|
@@ -208,8 +343,11 @@ function runHarvest(args, io) {
|
|
|
208
343
|
|
|
209
344
|
export {
|
|
210
345
|
nullPath,
|
|
346
|
+
expandNullTargets,
|
|
211
347
|
loadNull,
|
|
212
348
|
measureRun,
|
|
349
|
+
findImportPages,
|
|
350
|
+
serveDir,
|
|
213
351
|
formatHarvest,
|
|
214
352
|
describeTypicality,
|
|
215
353
|
runNull,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
protectedFacts,
|
|
17
17
|
scanSource,
|
|
18
18
|
visibleText
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-B2MJLGF6.js";
|
|
20
20
|
|
|
21
21
|
// src/character/config.ts
|
|
22
22
|
var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
|
|
@@ -377,14 +377,18 @@ Usage
|
|
|
377
377
|
craft snapshot <url> [--out <file>] [--width <px>] [--height <px>]
|
|
378
378
|
craft audit <url | snapshot.json> [--repo <dir>] [--null <null.json>] [--out <file>] [--json] [--strict] [ci options]
|
|
379
379
|
craft audit --pages <sitemap.xml | urls.txt> [--viewport 390,768,1440] [the same]
|
|
380
|
-
craft direction init [--snapshot <file>] [--client <name>] [--out <file>]
|
|
381
|
-
craft direction validate [--snapshot <file>] [--direction <file>] [--json]
|
|
380
|
+
craft direction init [--snapshot <file | url>] [--client <name>] [--brief "<text>"] [--out <file>]
|
|
381
|
+
craft direction validate [--snapshot <file | url>] [--page home] [--estate <estate.json>] [--direction <file>] [--strict] [--json]
|
|
382
|
+
craft direction research [--brief "<who, where, what>"] [--visual]
|
|
382
383
|
craft direction propose [--snapshot <file>] [--estate <estate.json | dir>] [--out <file>]
|
|
383
384
|
craft null build --brief "<text>" --out <dir> [--runs 20] [--parallel 4] [--model <name>]
|
|
384
|
-
craft
|
|
385
|
-
craft
|
|
385
|
+
craft null import <dir> --builder <name> --brief "<text>" [--out <dir>] [--model <name>]
|
|
386
|
+
craft null prompt --brief "<text>"
|
|
387
|
+
craft estate add [<url | snapshot.json>] --id <id> [--client <name>] [--direction <art-direction.json>] [--estate <file>]
|
|
388
|
+
craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--directions] [--json] [--strict]
|
|
386
389
|
craft report <url | snapshot.json> [--repo <dir>] [--null <dir>] [--estate <file>] [--direction <file>] [--json] [--strict]
|
|
387
390
|
craft retrofit <url | snapshot.json> [the same] [--out RETROFIT.md]
|
|
391
|
+
craft calibrate <labels.json> [--out <dir>] [--fresh] [--json]
|
|
388
392
|
|
|
389
393
|
scan Markup, component code and stylesheets (source and compiled CSS).
|
|
390
394
|
--staged reads the git index, for a pre-commit hook.
|
|
@@ -418,6 +422,10 @@ direction The site's art-direction.json: every choice with a reason from the
|
|
|
418
422
|
sources' PNG photos and drafts choices for a person to confirm.
|
|
419
423
|
null The counterfactual: about 20 pages \`claude -p\` builds from the brief
|
|
420
424
|
alone, snapshotted and fingerprinted into null.json. Resumable.
|
|
425
|
+
import does the same for pages another builder made (v0, Lovable):
|
|
426
|
+
each an HTML file, or a folder holding a built index.html, served
|
|
427
|
+
on loopback so its asset paths resolve. prompt prints what to give
|
|
428
|
+
the builder, so its pages answer the same brief. Resumable.
|
|
421
429
|
estate The register of shipped sites (estate.json). add fingerprints a site;
|
|
422
430
|
compare lists the nearest, and marks siblings: two sites closer than
|
|
423
431
|
two pages Claude builds for one brief. --strict exits 1 on a sibling.
|
|
@@ -426,6 +434,11 @@ report Every signal for one site: tells, typicality, the estate, the reasons.
|
|
|
426
434
|
Anything not given is "not measured", and never counts as a pass.
|
|
427
435
|
retrofit The report as a checklist: decide, then change type, colour, shape,
|
|
428
436
|
effects, motion and copy. Never the page grammar.
|
|
437
|
+
calibrate How well the catalogue separates a labelled set (ai, human,
|
|
438
|
+
ai-looking): precision and recall per tell, and CHARACTER.md's
|
|
439
|
+
first three targets. --out keeps results.json and any snapshots it
|
|
440
|
+
took; --fresh takes every snapshot again. A page not measured is
|
|
441
|
+
listed, and never counts as a pass. A measure: always exit 0.
|
|
429
442
|
|
|
430
443
|
ci options (scan, copy, audit)
|
|
431
444
|
--baseline <file> Report only findings the baseline does not already
|
|
@@ -461,10 +474,12 @@ var VALUE_FLAGS = {
|
|
|
461
474
|
"--pages": "pages",
|
|
462
475
|
"--viewport": "viewport",
|
|
463
476
|
"--component": "component",
|
|
464
|
-
"--competitor": "competitor"
|
|
477
|
+
"--competitor": "competitor",
|
|
478
|
+
"--page": "page",
|
|
479
|
+
"--builder": "builder"
|
|
465
480
|
};
|
|
466
481
|
function parseFlags(args) {
|
|
467
|
-
const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false };
|
|
482
|
+
const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false, directions: false, visual: false, fresh: false };
|
|
468
483
|
for (let i = 0; i < args.length; i += 1) {
|
|
469
484
|
const a = args[i];
|
|
470
485
|
if (a === "--json") flags.json = true;
|
|
@@ -472,6 +487,9 @@ function parseFlags(args) {
|
|
|
472
487
|
else if (a === "--staged") flags.staged = true;
|
|
473
488
|
else if (a === "--gate") flags.gate = true;
|
|
474
489
|
else if (a === "--update-baseline") flags.updateBaseline = true;
|
|
490
|
+
else if (a === "--directions") flags.directions = true;
|
|
491
|
+
else if (a === "--visual") flags.visual = true;
|
|
492
|
+
else if (a === "--fresh") flags.fresh = true;
|
|
475
493
|
else if (a in VALUE_FLAGS) {
|
|
476
494
|
const value = args[i + 1];
|
|
477
495
|
i += 1;
|
|
@@ -576,7 +594,7 @@ async function pageSnapshot(target, cwd) {
|
|
|
576
594
|
return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
|
|
577
595
|
}
|
|
578
596
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
|
|
579
|
-
const { snapshotUrl } = await import("./audit-
|
|
597
|
+
const { snapshotUrl } = await import("./audit-UJCUWORL.js");
|
|
580
598
|
return snapshotUrl(target);
|
|
581
599
|
}
|
|
582
600
|
async function briefFor(flags, cwd) {
|
|
@@ -587,7 +605,7 @@ async function briefFor(flags, cwd) {
|
|
|
587
605
|
if (typeof brief === "string" && brief.trim()) return brief;
|
|
588
606
|
} else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
|
|
589
607
|
if (flags.null) {
|
|
590
|
-
const { loadNull } = await import("./cli-
|
|
608
|
+
const { loadNull } = await import("./cli-AVYNHBFJ.js");
|
|
591
609
|
return loadNull(flags.null, cwd).brief;
|
|
592
610
|
}
|
|
593
611
|
return void 0;
|
|
@@ -613,25 +631,28 @@ async function compareWithCompetitor(flags, io) {
|
|
|
613
631
|
function run(argv, io) {
|
|
614
632
|
const [command, ...rest] = argv;
|
|
615
633
|
if (command === "direction") {
|
|
616
|
-
return import("./cli-
|
|
634
|
+
return import("./cli-K4LSRYBE.js").then((m) => m.runDirection(rest, io));
|
|
617
635
|
}
|
|
618
636
|
if (command === "report" || command === "retrofit") {
|
|
619
|
-
return import("./cli-
|
|
637
|
+
return import("./cli-CWORJYOU.js").then((m) => m.runReport(command, rest, io));
|
|
620
638
|
}
|
|
621
639
|
if (command === "estate") {
|
|
622
|
-
return import("./cli-
|
|
640
|
+
return import("./cli-PEATEW57.js").then((m) => m.runEstate(rest, io));
|
|
623
641
|
}
|
|
624
642
|
if (command === "null") {
|
|
625
|
-
return import("./cli-
|
|
643
|
+
return import("./cli-AVYNHBFJ.js").then((m) => m.runNull(rest, io));
|
|
644
|
+
}
|
|
645
|
+
if (command === "calibrate") {
|
|
646
|
+
return import("./cli-3GQROFCB.js").then((m) => m.runCalibrate(rest, io));
|
|
626
647
|
}
|
|
627
648
|
if (command === "tells" && rest[0] === "harvest") {
|
|
628
|
-
return import("./cli-
|
|
649
|
+
return import("./cli-AVYNHBFJ.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
|
|
629
650
|
io.err(`craft: ${error.message}`);
|
|
630
651
|
return 2;
|
|
631
652
|
});
|
|
632
653
|
}
|
|
633
654
|
if (command === "snapshot" || command === "audit") {
|
|
634
|
-
return import("./cli-
|
|
655
|
+
return import("./cli-52M62QBN.js").then((m) => m.runAudit(command, rest, io));
|
|
635
656
|
}
|
|
636
657
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
637
658
|
io.out(HELP);
|