@casomoltd/tooling 0.37.0 → 0.39.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
@@ -15,6 +15,7 @@ Shared linting, formatting, commit config, and CLI tools for Casomo Ltd's repos.
15
15
  | `@casomoltd/tooling/knip` | Knip config for unused exports/dependencies |
16
16
  | `@casomoltd/tooling/jscpd` | Copy-paste detection config |
17
17
  | `@casomoltd/tooling/readability` | `scoreText()` — readability scoring for page content |
18
+ | `@casomoltd/tooling/identity` | `CASOMO_ORGANIZATION`, the schema.org `Organization`, logo included, that every Casomo site names as publisher in its JSON-LD. App code imports it at build time, so a site that installs only production dependencies at runtime lists the package under `dependencies` |
18
19
  | `@casomoltd/tooling/buttondown` | For a repo using the `buttondown` command: the shared email blocks and skins (`createEmailBlocks`, `emailBaseCss`, `composeWebSkin`), `tokenReader()`, `loadSettings()` and `buildPayload()`, and `EMAIL_CHROME_HIDES` and `WEB_CHROME_HIDES` ([docs/buttondown.md](docs/buttondown.md)) |
19
20
 
20
21
  ### Bin commands
@@ -23,7 +24,8 @@ Shared linting, formatting, commit config, and CLI tools for Casomo Ltd's repos.
23
24
  |---|---|
24
25
  | `check-version` | Pre-push guard — rejects push if `package.json` version hasn't changed vs `origin/main` |
25
26
  | `check-linear` | Pre-push guard — rejects a push that would put a merge commit on the remote. History is linear; a branch catches up by rebasing. Only what the push adds is judged, so merges already on the remote stay |
26
- | `pre-push` | Husky pre-push hook — runs check, then the version, tag and linear-history guards |
27
+ | `pre-push` | Husky pre-push hook — runs check, then `check:routes` where a repo defines it, then the version, tag and linear-history guards |
28
+ | `check-routes` | Pre-push gate for a Next.js site — starts `next start` against the build `check` made, requests every page in `/sitemap.xml`, and fails if any is not a 200. Run it as `"check:routes": "check-routes"`; [why it runs on push](#quality-gates) |
27
29
  | `pre-commit` | Husky pre-commit hook — runs `npm run check` |
28
30
  | `commit-msg` | Husky commit-msg hook — runs commitlint, then `check-listed-files` |
29
31
  | `check-listed-files` | Refuse a commit that changes a file still listed in `eslint-suppressions.json`, unless a `Lint-deferred:` trailer says why ([docs/lint.md](docs/lint.md)) |
@@ -286,13 +288,17 @@ the local hooks are the gate, CI is the backstop.
286
288
 
287
289
  - **pre-commit**: `npm run check` — the repo's full health gate.
288
290
  - **commit-msg**: commitlint (house rules + the AI-attribution ban).
289
- - **pre-push**: `npm run check`, then the version and tag guards.
291
+ - **pre-push**: `npm run check`, then `check:routes` where the repo
292
+ defines it, then the version, tag and linear-history guards.
290
293
 
291
294
  **`check` owns the build.** A repo whose build fails is not
292
295
  healthy, and any assertion that reads build output has to run
293
296
  after the build in the same script — so `build` belongs in
294
297
  `check`, and appears nowhere else. Declaring it in the pre-push
295
298
  gate as well would give it two owners and build twice.
299
+ Exception: `check:routes` reads the build outside `check`. It
300
+ starts a server, which belongs to a push rather than to every
301
+ commit, so the pre-push hook runs it after `check`.
296
302
 
297
303
  ### What every repo's `check` must contain
298
304
 
@@ -9,8 +9,10 @@
9
9
  //
10
10
  // So this does not orchestrate the gates; it checks the composition:
11
11
  // 1. every REQUIRED gate is defined and actually run by `check`
12
- // 2. no gate-shaped script is defined but left unreferenced (a dead gate)
12
+ // 2. no gate-shaped script is defined but left unreferenced (a dead gate),
13
+ // other than a pre-push gate, which the hook runs instead
13
14
  // 3. no exception outlives the problem it was written for
15
+ // 4. no pre-push gate is run by `check`, which every commit pays for
14
16
  //
15
17
  // A repo opts out by declaring a reason in its own package.json. A blank
16
18
  // reason fails, so an opt-out can never be silent:
@@ -38,6 +40,11 @@ export const REQUIRED = [
38
40
  /** Gate-shaped but not mandatory: still dead if defined and never run. */
39
41
  export const OPTIONAL = ["check:readability"];
40
42
 
43
+ /** Gates the pre-push hook runs after `check`, never `check` itself: they
44
+ * start a server against the build, which a commit should not pay for.
45
+ * bin/pre-push.sh runs each by name, so the two lists must match. */
46
+ export const PRE_PUSH = ["check:routes"];
47
+
41
48
  /** A script is a gate only by exact membership or the `check:` prefix — never
42
49
  * by heuristic, so `dev`, `start`, `clean`, `lint:fix`, `test:watch`,
43
50
  * `build:clean` and `indexnow` need no denylist to be ignored. */
@@ -54,45 +61,57 @@ export const referencedGates = (check) =>
54
61
  .filter(Boolean),
55
62
  );
56
63
 
64
+ /** Each required gate that is not defined, or defined but never run. */
65
+ const requiredFailures = (scripts, run) =>
66
+ REQUIRED.flatMap((gate) => {
67
+ if (!(gate in scripts)) return [[gate, `required gate is not defined`]];
68
+ if (!run.has(gate)) {
69
+ return [[gate, `required gate is defined but \`check\` never runs it`]];
70
+ }
71
+ return [];
72
+ });
73
+
74
+ /** Each other gate-shaped script that `check` never runs. `check` itself is
75
+ * the chain, not a step within it, and the hook runs a pre-push gate. */
76
+ const deadGates = (scripts, run) =>
77
+ Object.keys(scripts)
78
+ .filter((n) => isGate(n) && n !== "check" && !REQUIRED.includes(n))
79
+ .filter((gate) => !PRE_PUSH.includes(gate))
80
+ .filter((gate) => !run.has(gate))
81
+ .map((gate) => [gate, `defined but \`check\` never runs it — a dead gate`]);
82
+
83
+ /** Each pre-push gate that `check` runs, so every commit starts a server. */
84
+ const prePushInCheck = (run) =>
85
+ PRE_PUSH.filter((gate) => run.has(gate)).map((gate) => [
86
+ gate,
87
+ `a pre-push gate: \`check\` runs on every commit`,
88
+ ]);
89
+
90
+ /** Sort the failures by their exception: none, a blank reason, or a reason. */
91
+ const byException = (failures, exceptions) => {
92
+ const sorted = {live: [], excused: [], unreasoned: []};
93
+ for (const [gate, reason] of failures) {
94
+ if (!(gate in exceptions)) sorted.live.push([gate, reason]);
95
+ else if (!String(exceptions[gate] ?? "").trim()) sorted.unreasoned.push(gate);
96
+ else sorted.excused.push([gate, exceptions[gate]]);
97
+ }
98
+ return sorted;
99
+ };
100
+
57
101
  export const evaluate = (pkg) => {
58
102
  const scripts = pkg.scripts ?? {};
59
103
  const exceptions = pkg.casomo?.gates ?? {};
60
104
  const run = referencedGates(scripts.check);
61
-
62
- // `check` itself is the chain, not a step within it.
63
- const defined = Object.keys(scripts).filter((n) => isGate(n) && n !== "check");
64
-
65
- const failures = [];
66
- for (const gate of REQUIRED) {
67
- if (!(gate in scripts)) failures.push([gate, `required gate is not defined`]);
68
- else if (!run.has(gate))
69
- failures.push([gate, `required gate is defined but \`check\` never runs it`]);
70
- }
71
- for (const gate of defined) {
72
- if (run.has(gate)) continue;
73
- if (failures.some(([g]) => g === gate)) continue; // already reported above
74
- failures.push([gate, `defined but \`check\` never runs it — a dead gate`]);
75
- }
76
-
77
- const excused = [];
78
- const unreasoned = [];
79
- const live = [];
80
- for (const [gate, reason] of failures) {
81
- if (!(gate in exceptions)) {
82
- live.push([gate, reason]);
83
- } else if (!String(exceptions[gate] ?? "").trim()) {
84
- unreasoned.push(gate);
85
- } else {
86
- excused.push([gate, exceptions[gate]]);
87
- }
88
- }
89
-
105
+ const failures = [
106
+ ...requiredFailures(scripts, run),
107
+ ...deadGates(scripts, run),
108
+ ...prePushInCheck(run),
109
+ ];
90
110
  // An exception whose failure has gone away is the same rot in reverse.
91
111
  const stale = Object.keys(exceptions).filter(
92
112
  (gate) => !failures.some(([g]) => g === gate),
93
113
  );
94
-
95
- return {live, excused, unreasoned, stale};
114
+ return {...byException(failures, exceptions), stale};
96
115
  };
97
116
 
98
117
  const main = () => {
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Request every page a built Next.js site lists in its sitemap from
4
+ * `next start` on a local port, and fail if any answers anything but 200.
5
+ *
6
+ * A build can pass while a page still fails at request time: a server
7
+ * component that throws, or a fetch that breaks only when the page renders.
8
+ * The pre-push hook runs this after `npm run check`, against the build
9
+ * `check` has just made; it makes no build of its own. It stays out of
10
+ * `check` because the pre-commit hook runs `check`, and starting a server
11
+ * belongs to a push, not to every commit.
12
+ *
13
+ * It reads the sitemap Next emits: plain `<loc>` elements, in a urlset or a
14
+ * sitemap index one level deep, with `&amp;` the only entity in a URL.
15
+ *
16
+ * Usage, in the site's root after `next build`:
17
+ * check-routes
18
+ *
19
+ * Exit codes:
20
+ * 0 every listed page answered 200
21
+ * 1 a page did not, the sitemap failed or listed none, or the server
22
+ * never started
23
+ */
24
+
25
+ import {spawn} from "node:child_process";
26
+ import {existsSync, realpathSync} from "node:fs";
27
+ import {createServer} from "node:net";
28
+ import {setTimeout as sleep} from "node:timers/promises";
29
+ import {pathToFileURL} from "node:url";
30
+
31
+ const HOST = "127.0.0.1";
32
+ const SITEMAP = "/sitemap.xml";
33
+ // The default distDir; a site that moves it is not supported.
34
+ const BUILD_ID = ".next/BUILD_ID";
35
+ // The site's own `next`, never one `npx` would offer to download.
36
+ const NEXT_BIN = "node_modules/.bin/next";
37
+ const HTTP_OK = 200;
38
+ const READY_POLL_MS = 250;
39
+ const READY_TIMEOUT_MS = 60_000;
40
+ // A page waiting on a dead upstream must fail the push, not hang it.
41
+ const REQUEST_TIMEOUT_MS = 30_000;
42
+ // Enough to keep a few hundred pages quick without starving the server.
43
+ const CONCURRENCY = 8;
44
+
45
+ /** Probe every path, CONCURRENCY at a time; return the ones not 200. */
46
+ export const failingPages = async (base, paths) => {
47
+ const queue = [...paths];
48
+ const failures = [];
49
+ const worker = async () => {
50
+ for (let path = queue.shift(); path !== undefined; path = queue.shift()) {
51
+ const result = await probe(base, path);
52
+ if (result.status !== HTTP_OK) failures.push(result);
53
+ }
54
+ };
55
+ await Promise.all(Array.from({length: CONCURRENCY}, worker));
56
+ return failures;
57
+ };
58
+
59
+ /** The path and query of every `<loc>` in a sitemap or sitemap index. The
60
+ * sitemap names the production host; the check asks the local server. */
61
+ const sitemapPaths = (xml) =>
62
+ [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map(([, loc]) => {
63
+ const url = new URL(loc.trim().replaceAll("&amp;", "&"));
64
+ return url.pathname + url.search;
65
+ });
66
+
67
+ /** A sitemap index lists other sitemaps, not pages. */
68
+ const isSitemapIndex = (xml) => /<sitemapindex[\s>]/.test(xml);
69
+
70
+ const message = (err) => (err instanceof Error ? err.message : String(err));
71
+
72
+ const get = (url, init = {}) =>
73
+ fetch(url, {...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)});
74
+
75
+ /** A sitemap's text; any answer but 200 is the failure, not a retry. */
76
+ const readSitemap = async (base, path) => {
77
+ const res = await get(base + path);
78
+ if (res.status !== HTTP_OK) throw new Error(`${path} answered ${res.status}`);
79
+ return res.text();
80
+ };
81
+
82
+ /** Every page path, once each, following a sitemap index one level down. */
83
+ const collectPaths = async (base, rootXml) => {
84
+ if (!isSitemapIndex(rootXml)) return [...new Set(sitemapPaths(rootXml))];
85
+ const children = await Promise.all(
86
+ sitemapPaths(rootXml).map((path) => readSitemap(base, path)),
87
+ );
88
+ return [...new Set(children.flatMap(sitemapPaths))];
89
+ };
90
+
91
+ /** One page's status, or the error that stopped the request. A redirect
92
+ * counts as a failure: a sitemap should list where a page lives. */
93
+ const probe = async (base, path) => {
94
+ try {
95
+ const res = await get(base + path, {redirect: "manual"});
96
+ await res.arrayBuffer();
97
+ return {path, status: res.status};
98
+ } catch (err) {
99
+ return {path, error: message(err)};
100
+ }
101
+ };
102
+
103
+ /** A free local port, so a dev server already on 3000 is never in the way. */
104
+ const freePort = () =>
105
+ new Promise((resolve, reject) => {
106
+ const listener = createServer();
107
+ listener.on("error", reject);
108
+ listener.listen(0, HOST, () => {
109
+ const {port} = listener.address();
110
+ listener.close(() => resolve(port));
111
+ });
112
+ });
113
+
114
+ /** `next start` in its own process group, so the stop reaches `next-server`
115
+ * and not only its launcher. `died` rejects the moment the server fails to
116
+ * spawn or exits, so a dead server fails the check now, not at the timeout. */
117
+ const startServer = (port) => {
118
+ const server = spawn(NEXT_BIN, ["start", "-p", String(port), "-H", HOST], {
119
+ stdio: ["ignore", "pipe", "pipe"],
120
+ detached: true,
121
+ });
122
+ const output = [];
123
+ server.stdout.on("data", (d) => output.push(String(d)));
124
+ server.stderr.on("data", (d) => output.push(String(d)));
125
+ const died = new Promise((_, reject) => {
126
+ server.on("error", (err) => reject(new Error(`could not start next: ${err}`)));
127
+ server.on("exit", (code) =>
128
+ reject(new Error(`next start exited (${code})\n${output.join("")}`)),
129
+ );
130
+ });
131
+ // The exit the stop itself causes is expected, so nothing awaits it then.
132
+ died.catch(() => {});
133
+ return {server, output, died};
134
+ };
135
+
136
+ /** Stop the server's process group; one that has already exited is fine. */
137
+ const stopServer = (server) => {
138
+ if (server.pid === undefined) return;
139
+ try {
140
+ process.kill(-server.pid, "SIGTERM");
141
+ } catch (err) {
142
+ if (err?.code !== "ESRCH") throw err;
143
+ }
144
+ };
145
+
146
+ /** The root sitemap once the server answers; keep polling only while the
147
+ * connection is refused. */
148
+ const waitForSitemap = async (base, output) => {
149
+ const deadline = Date.now() + READY_TIMEOUT_MS;
150
+ while (Date.now() < deadline) {
151
+ try {
152
+ return await readSitemap(base, SITEMAP);
153
+ } catch (err) {
154
+ if (!(err instanceof TypeError)) throw err;
155
+ }
156
+ await sleep(READY_POLL_MS);
157
+ }
158
+ throw new Error(`the server never answered ${SITEMAP}\n${output.join("")}`);
159
+ };
160
+
161
+ const report = (count, failures) => {
162
+ if (count === 0) {
163
+ console.error(`✗ check-routes: ${SITEMAP} lists no pages`);
164
+ return 1;
165
+ }
166
+ if (failures.length > 0) {
167
+ console.error(`✗ check-routes: ${failures.length} of ${count} pages failed`);
168
+ for (const {path, status, error} of failures) {
169
+ console.error(` ${status ?? error} ${path}`);
170
+ }
171
+ return 1;
172
+ }
173
+ console.log(`✓ check-routes: ${count} pages answered 200`);
174
+ return 0;
175
+ };
176
+
177
+ /** Start the server, check every page, stop it; the exit code. */
178
+ const run = async () => {
179
+ const port = await freePort();
180
+ const base = `http://${HOST}:${port}`;
181
+ const {server, output, died} = startServer(port);
182
+ try {
183
+ const rootXml = await Promise.race([waitForSitemap(base, output), died]);
184
+ const paths = await collectPaths(base, rootXml);
185
+ return report(paths.length, await failingPages(base, paths));
186
+ } catch (err) {
187
+ console.error(`✗ check-routes: ${message(err)}`);
188
+ return 1;
189
+ } finally {
190
+ stopServer(server);
191
+ }
192
+ };
193
+
194
+ const main = async () => {
195
+ if (!existsSync(BUILD_ID)) {
196
+ console.error(
197
+ `✗ check-routes: no ${BUILD_ID}; run \`next build\` first`
198
+ + " (a custom distDir is not supported)",
199
+ );
200
+ process.exit(1);
201
+ }
202
+ const code = await run();
203
+ // The server's pipes can keep node alive past the stop; the check is done.
204
+ process.exit(code);
205
+ };
206
+
207
+ /** True only when this file is the entry point. npm installs a bin as a
208
+ * symlink, so `process.argv[1]` is the link while `import.meta.url` is the
209
+ * target it resolves to: resolve before comparing, or `main` never runs. */
210
+ const isEntryPoint = () => {
211
+ try {
212
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
213
+ } catch {
214
+ return false;
215
+ }
216
+ };
217
+
218
+ if (isEntryPoint()) await main();
package/bin/pre-push.sh CHANGED
@@ -14,7 +14,12 @@ export PRE_PUSH_REFS
14
14
  # owners and run it twice wherever `check` builds. A repo that needs a build
15
15
  # gate adds `npm run build` to its own `check`; one that produces no artifact
16
16
  # simply doesn't.
17
+ #
18
+ # `check:routes` runs after `check`, not in it: see the README, "Quality
19
+ # gates". Repos without it skip it. Every gate in PRE_PUSH
20
+ # (bin/check-gates.mjs) must be run here.
17
21
  npm run check \
22
+ && npm run --if-present check:routes \
18
23
  && echo "$PRE_PUSH_REFS" | check-version \
19
24
  && echo "$PRE_PUSH_REFS" | check-tags \
20
25
  && echo "$PRE_PUSH_REFS" | check-linear
@@ -4,7 +4,8 @@
4
4
  // the allowlist and never look like a secret. Defence-in-depth on top of
5
5
  // package.json "files" — this fails CI if that allowlist is ever loosened
6
6
  // or a secret-bearing file sneaks in. The tooling repo is public, so the
7
- // published/installed surface must stay configs + CLI only.
7
+ // published/installed surface must stay configs, CLIs and the build-time
8
+ // data sites import, and nothing private.
8
9
  import {execSync} from "node:child_process";
9
10
 
10
11
  // Dirs/files a consumer is allowed to receive. npm always adds
@@ -20,12 +21,21 @@ const ALLOWED_PREFIXES = [
20
21
  "report/",
21
22
  "styles/",
22
23
  "buttondown/",
24
+ "identity/",
23
25
  ];
24
26
  const ALLOWED_FILES = ["README.md", "package.json", "LICENSE", "LICENSE.md"];
25
27
 
26
- // Anything matching these must never ship, even from an allowed dir.
27
- const SECRET_PATTERN =
28
- /(^|\/)(\.env(\..*)?|.*settings\.local\.json|.*\.pem|id_[rd]sa|.*\.key|\.npmrc)$/i;
28
+ // A file whose name alone marks it as a secret must never ship, even from an
29
+ // allowed dir: an env file, local settings, a key, or an npm token file.
30
+ const SECRET_NAMES = new Set([".env", ".npmrc", "id_rsa", "id_dsa"]);
31
+ const SECRET_ENDINGS = [".pem", ".key", "settings.local.json"];
32
+
33
+ function looksSecret(path) {
34
+ const name = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
35
+ return SECRET_NAMES.has(name)
36
+ || name.startsWith(".env.")
37
+ || SECRET_ENDINGS.some((ending) => name.endsWith(ending));
38
+ }
29
39
 
30
40
  // Tests live beside what they test, which puts them inside an allowed dir —
31
41
  // so the allowlist alone would ship them. Consumers get the tool, not its
@@ -40,7 +50,7 @@ const escaped = files.filter(
40
50
  !ALLOWED_FILES.includes(f) &&
41
51
  !ALLOWED_PREFIXES.some((prefix) => f.startsWith(prefix)),
42
52
  );
43
- const secrets = files.filter((f) => SECRET_PATTERN.test(f));
53
+ const secrets = files.filter(looksSecret);
44
54
  const tests = files.filter((f) => TEST_PATTERN.test(f));
45
55
 
46
56
  if (escaped.length || secrets.length || tests.length) {
@@ -0,0 +1,15 @@
1
+ /** Casomo Ltd as a schema.org `Organization`, for a page's JSON-LD.
2
+ *
3
+ * Every field is read-only because the value is frozen: spread it into an
4
+ * untyped object literal, or type a JSON-LD field that holds it as
5
+ * `readonly string[]`, never as `string[]`. */
6
+ export interface CasomoOrganization {
7
+ readonly "@type": "Organization";
8
+ readonly "@id": string;
9
+ readonly name: string;
10
+ readonly url: string;
11
+ readonly logo: string;
12
+ readonly sameAs: readonly string[];
13
+ }
14
+
15
+ export declare const CASOMO_ORGANIZATION: CasomoOrganization;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The public face of the identity entry: what a site imports. The value
3
+ * lives in organization.mjs, where the typecheck can read it against the
4
+ * declaration here; beside a .d.mts, TypeScript would skip it.
5
+ */
6
+ export {CASOMO_ORGANIZATION} from "./organization.mjs";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Casomo Ltd as search engines should see it: the one schema.org
3
+ * `Organization` every Casomo site names in its JSON-LD. Each site imports
4
+ * this rather than typing its own, so no two sites can disagree about who
5
+ * publishes them, and `sameAs` ties them all to the same profiles.
6
+ *
7
+ * Public facts only: the company's name, its mark and the public addresses it
8
+ * is known by. A person, such as an article's author, stays with the site.
9
+ */
10
+ const SITE = "https://casomoltd.com";
11
+
12
+ /** @type {import("./index.d.mts").CasomoOrganization} */
13
+ export const CASOMO_ORGANIZATION = Object.freeze({
14
+ "@type": "Organization",
15
+ // One id across every site, so a crawler merges them into one entity.
16
+ "@id": `${SITE}/#organization`,
17
+ name: "Casomo Ltd",
18
+ url: SITE,
19
+ // The company's mark, as its own site serves it. Each site keeps its own
20
+ // favicon and palette; this is the one logo the company is known by.
21
+ logo: `${SITE}/assets/icon.svg`,
22
+ sameAs: Object.freeze([
23
+ "https://casomo.co.uk",
24
+ "https://www.facebook.com/casomouk",
25
+ "https://www.linkedin.com/company/111967567/",
26
+ ]),
27
+ });
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@casomoltd/tooling",
3
- "version": "0.37.0",
4
- "description": "Casomo's shared dev config: ESLint, Prettier, commitlint, knip & jscpd presets and dev CLIs (git hooks, readability, screenshots, design specs)",
3
+ "version": "0.39.0",
4
+ "description": "Casomo's shared dev config: ESLint, Prettier, commitlint, knip & jscpd presets, dev CLIs (git hooks, readability, screenshots, design specs), and the build-time data and helpers sites import (the Buttondown engine, the shared Organization)",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/casomoltd/tooling.git"
8
+ "url": "git+https://github.com/casomoltd/tooling.git"
9
9
  },
10
10
  "files": [
11
11
  "bin/",
@@ -18,14 +18,16 @@
18
18
  "report/",
19
19
  "styles/",
20
20
  "buttondown/",
21
+ "identity/",
21
22
  "README.md",
22
- "!**/*.test.mjs"
23
+ "!**/*.test.mjs",
24
+ "!identity/tsconfig.json"
23
25
  ],
24
26
  "scripts": {
25
27
  "check": "node bin/check-gates.mjs && node bin/verify-pack.mjs && npm run check:private-refs && npm run check:personal-data && npm run lint && npm run typecheck && npm run test && npm run lint:md",
26
28
  "lint": "eslint .",
27
- "typecheck": "tsc --noEmit -p hooks/tsconfig.json",
28
- "test": "node hooks/test.mts && node bin/check-gates.test.mjs && node bin/check-listed-files.test.mjs && node bin/check-personal-data.test.mjs && node bin/casomo-config.test.mjs && node bin/buttondown.test.mjs && node commitlint/index.test.mjs && node readability/index.test.mjs",
29
+ "typecheck": "tsc --noEmit -p hooks/tsconfig.json && tsc --noEmit -p identity/tsconfig.json",
30
+ "test": "node hooks/test.mts && node bin/check-gates.test.mjs && node bin/check-listed-files.test.mjs && node bin/check-personal-data.test.mjs && node bin/check-routes.test.mjs && node bin/casomo-config.test.mjs && node bin/buttondown.test.mjs && node commitlint/index.test.mjs && node readability/index.test.mjs && node identity/index.test.mjs",
29
31
  "lint:md": "remark --frail --quiet --no-stdout --rc-path bin/skills-lint.config.mjs skills agents docs",
30
32
  "build": "echo 'no-op: source-only repo, nothing to build'",
31
33
  "prepare": "husky || true",
@@ -51,26 +53,31 @@
51
53
  "./buttondown": {
52
54
  "types": "./buttondown/index.d.mts",
53
55
  "default": "./buttondown/index.mjs"
56
+ },
57
+ "./identity": {
58
+ "types": "./identity/index.d.mts",
59
+ "default": "./identity/index.mjs"
54
60
  }
55
61
  },
56
62
  "bin": {
57
- "build-report": "./bin/build-report.mjs",
58
- "buttondown": "./bin/buttondown.mjs",
59
- "check-gates": "./bin/check-gates.mjs",
60
- "check-hooks": "./bin/check-hooks.sh",
61
- "check-listed-files": "./bin/check-listed-files.mjs",
62
- "check-linear": "./bin/check-linear.sh",
63
- "check-personal-data": "./bin/check-personal-data.mjs",
64
- "check-private-refs": "./bin/check-private-refs.mjs",
65
- "check-tags": "./bin/check-tags.sh",
66
- "check-version": "./bin/check-version.sh",
67
- "commit-msg": "./bin/commit-msg.sh",
68
- "pre-commit": "./bin/pre-commit.sh",
69
- "pre-push": "./bin/pre-push.sh",
70
- "readability": "./bin/readability.mjs",
71
- "render-figures": "./bin/render-figures.mjs",
72
- "screenshot": "./bin/screenshot.mjs",
73
- "spec-check": "./bin/spec-check.mjs"
63
+ "build-report": "bin/build-report.mjs",
64
+ "buttondown": "bin/buttondown.mjs",
65
+ "check-gates": "bin/check-gates.mjs",
66
+ "check-hooks": "bin/check-hooks.sh",
67
+ "check-listed-files": "bin/check-listed-files.mjs",
68
+ "check-linear": "bin/check-linear.sh",
69
+ "check-personal-data": "bin/check-personal-data.mjs",
70
+ "check-private-refs": "bin/check-private-refs.mjs",
71
+ "check-routes": "bin/check-routes.mjs",
72
+ "check-tags": "bin/check-tags.sh",
73
+ "check-version": "bin/check-version.sh",
74
+ "commit-msg": "bin/commit-msg.sh",
75
+ "pre-commit": "bin/pre-commit.sh",
76
+ "pre-push": "bin/pre-push.sh",
77
+ "readability": "bin/readability.mjs",
78
+ "render-figures": "bin/render-figures.mjs",
79
+ "screenshot": "bin/screenshot.mjs",
80
+ "spec-check": "bin/spec-check.mjs"
74
81
  },
75
82
  "publishConfig": {
76
83
  "access": "public"