@domandigital/craft 0.14.0 → 0.15.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.
@@ -16,7 +16,7 @@ import {
16
16
  protectedFacts,
17
17
  scanSource,
18
18
  visibleText
19
- } from "./chunk-GQVEYTBY.js";
19
+ } from "./chunk-LFNSB6W7.js";
20
20
 
21
21
  // src/character/config.ts
22
22
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -377,12 +377,13 @@ 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 estate add <url | snapshot.json> --id <id> [--client <name>] [--estate <file>]
385
- craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--json] [--strict]
385
+ craft estate add [<url | snapshot.json>] --id <id> [--client <name>] [--direction <art-direction.json>] [--estate <file>]
386
+ craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--directions] [--json] [--strict]
386
387
  craft report <url | snapshot.json> [--repo <dir>] [--null <dir>] [--estate <file>] [--direction <file>] [--json] [--strict]
387
388
  craft retrofit <url | snapshot.json> [the same] [--out RETROFIT.md]
388
389
 
@@ -461,10 +462,11 @@ var VALUE_FLAGS = {
461
462
  "--pages": "pages",
462
463
  "--viewport": "viewport",
463
464
  "--component": "component",
464
- "--competitor": "competitor"
465
+ "--competitor": "competitor",
466
+ "--page": "page"
465
467
  };
466
468
  function parseFlags(args) {
467
- const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false };
469
+ const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false, directions: false, visual: false };
468
470
  for (let i = 0; i < args.length; i += 1) {
469
471
  const a = args[i];
470
472
  if (a === "--json") flags.json = true;
@@ -472,6 +474,8 @@ function parseFlags(args) {
472
474
  else if (a === "--staged") flags.staged = true;
473
475
  else if (a === "--gate") flags.gate = true;
474
476
  else if (a === "--update-baseline") flags.updateBaseline = true;
477
+ else if (a === "--directions") flags.directions = true;
478
+ else if (a === "--visual") flags.visual = true;
475
479
  else if (a in VALUE_FLAGS) {
476
480
  const value = args[i + 1];
477
481
  i += 1;
@@ -576,7 +580,7 @@ async function pageSnapshot(target, cwd) {
576
580
  return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
577
581
  }
578
582
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
579
- const { snapshotUrl } = await import("./audit-6WHR7ZTV.js");
583
+ const { snapshotUrl } = await import("./audit-MTUC24XU.js");
580
584
  return snapshotUrl(target);
581
585
  }
582
586
  async function briefFor(flags, cwd) {
@@ -587,7 +591,7 @@ async function briefFor(flags, cwd) {
587
591
  if (typeof brief === "string" && brief.trim()) return brief;
588
592
  } else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
589
593
  if (flags.null) {
590
- const { loadNull } = await import("./cli-Z2IQG3LC.js");
594
+ const { loadNull } = await import("./cli-FLG3ZCAF.js");
591
595
  return loadNull(flags.null, cwd).brief;
592
596
  }
593
597
  return void 0;
@@ -613,25 +617,25 @@ async function compareWithCompetitor(flags, io) {
613
617
  function run(argv, io) {
614
618
  const [command, ...rest] = argv;
615
619
  if (command === "direction") {
616
- return import("./cli-O3CDSONO.js").then((m) => m.runDirection(rest, io));
620
+ return import("./cli-7OZCYOQY.js").then((m) => m.runDirection(rest, io));
617
621
  }
618
622
  if (command === "report" || command === "retrofit") {
619
- return import("./cli-WG3UJNUF.js").then((m) => m.runReport(command, rest, io));
623
+ return import("./cli-FCKHYAVZ.js").then((m) => m.runReport(command, rest, io));
620
624
  }
621
625
  if (command === "estate") {
622
- return import("./cli-3CGUKXY4.js").then((m) => m.runEstate(rest, io));
626
+ return import("./cli-22TSWAX6.js").then((m) => m.runEstate(rest, io));
623
627
  }
624
628
  if (command === "null") {
625
- return import("./cli-Z2IQG3LC.js").then((m) => m.runNull(rest, io));
629
+ return import("./cli-FLG3ZCAF.js").then((m) => m.runNull(rest, io));
626
630
  }
627
631
  if (command === "tells" && rest[0] === "harvest") {
628
- return import("./cli-Z2IQG3LC.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
632
+ return import("./cli-FLG3ZCAF.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
629
633
  io.err(`craft: ${error.message}`);
630
634
  return 2;
631
635
  });
632
636
  }
633
637
  if (command === "snapshot" || command === "audit") {
634
- return import("./cli-WSHPSWVU.js").then((m) => m.runAudit(command, rest, io));
638
+ return import("./cli-5ACKICBO.js").then((m) => m.runAudit(command, rest, io));
635
639
  }
636
640
  if (!command || command === "help" || command === "--help" || command === "-h") {
637
641
  io.out(HELP);
@@ -0,0 +1,27 @@
1
+ // src/null/generate.ts
2
+ import { spawn } from "child_process";
3
+ import { mkdtempSync, rmSync } from "fs";
4
+ import { tmpdir } from "os";
5
+ import { join } from "path";
6
+ function generate(prompt, model) {
7
+ const bin = process.env.CRAFT_CLAUDE ?? "claude";
8
+ const args = ["-p", "--tools", "", "--no-session-persistence", "--disable-slash-commands", "--setting-sources", "", "--strict-mcp-config", ...model ? ["--model", model] : [], prompt];
9
+ const cwd = mkdtempSync(join(tmpdir(), "craft-null-"));
10
+ return new Promise((done, fail) => {
11
+ const child = spawn(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
12
+ let out = "";
13
+ let err = "";
14
+ child.stdout.on("data", (d) => out += d.toString());
15
+ child.stderr.on("data", (d) => err += d.toString());
16
+ child.on("error", (e) => fail(new Error(`${bin}: ${e.message}. Install Claude Code, or set CRAFT_CLAUDE.`)));
17
+ child.on("close", (code) => {
18
+ rmSync(cwd, { recursive: true, force: true });
19
+ if (code === 0) done(out);
20
+ else fail(new Error(`${bin} exited ${code}: ${(err || out).trim().slice(0, 200)}`));
21
+ });
22
+ });
23
+ }
24
+
25
+ export {
26
+ generate
27
+ };
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  auditSnapshot,
6
6
  fingerprint
7
- } from "./chunk-GQVEYTBY.js";
7
+ } from "./chunk-LFNSB6W7.js";
8
8
  import {
9
9
  SNAPSHOT_VERSION
10
10
  } from "./chunk-QYBAVU3P.js";
@@ -1,15 +1,23 @@
1
+ import {
2
+ compareDirections,
3
+ compareToDeclared,
4
+ formatDirectionMatches,
5
+ summariseDirection,
6
+ validateDirection
7
+ } from "./chunk-YDURAOYR.js";
1
8
  import {
2
9
  loadNull
3
- } from "./chunk-XVBR2UPH.js";
10
+ } from "./chunk-RXXRXV3V.js";
4
11
  import {
5
12
  parseFlags,
6
13
  toJson
7
- } from "./chunk-3OTKMKJQ.js";
14
+ } from "./chunk-MUVFECFC.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-GQVEYTBY.js";
29
+ } from "./chunk-LFNSB6W7.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] <= 0.2;
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-6WHR7ZTV.js");
106
+ const { snapshotUrl } = await import("./audit-MTUC24XU.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.sections ?? []).some((x) => x.role === role)).length;
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,7 +1,10 @@
1
+ import {
2
+ generate
3
+ } from "./chunk-OQXTQQAU.js";
1
4
  import {
2
5
  parseFlags,
3
6
  toJson
4
- } from "./chunk-3OTKMKJQ.js";
7
+ } from "./chunk-MUVFECFC.js";
5
8
  import {
6
9
  readSnapshot
7
10
  } from "./chunk-WUACK2HX.js";
@@ -17,34 +20,14 @@ import {
17
20
  hueSwatch,
18
21
  nullPrompt,
19
22
  scanSource
20
- } from "./chunk-GQVEYTBY.js";
23
+ } from "./chunk-LFNSB6W7.js";
21
24
 
22
25
  // src/null/cli.ts
23
- import { spawn } from "child_process";
24
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
25
- import { tmpdir } from "os";
26
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
26
27
  import { join, relative, resolve } from "path";
27
28
  import { pathToFileURL } from "url";
28
29
  var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
29
30
  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
31
  async function pool(items, size, work) {
49
32
  const queue = [...items];
50
33
  await Promise.all(Array.from({ length: Math.max(1, Math.min(size, queue.length)) }, async () => {
@@ -115,7 +98,7 @@ async function build(flags, io) {
115
98
  const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
116
99
  if (toShoot.length) {
117
100
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
118
- const { snapshotUrls } = await import("./audit-6WHR7ZTV.js");
101
+ const { snapshotUrls } = await import("./audit-MTUC24XU.js");
119
102
  const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
120
103
  results.forEach((r, i) => {
121
104
  const id = toShoot[i];