@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 +2 -1
- package/package.json +2 -2
- package/template/AGENTS.md +23 -3
- package/template/README.md +7 -2
- package/template/_gitignore +3 -0
- package/template/package.json +1 -0
- package/template/scripts/hearth.mjs +93 -12
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.
|
|
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.
|
|
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"
|
package/template/AGENTS.md
CHANGED
|
@@ -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
|
|
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
|
|
package/template/README.md
CHANGED
|
@@ -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.
|
|
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
|
|
package/template/_gitignore
CHANGED
package/template/package.json
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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`);
|