memento-mori-jester 0.1.78 → 0.1.80

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/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to Memento Mori Jester are tracked here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.1.80
8
+
9
+ - Added a deterministic `promo/share-kit/social-card.svg` for GitHub, X, and project-update link previews.
10
+ - Added `npm run promo:card` and `npm run promo:card:check` to regenerate or verify the social card.
11
+ - Extended promo freshness checks and docs so the social card stays part of the maintainer release flow.
12
+
13
+ ## 0.1.79
14
+
15
+ - Added `npm run promo:check` to verify the current repo-local promo video, stills, docs, and fixture evidence numbers stay aligned.
16
+ - Wired promo freshness validation into `npm test` and the production-readiness guard.
17
+ - Updated release, readiness, roadmap, and promo docs with the new maintainer check.
18
+
7
19
  ## 0.1.78
8
20
 
9
21
  - Added a refreshed HyperFrames X demo render under `promo/x-demo-v0.1.78` with current version and fixture-evidence numbers.
package/README.md CHANGED
@@ -503,6 +503,8 @@ Use the false-positive template for noisy cautions or blocks. Include `jester su
503
503
  Maintainers can use [docs/MAINTAINER_TRIAGE.md](docs/MAINTAINER_TRIAGE.md) to turn useful false-positive reports into redacted fixtures.
504
504
  Run `npm run fixtures:check` before merging fixture changes; it catches duplicate IDs, missing rule metadata, weak descriptions, unsafe-looking content, and duplicate content.
505
505
  Run `npm run fixtures:report` to see fixture coverage by rule, rule family, preset slice, kind, verdict, quiet-pass boundaries, feasible pass-case gaps, and curation-next guidance before choosing the next fixture. Use `npm run fixtures:report -- --markdown` when you want a paste-ready summary for release notes or GitHub issues.
506
+ Run `npm run promo:card` to regenerate the repo-local social preview card after changing its copy or design.
507
+ Run `npm run promo:check` after editing promo assets; it checks the current demo video, stills, docs, and fixture evidence numbers stay in sync.
506
508
 
507
509
  For vulnerabilities, private code exposure, or credential-handling concerns, follow [SECURITY.md](SECURITY.md) instead of opening a public issue with sensitive details.
508
510
 
@@ -512,6 +514,7 @@ Release checklist:
512
514
 
513
515
  ```powershell
514
516
  npm.cmd test
517
+ npm.cmd run promo:check
515
518
  npm.cmd run production:check
516
519
  npm.cmd run pack:dry
517
520
  git tag -a v0.1.x -m "Memento Mori Jester v0.1.x"
package/ROADMAP.md CHANGED
@@ -6,6 +6,8 @@ Memento Mori Jester is usable today as a CLI, MCP server, GitHub Action, and git
6
6
 
7
7
  ## Recently Shipped
8
8
 
9
+ - Social preview card in v0.1.80, adding a deterministic 1200x630 promo card plus generation and freshness checks.
10
+ - Promo freshness check in v0.1.79, verifying the current demo video, share-kit stills, docs, and fixture evidence numbers before public posting.
9
11
  - Fresh demo render in v0.1.78, updating the repo-local X video and share-kit stills to current version and fixture totals.
10
12
  - Promo/share kit in v0.1.77, adding X post copy, a short demo script, a posting checklist, and still images from the existing demo video.
11
13
  - Real-world preset quiet-pass curation in v0.1.76, adding eight safe examples across python, security, web, and AI workflows while keeping fixture coverage gaps clean.
@@ -69,7 +71,7 @@ Memento Mori Jester is usable today as a CLI, MCP server, GitHub Action, and git
69
71
 
70
72
  - Collect real-world reports for the next lowest-count preset slices now highlighted by `fixtures:report`.
71
73
  - Add more framework-specific false-positive examples from real reports so tuning guidance keeps getting sharper.
72
- - Add a small promo freshness check so future demo copy does not drift from package version or fixture totals.
74
+ - Add a small web landing page that reuses the existing demo, social card, and start command.
73
75
 
74
76
  ## Quality And Safety
75
77
 
@@ -9,6 +9,7 @@ This checklist defines what "production grade" means for Memento Mori Jester rig
9
9
  - GitHub Releases and npm publishing are automated from annotated `v*` tags through GitHub Actions trusted publishing.
10
10
  - CI runs tests and a package dry run on every push to `main` and pull request.
11
11
  - The local playground, GitHub Action, MCP setup snippets, preset examples, fixtures, and release notes ship in the npm package.
12
+ - Repo-local promo assets stay outside the npm package, but `npm run promo:check` keeps the current demo video, stills, social card, docs, and fixture evidence numbers aligned.
12
13
 
13
14
  ## npm Package
14
15
 
@@ -54,6 +55,7 @@ This checklist defines what "production grade" means for Memento Mori Jester rig
54
55
  - `docs/MAINTAINER_TRIAGE.md` explains how to turn useful false-positive reports into fixture coverage before changing rule logic.
55
56
  - `npm run fixtures:check` validates fixture IDs, metadata, unsafe-looking content, duplicate content, and explicit expected/absent rule intent.
56
57
  - `npm run fixtures:report` shows fixture coverage by rule, rule family, preset slice, kind, verdict, quiet-pass rule boundaries, and feasible pass-case gaps so maintainers can pick the next fixture target; `npm run fixtures:report -- --markdown` produces a paste-ready maintainer snapshot.
58
+ - `npm run promo:card` regenerates the deterministic social preview card, and `npm run promo:check` verifies current repo-local promo assets against the current fixture evidence before maintainers post or refresh the demo.
57
59
  - npm publish has a manual workflow fallback, but the normal release path is tag-driven trusted publishing.
58
60
 
59
61
  ## Static Guard
@@ -69,6 +71,7 @@ This checklist defines what "production grade" means for Memento Mori Jester rig
69
71
  - maintainer triage docs exist and link noisy-rule reports back to fixture coverage.
70
72
  - fixture authoring checks are wired into `npm test`.
71
73
  - fixture coverage reports are wired into `npm test`.
74
+ - promo freshness checks are wired into `npm test`.
72
75
 
73
76
  `npm test` runs this check after the TypeScript build and unit tests.
74
77
 
package/docs/RELEASE.md CHANGED
@@ -12,6 +12,8 @@ npm.cmd run fixtures:check
12
12
  npm.cmd run fixtures:report
13
13
  npm.cmd run fixtures:report -- --json
14
14
  npm.cmd run fixtures:report -- --markdown
15
+ npm.cmd run promo:card:check
16
+ npm.cmd run promo:check
15
17
  npm.cmd run pack:dry
16
18
  git diff --check
17
19
  ```
@@ -0,0 +1,47 @@
1
+ # Memento Mori Jester v0.1.79
2
+
3
+ ## Summary
4
+
5
+ This release adds a repo-local promo freshness check so maintainers can verify the current demo video, stills, docs, and fixture evidence numbers before posting or refreshing public assets.
6
+
7
+ ## What Changed
8
+
9
+ - Added `scripts/check-promo-freshness.mjs`.
10
+ - Added `npm run promo:check`.
11
+ - Wired `promo:check` into `npm test`.
12
+ - Updated production-readiness checks so the promo freshness guard cannot silently disappear.
13
+ - Updated README, release docs, production-readiness docs, promo docs, roadmap, changelog, and release notes.
14
+
15
+ ## Public Interface
16
+
17
+ - No CLI command changes.
18
+ - No MCP tool changes.
19
+ - No config schema changes.
20
+ - No review rule, scoring, or verdict behavior changes.
21
+ - No GitHub Action behavior changes.
22
+ - `promo/` remains outside the npm package `files` list.
23
+
24
+ ## Release Validation
25
+
26
+ ```powershell
27
+ npm.cmd test
28
+ npm.cmd run demo:svg:check
29
+ npm.cmd run promo:check
30
+ npm.cmd run pack:dry
31
+ git diff --check
32
+ git diff | node .\dist\cli.js diff --fail-on block --subject "v0.1.79 promo freshness check"
33
+ ```
34
+
35
+ For a future same-version promo refresh, maintainers can run:
36
+
37
+ ```powershell
38
+ npm.cmd run promo:check -- --require-package-version
39
+ ```
40
+
41
+ This release does not require that strict mode because the current public demo snapshot remains `x-demo-v0.1.78`.
42
+
43
+ Expected:
44
+
45
+ - default `promo:check` passes for the current published demo snapshot,
46
+ - `--require-package-version` is available for intentional same-version promo refreshes,
47
+ - GitHub Release and npm Publish complete from the `v0.1.79` tag.
@@ -0,0 +1,40 @@
1
+ # Memento Mori Jester v0.1.80
2
+
3
+ ## Summary
4
+
5
+ This release adds a deterministic social preview card for GitHub, X, and project-update links. It keeps the promo work local and reviewable while making shared links look intentional.
6
+
7
+ ## What Changed
8
+
9
+ - Added `promo/share-kit/social-card.svg`.
10
+ - Added `scripts/render-social-card.mjs`.
11
+ - Added `npm run promo:card` and `npm run promo:card:check`.
12
+ - Extended `npm run promo:check` to verify the social card exists, has 1200x630 dimensions, includes the product name, and includes the `npx` start command.
13
+ - Updated promo docs, release docs, production-readiness docs, roadmap, changelog, and release notes.
14
+
15
+ ## Public Interface
16
+
17
+ - No CLI command changes.
18
+ - No MCP tool changes.
19
+ - No config schema changes.
20
+ - No review rule, scoring, or verdict behavior changes.
21
+ - No GitHub Action behavior changes.
22
+ - `promo/` remains outside the npm package `files` list.
23
+
24
+ ## Release Validation
25
+
26
+ ```powershell
27
+ npm.cmd test
28
+ npm.cmd run demo:svg:check
29
+ npm.cmd run promo:card:check
30
+ npm.cmd run promo:check
31
+ npm.cmd run pack:dry
32
+ git diff --check
33
+ git diff | node .\dist\cli.js diff --fail-on block --subject "v0.1.80 social preview card"
34
+ ```
35
+
36
+ Expected:
37
+
38
+ - `promo/share-kit/social-card.svg` is deterministic and current,
39
+ - default `promo:check` includes social-card validation,
40
+ - GitHub Release and npm Publish complete from the `v0.1.80` tag.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memento-mori-jester",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
4
4
  "description": "A local court-jester sidecar for AI coding agents: review plans, commands, diffs, and final claims before they get too pleased with themselves.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -40,12 +40,15 @@
40
40
  "build": "tsc -p tsconfig.json",
41
41
  "start": "node dist/server.js",
42
42
  "start:mcp": "node dist/server.js",
43
- "test": "npm run build && node scripts/run-tests.mjs && npm run fixtures:check && npm run fixtures:report && npm run production:check",
43
+ "test": "npm run build && node scripts/run-tests.mjs && npm run fixtures:check && npm run fixtures:report && npm run promo:check && npm run production:check",
44
44
  "doctor": "node dist/cli.js doctor",
45
45
  "demo:svg": "node scripts/render-demo-svg.mjs",
46
46
  "demo:svg:check": "node scripts/render-demo-svg.mjs --check",
47
47
  "fixtures:check": "node scripts/check-fixtures.mjs",
48
48
  "fixtures:report": "node scripts/report-fixtures.mjs",
49
+ "promo:card": "node scripts/render-social-card.mjs",
50
+ "promo:card:check": "node scripts/render-social-card.mjs --check",
51
+ "promo:check": "node scripts/check-promo-freshness.mjs",
49
52
  "production:check": "node scripts/check-production-readiness.mjs",
50
53
  "pack:dry": "npm pack --dry-run",
51
54
  "prepare": "npm run build",
@@ -64,6 +64,8 @@ for (const path of [
64
64
  "docs/MAINTAINER_TRIAGE.md",
65
65
  `docs/RELEASE_NOTES_${tag}.md`,
66
66
  "action.yml",
67
+ "scripts/check-promo-freshness.mjs",
68
+ "scripts/render-social-card.mjs",
67
69
  "scripts/check-fixtures.mjs",
68
70
  "scripts/report-fixtures.mjs",
69
71
  ".github/ISSUE_TEMPLATE/bug_report.yml",
@@ -132,8 +134,14 @@ requireText("scripts/report-fixtures.mjs", /--markdown/, "Markdown fixture repor
132
134
  forbidText("scripts/report-fixtures.mjs", /src\/config\.ts|src\/types\.ts/, "source-only fixture report dependencies");
133
135
  requireText("package.json", /"fixtures:check": "node scripts\/check-fixtures\.mjs"/, "fixture authoring check script");
134
136
  requireText("package.json", /"fixtures:report": "node scripts\/report-fixtures\.mjs"/, "fixture coverage report script");
137
+ requireText("package.json", /"promo:card": "node scripts\/render-social-card\.mjs"/, "social card render script");
138
+ requireText("package.json", /"promo:card:check": "node scripts\/render-social-card\.mjs --check"/, "social card stale check script");
139
+ requireText("package.json", /"promo:check": "node scripts\/check-promo-freshness\.mjs"/, "promo freshness check script");
135
140
  requireText("package.json", /npm run fixtures:check/, "fixture authoring check in npm test");
136
141
  requireText("package.json", /npm run fixtures:report/, "fixture coverage report in npm test");
142
+ requireText("package.json", /npm run promo:check/, "promo freshness check in npm test");
143
+ requireText("scripts/check-promo-freshness.mjs", /--require-package-version/, "optional strict package-version promo check");
144
+ requireText("scripts/check-promo-freshness.mjs", /social-card\.svg/, "social-card freshness check");
137
145
  requireText("SECURITY.md", /doctor --json/, "doctor JSON redaction guidance");
138
146
  requireText("SECURITY.md", /security\/advisories\/new/, "private vulnerability report link");
139
147
  requireText(".github/ISSUE_TEMPLATE/bug_report.yml", /doctor --json/, "doctor JSON support prompt");
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const root = process.cwd();
7
+ const failures = [];
8
+ const args = new Set(process.argv.slice(2));
9
+ const requirePackageVersion = args.has("--require-package-version");
10
+
11
+ for (const arg of args) {
12
+ if (arg !== "--require-package-version") {
13
+ failures.push(`Unknown option: ${arg}`);
14
+ }
15
+ }
16
+
17
+ function read(path) {
18
+ return readFileSync(join(root, path), "utf8");
19
+ }
20
+
21
+ function readJson(path) {
22
+ return JSON.parse(read(path));
23
+ }
24
+
25
+ function escapeRegExp(value) {
26
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ }
28
+
29
+ function requireFile(path, description, minBytes = 1) {
30
+ const fullPath = join(root, path);
31
+ if (!existsSync(fullPath)) {
32
+ failures.push(`${description} is missing: ${path}`);
33
+ return;
34
+ }
35
+ const size = statSync(fullPath).size;
36
+ if (size < minBytes) {
37
+ failures.push(`${description} looks too small: ${path} (${size} bytes).`);
38
+ }
39
+ }
40
+
41
+ function requireText(path, pattern, description) {
42
+ const content = read(path);
43
+ if (!pattern.test(content)) {
44
+ failures.push(`${path} should include ${description}.`);
45
+ }
46
+ }
47
+
48
+ function failJson(path, error) {
49
+ failures.push(`${path} could not be parsed: ${error instanceof Error ? error.message : String(error)}.`);
50
+ }
51
+
52
+ function loadRiskyDomainEvidence() {
53
+ const cliPath = join(root, "dist", "cli.js");
54
+ if (!existsSync(cliPath)) {
55
+ failures.push("dist/cli.js is missing; run `npm run build` before `npm run promo:check`.");
56
+ return null;
57
+ }
58
+
59
+ const result = spawnSync(
60
+ process.execPath,
61
+ [cliPath, "tune", "risky-domain", "--json", "--no-config"],
62
+ { cwd: root, encoding: "utf8" }
63
+ );
64
+
65
+ if (result.status !== 0) {
66
+ failures.push(`Could not load risky-domain fixture evidence: ${result.stderr || result.stdout}`.trim());
67
+ return null;
68
+ }
69
+
70
+ try {
71
+ const parsed = JSON.parse(result.stdout);
72
+ return parsed.fixtureEvidence ?? null;
73
+ } catch (error) {
74
+ failures.push(`Could not parse risky-domain fixture evidence JSON: ${error instanceof Error ? error.message : String(error)}.`);
75
+ return null;
76
+ }
77
+ }
78
+
79
+ let packageJson;
80
+ try {
81
+ packageJson = readJson("package.json");
82
+ } catch (error) {
83
+ failJson("package.json", error);
84
+ packageJson = {};
85
+ }
86
+
87
+ let fixtures;
88
+ try {
89
+ fixtures = readJson("examples/fixtures/preset-review-cases.json");
90
+ } catch (error) {
91
+ failJson("examples/fixtures/preset-review-cases.json", error);
92
+ fixtures = [];
93
+ }
94
+
95
+ const promoReadmePath = "promo/README.md";
96
+ const promoReadme = read(promoReadmePath);
97
+ const currentVideo = promoReadme.match(/Final vertical demo video:\s*\[([^\]]+)\]\(([^)]+)\)/);
98
+
99
+ if (!currentVideo) {
100
+ failures.push(`${promoReadmePath} should link the final vertical demo video.`);
101
+ }
102
+
103
+ const linkedLabel = currentVideo?.[1] ?? "";
104
+ const linkedTarget = currentVideo?.[2] ?? "";
105
+ if (linkedLabel && linkedTarget && linkedLabel !== linkedTarget) {
106
+ failures.push(`${promoReadmePath} video link label and target should match.`);
107
+ }
108
+
109
+ const videoMatch = linkedTarget.match(/^x-demo-v(\d+\.\d+\.\d+)\/renders\/memento-mori-jester-x-demo-v\1\.mp4$/);
110
+ if (!videoMatch) {
111
+ failures.push(`${promoReadmePath} video should point at x-demo-vX.Y.Z/renders/memento-mori-jester-x-demo-vX.Y.Z.mp4.`);
112
+ }
113
+
114
+ const promoVersion = videoMatch?.[1] ?? "unknown";
115
+ const demoId = `x-demo-v${promoVersion}`;
116
+ const demoDir = `promo/${demoId}`;
117
+ const demoVideoPath = linkedTarget ? `promo/${linkedTarget}` : "";
118
+ const fixtureTotal = Array.isArray(fixtures) ? fixtures.length : 0;
119
+ const riskyEvidence = loadRiskyDomainEvidence();
120
+ const socialCardPath = "promo/share-kit/social-card.svg";
121
+
122
+ if (requirePackageVersion && packageJson.version !== promoVersion) {
123
+ failures.push(`Current promo version v${promoVersion} should match package.json ${packageJson.version} when --require-package-version is used.`);
124
+ }
125
+
126
+ if (promoVersion !== "unknown") {
127
+ requireFile(`${demoDir}/index.html`, "current promo demo HTML");
128
+ requireFile(`${demoDir}/README.md`, "current promo demo README");
129
+ requireFile(`${demoDir}/package.json`, "current promo demo package.json");
130
+ requireFile(`${demoDir}/package-lock.json`, "current promo demo package-lock.json");
131
+ requireFile(`${demoDir}/meta.json`, "current promo demo metadata");
132
+ requireFile(demoVideoPath, "current promo demo video", 100_000);
133
+
134
+ for (const still of ["01-opener.jpg", "02-command-block.jpg", "03-tuning-evidence.jpg", "04-try-it.jpg"]) {
135
+ requireFile(`promo/share-kit/stills/${still}`, `share-kit still ${still}`, 50_000);
136
+ }
137
+ requireFile(socialCardPath, "social preview card", 2_000);
138
+ requireText(socialCardPath, /width="1200" height="630"/, "1200x630 social-card dimensions");
139
+ requireText(socialCardPath, /Memento Mori Jester/, "product name");
140
+ requireText(socialCardPath, /npx -y memento-mori-jester@latest start/, "start command");
141
+ requireText("promo/share-kit/README.md", /social-card\.svg/, "social-card asset row");
142
+
143
+ try {
144
+ const demoPackage = readJson(`${demoDir}/package.json`);
145
+ if (demoPackage.name !== demoId) {
146
+ failures.push(`${demoDir}/package.json name should be ${demoId}.`);
147
+ }
148
+ } catch (error) {
149
+ failJson(`${demoDir}/package.json`, error);
150
+ }
151
+
152
+ try {
153
+ const demoLock = readJson(`${demoDir}/package-lock.json`);
154
+ if (demoLock.name !== demoId || demoLock.packages?.[""]?.name !== demoId) {
155
+ failures.push(`${demoDir}/package-lock.json root name should be ${demoId}.`);
156
+ }
157
+ } catch (error) {
158
+ failJson(`${demoDir}/package-lock.json`, error);
159
+ }
160
+
161
+ try {
162
+ const demoMeta = readJson(`${demoDir}/meta.json`);
163
+ if (demoMeta.id !== demoId || demoMeta.name !== demoId) {
164
+ failures.push(`${demoDir}/meta.json id and name should be ${demoId}.`);
165
+ }
166
+ } catch (error) {
167
+ failJson(`${demoDir}/meta.json`, error);
168
+ }
169
+
170
+ const escapedVersion = escapeRegExp(promoVersion);
171
+ requireText(`${demoDir}/README.md`, new RegExp(`# Memento Mori Jester X Demo v${escapedVersion}`), `demo title v${promoVersion}`);
172
+ requireText(`${demoDir}/README.md`, new RegExp(`renders/memento-mori-jester-x-demo-v${escapedVersion}\\.mp4`), "current render path");
173
+ requireText(`${demoDir}/index.html`, new RegExp(`<span>v${escapedVersion}</span>`), `visible version v${promoVersion}`);
174
+ requireText(`${demoDir}/index.html`, new RegExp(`PASS package-version: ${escapedVersion}`), `doctor package version ${promoVersion}`);
175
+ requireText(`${demoDir}/index.html`, new RegExp(`<strong>${fixtureTotal}</strong>\\s*<span>fixtures checked</span>`), `${fixtureTotal} fixture count`);
176
+ requireText("promo/share-kit/README.md", new RegExp(escapeRegExp(`../${linkedTarget}`)), "current promo video path");
177
+
178
+ if (riskyEvidence) {
179
+ requireText(
180
+ `${demoDir}/index.html`,
181
+ new RegExp(`<strong>${riskyEvidence.matchCount}</strong>\\s*<span>risky-domain matches</span>`),
182
+ `${riskyEvidence.matchCount} risky-domain match count`
183
+ );
184
+ requireText(
185
+ `${demoDir}/index.html`,
186
+ new RegExp(`<strong>${riskyEvidence.quietPassCount}</strong>\\s*<span>quiet-pass examples</span>`),
187
+ `${riskyEvidence.quietPassCount} quiet-pass count`
188
+ );
189
+ requireText("docs/DEMO.md", new RegExp(`Total fixtures checked: ${riskyEvidence.totalFixtures}`), "current tune fixture total");
190
+ requireText("docs/DEMO.md", new RegExp(`Matching fixtures: ${riskyEvidence.matchCount}`), "current tune match count");
191
+ requireText("docs/DEMO.md", new RegExp(`Quiet-pass fixtures: ${riskyEvidence.quietPassCount}`), "current tune quiet-pass count");
192
+ }
193
+ }
194
+
195
+ if (failures.length > 0) {
196
+ process.stderr.write("Promo freshness check failed:\n");
197
+ for (const failure of failures) {
198
+ process.stderr.write(`- ${failure}\n`);
199
+ }
200
+ process.exit(1);
201
+ }
202
+
203
+ process.stdout.write(
204
+ `Promo freshness check passed for ${demoId}: ${fixtureTotal} fixtures, ` +
205
+ `${riskyEvidence?.matchCount ?? "unknown"} risky-domain matches, ` +
206
+ `${riskyEvidence?.quietPassCount ?? "unknown"} quiet-pass examples.\n`
207
+ );
@@ -0,0 +1,105 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
6
+ const targetPath = resolve(root, "promo/share-kit/social-card.svg");
7
+ const mode = process.argv.includes("--check") ? "check" : "write";
8
+
9
+ const width = 1200;
10
+ const height = 630;
11
+
12
+ const copy = {
13
+ title: "Memento Mori Jester",
14
+ eyebrow: "For AI agents with ambition",
15
+ headline: ["A local court jester", "for AI coding agents."],
16
+ detail: ["Reviews commands, plans, diffs, and final answers", "before the agent gets too pleased with itself."],
17
+ command: "npx -y memento-mori-jester@latest start",
18
+ footer: "Codex | Claude Code | MCP | hooks | CI"
19
+ };
20
+
21
+ function escapeXml(value) {
22
+ return value
23
+ .replaceAll("&", "&amp;")
24
+ .replaceAll("<", "&lt;")
25
+ .replaceAll(">", "&gt;")
26
+ .replaceAll("\"", "&quot;");
27
+ }
28
+
29
+ function text(value) {
30
+ return escapeXml(value);
31
+ }
32
+
33
+ function renderSvg() {
34
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="title desc">
35
+ <title id="title">${text(copy.title)} social preview card</title>
36
+ <desc id="desc">${text(copy.detail.join(" "))}</desc>
37
+ <defs>
38
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
39
+ <stop offset="0%" stop-color="#11100e"/>
40
+ <stop offset="58%" stop-color="#171410"/>
41
+ <stop offset="100%" stop-color="#211b14"/>
42
+ </linearGradient>
43
+ <radialGradient id="heat" cx="78%" cy="16%" r="70%">
44
+ <stop offset="0%" stop-color="#d7a642" stop-opacity="0.36"/>
45
+ <stop offset="42%" stop-color="#d64b3f" stop-opacity="0.12"/>
46
+ <stop offset="100%" stop-color="#11100e" stop-opacity="0"/>
47
+ </radialGradient>
48
+ <filter id="soft-shadow" x="-20%" y="-20%" width="140%" height="140%">
49
+ <feDropShadow dx="0" dy="24" stdDeviation="24" flood-color="#050403" flood-opacity="0.38"/>
50
+ </filter>
51
+ </defs>
52
+ <rect width="${width}" height="${height}" fill="url(#bg)"/>
53
+ <rect width="${width}" height="${height}" fill="url(#heat)"/>
54
+ <g opacity="0.09">
55
+ <path d="M0 90H1200M0 180H1200M0 270H1200M0 360H1200M0 450H1200M0 540H1200" stroke="#f2eadc" stroke-width="1"/>
56
+ <path d="M100 0V630M200 0V630M300 0V630M400 0V630M500 0V630M600 0V630M700 0V630M800 0V630M900 0V630M1000 0V630M1100 0V630" stroke="#f2eadc" stroke-width="1"/>
57
+ </g>
58
+ <g transform="translate(875 74)" opacity="0.55">
59
+ <circle cx="150" cy="150" r="145" fill="none" stroke="#d7a642" stroke-width="3" opacity="0.55"/>
60
+ <circle cx="150" cy="150" r="95" fill="none" stroke="#d64b3f" stroke-width="3" opacity="0.36"/>
61
+ <circle cx="150" cy="150" r="48" fill="none" stroke="#f2eadc" stroke-width="3" opacity="0.28"/>
62
+ </g>
63
+ <text x="-36" y="612" fill="#f2eadc" opacity="0.055" font-family="Georgia, serif" font-size="132" font-weight="900">remember you are shipping code</text>
64
+ <g transform="translate(74 66)">
65
+ <g transform="translate(0 0)">
66
+ <circle cx="27" cy="27" r="27" fill="#f2eadc"/>
67
+ <path d="M27 0A27 27 0 0 1 54 27H27Z" fill="#d64b3f"/>
68
+ <path d="M27 27H54A27 27 0 0 1 13 50Z" fill="#d7a642"/>
69
+ <path d="M27 0V27H0A27 27 0 0 1 27 0Z" fill="#f2eadc"/>
70
+ </g>
71
+ <text x="76" y="36" fill="#f2eadc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="25" font-weight="800" letter-spacing="4">${text(copy.title.toUpperCase())}</text>
72
+ </g>
73
+ <g transform="translate(76 166)">
74
+ <text x="0" y="0" fill="#d7a642" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="22" font-weight="700" letter-spacing="3">${text(copy.eyebrow.toUpperCase())}</text>
75
+ <text x="0" y="76" fill="#f2eadc" font-family="Georgia, serif" font-size="72" font-weight="900">${text(copy.headline[0])}</text>
76
+ <text x="0" y="146" fill="#f2eadc" font-family="Georgia, serif" font-size="72" font-weight="900">${text(copy.headline[1])}</text>
77
+ <text x="0" y="202" fill="#d8cbb5" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="25">${text(copy.detail[0])}</text>
78
+ <text x="0" y="238" fill="#d8cbb5" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="25">${text(copy.detail[1])}</text>
79
+ </g>
80
+ <g transform="translate(76 432)" filter="url(#soft-shadow)">
81
+ <rect x="0" y="0" width="812" height="88" rx="18" fill="#1b1814" stroke="#d7a642" stroke-opacity="0.48" stroke-width="2"/>
82
+ <text x="32" y="55" fill="#d7a642" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="28">$</text>
83
+ <text x="64" y="55" fill="#f2eadc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="28" font-weight="700">${text(copy.command)}</text>
84
+ </g>
85
+ <g transform="translate(76 546)">
86
+ <rect x="0" y="-28" width="650" height="48" rx="24" fill="#d64b3f"/>
87
+ <text x="28" y="3" fill="#fff3eb" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="18" font-weight="800" letter-spacing="1.5">${text(copy.footer.toUpperCase())}</text>
88
+ </g>
89
+ </svg>
90
+ `;
91
+ }
92
+
93
+ const svg = renderSvg();
94
+
95
+ if (mode === "check") {
96
+ const existing = await readFile(targetPath, "utf8");
97
+ if (existing !== svg) {
98
+ process.stderr.write("promo/share-kit/social-card.svg is stale. Run `npm run promo:card`.\n");
99
+ process.exitCode = 1;
100
+ }
101
+ } else {
102
+ await mkdir(dirname(targetPath), { recursive: true });
103
+ await writeFile(targetPath, svg, "utf8");
104
+ process.stdout.write(`Wrote ${targetPath}\n`);
105
+ }