wand-kit 0.1.0 → 0.2.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/PROMPT.md CHANGED
@@ -37,8 +37,11 @@ strokes) and `art` (which file plays which role). A component asks for a role
37
37
  `tokens.json` and `BRAND.md` differ, `BRAND.md` is right and `tokens.json` has
38
38
  drifted.
39
39
 
40
- The new brand is for internal testing until the FK Roman Standard licence for
41
- documents is settled (`brand-next/fonts/LICENSE-FONTS.md`).
40
+ `current` is the default while the two overlap; a job is on `next` when the
41
+ user asks for the new brand. The default is one constant, `DEFAULT_BRAND` in
42
+ `kit/lib/brand.js`, and `doctor` prints the rule from it. The FK Roman
43
+ Standard licence for documents is still to be confirmed
44
+ (`brand-next/fonts/LICENSE-FONTS.md`).
42
45
 
43
46
  ## 1. Surfaces — locked
44
47
 
package/SKILL.md CHANGED
@@ -1,19 +1,19 @@
1
1
  ---
2
- name: wand-studio
3
- description: Design any Wand sales or marketing material on either brand - a deck, a document, a one-pager, a case study, social cards - as PDF, PowerPoint and one-file HTML, with gates. Use when a .pptx, .docx or .pdf is attached, or branded output is wanted from a brief.
2
+ name: wand-design
3
+ description: Rebrand or create Wand material on the current or the new brand - decks, documents, one-pagers, case studies, social cards. Use when a .pptx, .docx or .pdf is attached, or branded work is wanted.
4
4
  compatibility: Requires Node.js 18+, code execution enabled in claude.ai (Settings > Features), and a Chromium the kit can find (Playwright's cache or the system). No network needed once installed. Python 3 embeds fonts in the .pptx; without it the deck ships with the faces named, not embedded.
5
5
  license: Proprietary - Wand AI
6
6
  metadata:
7
- version: "0.1.0"
7
+ version: "0.2.0"
8
8
  ---
9
9
 
10
10
 
11
- # Wand studio
11
+ # Wand design
12
12
 
13
13
  ## First: find the kit and stand in it
14
14
 
15
15
  1. **Installed from npm** (the usual way): it is at `node_modules/wand-kit/`.
16
- If it is not there yet: `npm install wand-kit@next --no-audit --no-fund`.
16
+ If it is not there yet: `npm install wand-kit@latest --no-audit --no-fund`.
17
17
  2. **Attached to the chat as a zip**: unzip it and enter the folder it makes.
18
18
  3. **You are already inside it**: `wand.js` is next to this file.
19
19
 
@@ -35,9 +35,15 @@ PDF, the PNGs, the PowerPoint and the one-file HTML are all read from it.
35
35
  `PROMPT.md` is the design system: read it once for design decisions, not per
36
36
  page. `node wand.js components` lists the vocabulary.
37
37
 
38
- Two brands: `current` (Geist, the default) and `next` (the website's; for
39
- internal testing until its font licence is settled). Use `next` only when the
40
- user says *new brand*, *new guidelines* or *website look*.
38
+ ## Which brand
39
+
40
+ Two brands, one kit: `current` (Geist) and `next` (the website's). `doctor`
41
+ ends with the brand rule: which one is the default, and the words that ask
42
+ for the other. Follow it as printed. When the user names no brand, build on
43
+ the default without asking; when their words ask for the other, pass
44
+ `--brand` to `new`. One brand per piece, never a mix, and the review note
45
+ says which. The rule lives in the kit, not here, so it can change without
46
+ anyone reinstalling the skill.
41
47
 
42
48
  ## Read only what the job needs
43
49
 
@@ -57,8 +63,8 @@ user says *new brand*, *new guidelines* or *website look*.
57
63
  text runs in order, or use the decks kit's `tools/extract.py` if present.
58
64
  2. **Write the page list in the chat before any markup**: one line per page
59
65
  with the claim it carries and which source lines it takes. Then compose.
60
- 3. `node wand.js new work/<name> --surface letter|slide --brand current`,
61
- write `pages(c)` with `c.L(n)` for every string, declare every departure
66
+ 3. `node wand.js new work/<name> --surface letter|slide` (plus `--brand` only
67
+ when the brand rule says so), write `pages(c)` with `c.L(n)` for every string, declare every departure
62
68
  in `edits` (no em dashes: swap them for commas or semicolons there) and
63
69
  every line left out in `cut`.
64
70
  4. `node wand.js build work/<name>/job.mjs` until every hard gate is ok. Look
@@ -84,10 +90,21 @@ they are gone. Say in the note what was cut and what was generalised.
84
90
  `--surface square` or `og`; one `c.page()` per card; `out/pages/pNN.png` are
85
91
  the deliverables.
86
92
 
93
+ ### Older Wand work
94
+
95
+ A deck or document made before this kit has no `job.mjs`. Treat it as a
96
+ source and rebuild it (the first job above), on the brand the rule gives.
97
+ One exception: a small edit (a date, a word, a figure) to a deck the old
98
+ decks kit built, where they want the file otherwise untouched. For that edit
99
+ only, `npm install wand-decks-kit@0.11.0 --no-audit --no-fund` and follow
100
+ that kit's SKILL.md on editing (`node wand.js show`, then `edit --set`).
101
+ An older copy of this skill may describe that kit, a routing table between
102
+ two kits, or `edit --set`; none of it applies to anything else.
103
+
87
104
  ## The review note, always
88
105
 
89
- What was changed from the source and why (every edit, every cut); the gate
90
- table as printed; what a human still has to decide (a figure to confirm, a
106
+ Which brand it is on; what was changed from the source and why (every edit,
107
+ every cut); the gate table as printed; what a human still has to decide (a figure to confirm, a
91
108
  quote to clear, a claim legal should see). A file delivered without the gate
92
109
  table has been checked by nobody.
93
110
 
package/START-HERE.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ```bash
4
4
  node wand.js doctor # what works here; chromium must name a binary
5
- node wand.js new work/acme --surface letter --brand current
5
+ node wand.js new work/acme --surface letter # the default brand; doctor prints the rule
6
6
  # write work/acme/job.mjs (pages(c)) and, for a rebuild, work/acme/content.txt
7
7
  node wand.js build work/acme/job.mjs # gates, PDF, a PNG per page, out/sheet.png
8
8
  node wand.js ship work/acme/job.mjs --pptx # + one-file HTML, PowerPoint
@@ -4,7 +4,9 @@
4
4
  * It is the regression for the component library: a change to components.js
5
5
  * or to a brand's tokens is reviewed here once, on one sheet, before any job.
6
6
  */
7
- const brand = process.env.WAND_GALLERY_BRAND || "current";
7
+ import { DEFAULT_BRAND } from "../../lib/brand.js";
8
+
9
+ const brand = process.env.WAND_GALLERY_BRAND || DEFAULT_BRAND;
8
10
 
9
11
  export default {
10
12
  name: `Gallery ${brand}`,
package/lib/brand.js CHANGED
@@ -26,6 +26,26 @@ const KIT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
26
26
  const REPO_DIRS = { current: "brand", next: "brand-next" };
27
27
  export const BRANDS = Object.keys(REPO_DIRS);
28
28
 
29
+ // The brand a job gets when the user names none. While the two brands overlap
30
+ // this is the only switch: set it to "next" and publish, and every chat builds
31
+ // on the website's brand unless the user asks for the current one. The skill
32
+ // never states the default; `doctor` prints it from here.
33
+ export const DEFAULT_BRAND = "current";
34
+
35
+ // What a user says when they want each brand, so the rule reads the same
36
+ // whichever way the default points.
37
+ export const ASKED_AS = {
38
+ current: ["current brand", "old brand", "existing guidelines", "Geist"],
39
+ next: ["new brand", "new branding", "new brand guidelines", "the website's look"],
40
+ };
41
+
42
+ /** The default and the words that switch away from it, as one line. */
43
+ export function brandRule() {
44
+ const other = BRANDS.find((b) => b !== DEFAULT_BRAND);
45
+ const words = ASKED_AS[other].map((w) => `"${w}"`);
46
+ return `${DEFAULT_BRAND} by default; ${other} only when the user asks for ${words.slice(0, -1).join(", ")} or ${words.at(-1)}`;
47
+ }
48
+
29
49
  /** Absolute path of a brand directory, or an error naming every place looked. */
30
50
  export function brandDir(name) {
31
51
  if (!REPO_DIRS[name]) throw new Error(`unknown brand "${name}" — one of ${BRANDS.join(", ")}`);
package/lib/engine.js CHANGED
@@ -17,7 +17,7 @@ import path from "node:path";
17
17
  import { pathToFileURL, fileURLToPath } from "node:url";
18
18
  import { createRequire } from "node:module";
19
19
  import { execFileSync, spawnSync } from "node:child_process";
20
- import { loadBrand } from "./brand.js";
20
+ import { loadBrand, DEFAULT_BRAND } from "./brand.js";
21
21
  import { surface as surfaceOf, surfaceCss } from "./surfaces.js";
22
22
  import { bind, COMPONENTS_CSS } from "./components.js";
23
23
  import { loadContent, coverage } from "./content.js";
@@ -179,7 +179,7 @@ function GATES({ min, chars, tokens, families, base }) {
179
179
  export async function build(jobPath, { outputs: wantOutputs = true, outDir = null, quiet = false } = {}) {
180
180
  const t0 = Date.now();
181
181
  const { job, file, dir } = await loadJob(jobPath);
182
- const brand = loadBrand(job.brand || "current", job.theme || null);
182
+ const brand = loadBrand(job.brand || DEFAULT_BRAND, job.theme || null);
183
183
  const surface = surfaceOf(job.surface || "letter");
184
184
  const content = loadContent(dir, job);
185
185
  const c = bind({ brand, surface, job, content });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wand-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "One HTML design surface for every sales and marketing material, on either Wand brand: decks, documents, one-pagers, case studies, social cards. Every export (PDF, PNG, PowerPoint, one-file HTML) is read from the page that was reviewed.",
5
5
  "license": "SEE LICENSE IN PROMPT.md",
6
6
  "bin": {
package/wand.js CHANGED
@@ -2,9 +2,10 @@
2
2
  /**
3
3
  * wand.js — the studio's front door.
4
4
  *
5
- * node wand.js doctor what works here
6
- * node wand.js new <dir> [--surface letter] [--brand current] [--name "Title"]
7
- * a job folder to compose in
5
+ * node wand.js doctor what works here, and the brand rule
6
+ * node wand.js new <dir> [--surface letter] [--brand current|next] [--name "Title"]
7
+ * a job folder to compose in, on the default brand
8
+ * unless --brand names the other
8
9
  * node wand.js build <job.mjs> gates, then the PDF, a PNG per page, the sheet
9
10
  * node wand.js check <job.mjs> gates only
10
11
  * node wand.js ship <job.mjs> [--pptx] [--no-html] everything: build, one-file HTML, PowerPoint
@@ -21,6 +22,7 @@ import path from "node:path";
21
22
  import { fileURLToPath } from "node:url";
22
23
  import { createRequire } from "node:module";
23
24
  import { execFileSync, spawnSync } from "node:child_process";
25
+ import { BRANDS, DEFAULT_BRAND, ASKED_AS, brandRule } from "./lib/brand.js";
24
26
 
25
27
  const require = createRequire(import.meta.url);
26
28
  const KIT = path.dirname(fileURLToPath(import.meta.url));
@@ -33,7 +35,7 @@ const rel = (p) => path.relative(process.cwd(), p) || ".";
33
35
 
34
36
  async function doctor() {
35
37
  const { findChrome, has } = await import("./lib/engine.js");
36
- const { BRANDS, loadBrand } = await import("./lib/brand.js");
38
+ const { loadBrand } = await import("./lib/brand.js");
37
39
  const rows = [];
38
40
  rows.push(["node", process.version, +process.version.slice(1).split(".")[0] >= 18 ? "ok" : "MISSING - Node 18 or newer"]);
39
41
  for (const d of ["playwright-core", "pptxgenjs", "jszip", "fontkit"]) { let s = "ok"; try { require.resolve(d); } catch { s = `MISSING - npm install in ${rel(KIT)}`; } rows.push([d, s === "ok" ? "bundled" : "-", s]); }
@@ -47,13 +49,15 @@ async function doctor() {
47
49
  console.log("");
48
50
  for (const [k, v, s] of rows) console.log(`${s === "ok" ? " " : "! "}${k.padEnd(w)} ${String(v).padEnd(36)}${s === "ok" ? "" : s}`);
49
51
  const blocked = rows.some((r) => r[2].startsWith("MISSING") || r[2].startsWith("no renderer"));
52
+ console.log(`\n brand rule ${brandRule()}`);
50
53
  console.log(blocked ? "\nBLOCKED - fix the rows marked !\n" : "\nready - node wand.js new <dir>, then build <dir>/job.mjs\n");
51
54
  return blocked ? 1 : 0;
52
55
  }
53
56
 
54
57
  function newJob() {
55
58
  const dir = args[1]; if (!dir) die("usage: node wand.js new <dir> [--surface letter|a4|slide|square|og] [--brand current|next] [--name \"Title\"]");
56
- const surface = flag("--surface", "letter"), brand = flag("--brand", "current"), name = flag("--name", path.basename(dir));
59
+ const surface = flag("--surface", "letter"), brand = flag("--brand", DEFAULT_BRAND), name = flag("--name", path.basename(dir));
60
+ if (!BRANDS.includes(brand)) die(`unknown brand "${brand}" - one of ${BRANDS.join(", ")}`);
57
61
  const tpl = path.join(KIT, "templates", `${["slide", "square", "og"].includes(surface) ? (surface === "slide" ? "slide" : "card") : "page"}.mjs`);
58
62
  if (!fs.existsSync(tpl)) die(`no template for surface "${surface}"`);
59
63
  if (fs.existsSync(path.join(dir, "job.mjs"))) die(`${dir}/job.mjs already exists`);
@@ -120,7 +124,7 @@ async function watch(jobPath) {
120
124
  }
121
125
 
122
126
  async function gallery() {
123
- const brand = flag("--brand", "current");
127
+ const brand = flag("--brand", DEFAULT_BRAND);
124
128
  const { build, closeBrowser } = await import("./lib/engine.js");
125
129
  process.env.WAND_GALLERY_BRAND = brand;
126
130
  const r = await build(path.join(KIT, "examples", "gallery", "job.mjs"), {});
@@ -148,9 +152,9 @@ async function components() {
148
152
  }
149
153
 
150
154
  async function brands() {
151
- const { BRANDS, loadBrand } = await import("./lib/brand.js");
152
- for (const b of BRANDS) { const br = loadBrand(b); console.log(`\n ${b.padEnd(8)} ${br.title}\n ${"".padEnd(8)} ${rel(br.dir)} · faces: ${br.families.join(", ")} · ${br.faceFiles.length} files · ${Object.keys(br.tokens.art).length} art roles`); }
153
- console.log("");
155
+ const { loadBrand } = await import("./lib/brand.js");
156
+ for (const b of BRANDS) { const br = loadBrand(b); console.log(`\n ${b.padEnd(8)} ${br.title}${b === DEFAULT_BRAND ? " (default)" : ""}\n ${"".padEnd(8)} ${rel(br.dir)} · faces: ${br.families.join(", ")} · ${br.faceFiles.length} files · ${Object.keys(br.tokens.art).length} art roles\n ${"".padEnd(8)} asked for as: ${ASKED_AS[b].join(", ")}`); }
157
+ console.log(`\n rule: ${brandRule()}\n`);
154
158
  }
155
159
 
156
160
  try {