@the-smithy/create-hearth-site 0.26.0 → 0.27.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/README.md CHANGED
@@ -40,7 +40,8 @@ cd acme-website
40
40
 
41
41
  Add an API key to `.env.local` (Hearth → the site → API & webhooks → API keys), and run `pnpm dev`. The
42
42
  new site's README walks through the first run, previews and troubleshooting. Its `HOSTING.md` covers deploying to
43
- Netlify or Vercel, and its `AGENTS.md` holds the instructions for coding agents.
43
+ Netlify or Vercel, and its `AGENTS.md` holds the instructions for coding agents, who are set up to find code with
44
+ [Drift](https://github.com/The-Smithy/drift).
44
45
 
45
46
  ## Documentation
46
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-smithy/create-hearth-site",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Start a new client website powered by Hearth: pnpm create @the-smithy/hearth-site",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -30,7 +30,7 @@
30
30
  ],
31
31
  "devDependencies": {
32
32
  "vitest": "^5.0.3",
33
- "@the-smithy/hearth-schema": "^0.26.0"
33
+ "@the-smithy/hearth-schema": "^0.27.0"
34
34
  },
35
35
  "scripts": {
36
36
  "test": "node --test \"test/**/*.test.js\" && vitest run test/blocks.test.ts test/overrides.test.ts"
@@ -4,6 +4,26 @@ This is a client website whose content comes from Hearth, The Smithy's headless
4
4
  replaced by `pnpm hearth:update`: notes about this particular site go in `SITE.md`, which you should read too.
5
5
  `CLAUDE.md` and `GEMINI.md` only point here, for the assistants that look for those names.
6
6
 
7
+ <!-- drift:start -->
8
+ ## Finding code: use Drift first
9
+
10
+ Before grepping or reading whole files, use [Drift](https://github.com/The-Smithy/drift). It costs far fewer tokens.
11
+
12
+ - `drift query <symbol>` or `drift find <words>`: where something is defined
13
+ - `drift show <symbol>` or `drift sig <symbol>`: its source, or just its signature
14
+ - `drift outline <file>`: a file's definitions and imports
15
+ - `drift refs <symbol>`, `drift importers <file>`, `drift tests --for <file>`: what uses it
16
+ - `drift context <symbol or file>`: all of the above before changing something
17
+ - `drift map`: orientation in a new repo
18
+
19
+ Never read `.drift-index.json`. Grep is still right for strings and comments. If `drift` isn't installed: `cargo install --git https://github.com/The-Smithy/drift`.
20
+ <!-- drift:end -->
21
+
22
+ Here, `pnpm hearth:drift` is the way to install or update Drift and its skill: it gets the latest release from GitHub,
23
+ without Rust or an API key, and `pnpm hearth:docs` runs it too. Drift is only for working on the code. The build and
24
+ deploy don't use it, so don't add it to `package.json` or the host's build, and if it can't be installed here, carry on
25
+ with grep.
26
+
7
27
  ## How this site is built: Hearth's code, the site's styling
8
28
 
9
29
  Everything that works with Hearth is already here, and it's the same in every site Hearth runs: every block type,
@@ -139,8 +159,8 @@ Do these before writing code, and tell the developer what you found:
139
159
  fix `.env.local`; don't guess values.
140
160
  2. **Get the full reference:** `pnpm hearth:docs` saves Hearth's API guide (every block and its fields, events,
141
161
  forms, previews, SEO) to `hearth-api.md`. Read the parts you need from it, and from
142
- `node_modules/@the-smithy/hearth/llms.md` for the SDK. It also installs any skills Hearth offers this site (see
143
- "Skills" below), so run it again when they're missing. These aren't committed, so some assistants' search skips
162
+ `node_modules/@the-smithy/hearth/llms.md` for the SDK. It also installs Drift and any skills Hearth offers this site
163
+ (see "Skills" below), so run it again when they're missing. These aren't committed, so some assistants' search skips
144
164
  them: open them by their exact path, or from the terminal if your tool won't.
145
165
  3. **"Nothing is published"** means every page is a 404 until editors publish in Hearth. That's not a code
146
166
  problem: say so, and don't add placeholder content to work around it.
@@ -153,7 +173,7 @@ and, as the same copies, in `.claude/skills/`, since Claude Code only reads that
153
173
  for and when to use it. If your assistant doesn't load them by itself, read the ones that fit the task and follow them.
154
174
  Use them for that work, design work in particular, rather than starting from scratch. They aren't committed (a
155
175
  `.gitignore` in each folder lists them), so a fresh checkout gets them by running `pnpm hearth:docs`; never edit them,
156
- because the next run replaces them.
176
+ because the next run replaces them. The `drift` skill works the same way, from `pnpm hearth:drift`.
157
177
 
158
178
  ## Hosting
159
179
 
@@ -42,12 +42,17 @@ without touching your styling. See "Styling the site" below.
42
42
  ## Working with an AI coding assistant
43
43
 
44
44
  The project comes with instructions for coding agents in `AGENTS.md`, which Codex, Cursor, Copilot and others read on
45
- their own. `CLAUDE.md` and `GEMINI.md` point Claude Code and Gemini CLI to it. Once your API key is in `.env.local`:
45
+ their own. `CLAUDE.md` and `GEMINI.md` point Claude Code and Gemini CLI to it.
46
+
47
+ Agents also use [Drift](https://github.com/The-Smithy/drift) to find code with far fewer tokens. `pnpm hearth:drift`
48
+ installs or updates it and its skill from the latest release on GitHub. The site builds and deploys without it.
49
+
50
+ Once your API key is in `.env.local`:
46
51
 
47
52
  1. **Check the connection yourself, or have the agent do it:** `pnpm hearth:check` confirms the key works and
48
53
  lists the published pages and the block types they use. It never prints the key.
49
54
  2. **Give the agent the full reference:** `pnpm hearth:docs` saves Hearth's API guide to `hearth-api.md` (not
50
- committed), and installs any coding-assistant skills Hearth offers your site in `.agents/skills/` and
55
+ committed), and installs Drift and any coding-assistant skills Hearth offers your site in `.agents/skills/` and
51
56
  `.claude/skills/`. The agent's instructions already point it at both.
52
57
  3. **Then tell it what to build.** Prompts that work well:
53
58
 
@@ -13,3 +13,6 @@ next-env.d.ts
13
13
 
14
14
  # Hearth's API guide, downloaded by pnpm hearth:docs for AI assistants.
15
15
  hearth-api.md
16
+
17
+ # Drift's code index, rebuilt as needed.
18
+ .drift-index.json
@@ -10,6 +10,7 @@
10
10
  "typecheck": "tsc --noEmit",
11
11
  "hearth:check": "node scripts/hearth.mjs check",
12
12
  "hearth:docs": "node scripts/hearth.mjs docs",
13
+ "hearth:drift": "node scripts/hearth.mjs drift",
13
14
  "hearth:update": "node scripts/hearth.mjs update"
14
15
  },
15
16
  "dependencies": {
@@ -2,13 +2,16 @@
2
2
  // pnpm hearth:check says whether The Smithy has released anything newer than this site uses, checks .env.local and
3
3
  // the connection to Hearth, then lists what's published
4
4
  // pnpm hearth:docs saves Hearth's full API guide to hearth-api.md (not committed) for an AI assistant to read,
5
- // and installs any coding-assistant skills Hearth offers this site
5
+ // and installs any coding-assistant skills Hearth offers this site, and Drift
6
+ // pnpm hearth:drift installs or updates Drift, which finds code for coding agents, from its latest GitHub release
6
7
  // pnpm hearth:update updates Hearth's code in this site (src/hearth and the other files its manifest lists) to the
7
8
  // latest release, and the packages it needs; src/theme and your own files stay as they are
8
9
  import { spawnSync } from "node:child_process";
9
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
10
+ import { createHash } from "node:crypto";
11
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
12
+ import { homedir } from "node:os";
10
13
  import path from "node:path";
11
- import { gunzipSync } from "node:zlib";
14
+ import { gunzipSync, inflateRawSync } from "node:zlib";
12
15
 
13
16
  const command = process.argv[2];
14
17
  /** Stops with a message. Thrown rather than process.exit(), which can crash Node on Windows mid-request. */
@@ -19,6 +22,13 @@ const fail = (message) => {
19
22
 
20
23
  const PACKAGE = "@the-smithy/create-hearth-site";
21
24
  const SKILL_DIRS = [".agents/skills", ".claude/skills"];
25
+ /** Drift's release archive for each kind of machine it has a binary for. */
26
+ const DRIFT_ARCHIVES = {
27
+ "win32-x64": "drift-x86_64-pc-windows-msvc.zip",
28
+ "linux-x64": "drift-x86_64-unknown-linux-musl.tar.gz",
29
+ "darwin-arm64": "drift-aarch64-apple-darwin.tar.gz",
30
+ "darwin-x64": "drift-x86_64-apple-darwin.tar.gz",
31
+ };
22
32
  const MANIFEST = "src/hearth/manifest.json";
23
33
  /** The packages hearth:update raises; it leaves the site's other Smithy packages to it. */
24
34
  const UPDATED_TOGETHER = [PACKAGE, "@the-smithy/hearth", "@the-smithy/hearth-schema"];
@@ -149,6 +159,20 @@ async function docs() {
149
159
  writeFileSync("hearth-api.md", await res.text());
150
160
  console.log("✓ Saved Hearth's full API guide to hearth-api.md (not committed). Point your coding assistant at it.");
151
161
  await skills(env);
162
+ // Hearth's guide and skills are what this command is for, so Drift failing doesn't fail it.
163
+ await drift().catch((err) => console.log(`! Skipped Drift: ${err.message}`));
164
+ }
165
+
166
+ /** The skills a folder's .gitignore lists, which are the installed ones; anything else there is the site's own. */
167
+ function listedSkills(dir) {
168
+ const listFile = path.join(dir, ".gitignore");
169
+ if (!existsSync(listFile)) return [];
170
+ return readFileSync(listFile, "utf8").split(/\r?\n/).map((l) => l.replace(/^\/|\/$/g, "")).filter((l) => l && !l.startsWith("#") && l !== ".gitignore" && l !== "drift");
171
+ }
172
+
173
+ function listSkills(dir, names) {
174
+ mkdirSync(dir, { recursive: true });
175
+ writeFileSync(path.join(dir, ".gitignore"), ["# Installed by pnpm hearth:docs and hearth:drift; refreshed each run.", ".gitignore", "/drift/", ...names.map((n) => `/${n}/`)].join("\n") + "\n");
152
176
  }
153
177
 
154
178
  /**
@@ -164,10 +188,7 @@ async function skills(env) {
164
188
  const safe = (p) => /^[a-z0-9][a-z0-9-]*$/.test(p);
165
189
  const names = offered.map((s) => s.name).filter(safe);
166
190
  for (const dir of SKILL_DIRS) {
167
- const listFile = path.join(dir, ".gitignore");
168
- const before = existsSync(listFile)
169
- ? readFileSync(listFile, "utf8").split(/\r?\n/).map((l) => l.replace(/^\/|\/$/g, "")).filter((l) => l && !l.startsWith("#") && l !== ".gitignore")
170
- : [];
191
+ const before = listedSkills(dir);
171
192
  for (const name of before) if (safe(name) && !names.includes(name)) rmSync(path.join(dir, name), { recursive: true, force: true });
172
193
  for (const skill of offered.filter((s) => safe(s.name))) {
173
194
  rmSync(path.join(dir, skill.name), { recursive: true, force: true });
@@ -178,15 +199,74 @@ async function skills(env) {
178
199
  writeFileSync(target, file.content);
179
200
  }
180
201
  }
181
- if (names.length || before.length) {
182
- mkdirSync(dir, { recursive: true });
183
- writeFileSync(listFile, ["# Installed by pnpm hearth:docs from Hearth; refreshed each run.", ".gitignore", ...names.map((n) => `/${n}/`)].join("\n") + "\n");
184
- }
202
+ if (names.length || before.length) listSkills(dir, names);
185
203
  }
186
204
  if (names.length) console.log(`✓ Installed ${names.length === 1 ? "a skill" : `${names.length} skills`} for your coding assistant in ${SKILL_DIRS.map((d) => `${d}/`).join(" and ")}: ${names.join(", ")}.`);
187
205
  }
188
206
 
189
207
 
208
+ /**
209
+ * Installs or updates Drift from its latest GitHub release, replacing the copy already on the PATH, then has it write
210
+ * its own skill, so both stay current without a new Hearth release. Only hearth:drift and hearth:docs run this, never
211
+ * an install or a build: a host doesn't need Drift, and shouldn't download and run a program on every deploy.
212
+ */
213
+ async function drift() {
214
+ const releases = process.env.HEARTH_DRIFT_RELEASES ?? "https://github.com/The-Smithy/drift/releases";
215
+ const windows = process.platform === "win32";
216
+ const name = windows ? "drift.exe" : "drift";
217
+ const found = spawnSync(windows ? "where" : "which", ["drift"], { encoding: "utf8" });
218
+ const onPath = found.status === 0 ? found.stdout.split(/\r?\n/)[0].trim() : null;
219
+ const have = onPath && spawnSync(onPath, ["--version"], { encoding: "utf8" }).stdout?.trim().split(" ")[1];
220
+ const tag = (await fetch(`${releases}/latest`, { redirect: "manual" })).headers.get("location")?.match(/\/tag\/([^/]+)$/)?.[1];
221
+ if (!tag) fail("GitHub didn't say which release of Drift is the latest.");
222
+ const latest = tag.replace(/^v/, "");
223
+ const bin = onPath ?? path.join(windows ? path.join(process.env.LOCALAPPDATA ?? homedir(), "Programs", "drift") : path.join(homedir(), ".local", "bin"), name);
224
+
225
+ if (have && !lower(have, latest)) console.log(`✓ Drift ${have} is up to date.`);
226
+ else {
227
+ const archive = DRIFT_ARCHIVES[`${process.platform}-${process.arch}`];
228
+ if (!archive) fail(`Drift has no download for ${process.platform} ${process.arch}. Install it with: cargo install --git https://github.com/The-Smithy/drift`);
229
+ const download = async (file) => {
230
+ const res = await fetch(`${releases}/download/${tag}/${file}`);
231
+ if (!res.ok) fail(`Couldn't download ${file} from Drift's releases (${res.status}).`);
232
+ return Buffer.from(await res.arrayBuffer());
233
+ };
234
+ const [body, sums] = await Promise.all([download(archive), download("SHA256SUMS")]);
235
+ const sum = sums.toString("utf8").split(/\r?\n/).find((line) => line.endsWith(` ${archive}`))?.split(/\s+/)[0];
236
+ if (sum !== createHash("sha256").update(body).digest("hex")) fail(`${archive} doesn't match its checksum in Drift's release, so it wasn't installed.`);
237
+ const binary = (windows ? unzip : untar)(body).get(name);
238
+ if (!binary) fail(`${archive} has no ${name}.`);
239
+ mkdirSync(path.dirname(bin), { recursive: true });
240
+ writeFileSync(`${bin}.new`, binary);
241
+ chmodSync(`${bin}.new`, 0o755);
242
+ renameSync(`${bin}.new`, bin);
243
+ console.log(`✓ ${have ? `Updated Drift from ${have} to` : "Installed Drift"} ${latest} at ${bin}`);
244
+ if (!onPath) console.log(` Add ${path.dirname(bin)} to your PATH so you and your coding assistant can run drift.`);
245
+ }
246
+
247
+ // Drift's skill follows its version, so like Hearth's skills it's installed rather than committed.
248
+ for (const dir of SKILL_DIRS) listSkills(dir, listedSkills(dir));
249
+ const install = spawnSync(bin, ["install", "."], { encoding: "utf8" });
250
+ if (install.status !== 0) fail(`Drift couldn't install its skill: ${install.stderr?.trim() || install.error?.message}`);
251
+ console.log(`✓ Installed Drift's skill in ${SKILL_DIRS.map((d) => `${d}/`).join(" and ")}.`);
252
+ }
253
+
254
+ /** The files in a zip archive, by name. */
255
+ function unzip(zip) {
256
+ const files = new Map();
257
+ const end = zip.lastIndexOf(Buffer.from("PK\x05\x06", "latin1"));
258
+ let at = zip.readUInt32LE(end + 16);
259
+ for (let i = 0; i < zip.readUInt16LE(end + 10); i++) {
260
+ const nameLength = zip.readUInt16LE(at + 28);
261
+ const header = zip.readUInt32LE(at + 42);
262
+ const start = header + 30 + zip.readUInt16LE(header + 26) + zip.readUInt16LE(header + 28);
263
+ const body = zip.subarray(start, start + zip.readUInt32LE(at + 20));
264
+ files.set(zip.toString("utf8", at + 46, at + 46 + nameLength), zip.readUInt16LE(at + 10) === 8 ? inflateRawSync(body) : body);
265
+ at += 46 + nameLength + zip.readUInt16LE(at + 30) + zip.readUInt16LE(at + 32);
266
+ }
267
+ return files;
268
+ }
269
+
190
270
  /** The files in an npm package tarball (.tgz), by their path inside it ("package/…"). */
191
271
  function untar(tgz) {
192
272
  const tar = gunzipSync(tgz);
@@ -323,7 +403,8 @@ try {
323
403
  if (command === "check") await check();
324
404
  else if (command === "docs") await docs();
325
405
  else if (command === "update") await update();
326
- else fail("Usage: node scripts/hearth.mjs check | docs | update [--version x.y.z] [--force]");
406
+ else if (command === "drift") await drift().catch((err) => fail(err instanceof Stop ? err.message : `Couldn't install Drift: ${err.message}`));
407
+ else fail("Usage: node scripts/hearth.mjs check | docs | drift | update [--version x.y.z] [--force]");
327
408
  } catch (err) {
328
409
  if (!(err instanceof Stop)) throw err;
329
410
  console.error(`\n✗ ${err.message}\n`);