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 +5 -2
- package/SKILL.md +29 -12
- package/START-HERE.md +1 -1
- package/examples/gallery/job.mjs +3 -1
- package/lib/brand.js +20 -0
- package/lib/engine.js +2 -2
- package/package.json +1 -1
- package/wand.js +13 -9
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
|
-
|
|
41
|
-
|
|
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-
|
|
3
|
-
description:
|
|
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.
|
|
7
|
+
version: "0.2.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
# Wand
|
|
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@
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
package/examples/gallery/job.mjs
CHANGED
|
@@ -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
|
-
|
|
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 ||
|
|
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.
|
|
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 {
|
|
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",
|
|
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",
|
|
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 {
|
|
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 {
|