Public launch readiness: gallery + MCP server + GitHub Action + article - #43
Conversation
- Add docs/launch/ph.md: tagline options, maker comment drafts, 5-slot gallery plan with capture commands, version-signal analysis (recommends cutting 0.10.0 over 1.0.0-beta.0), and day-of checklist. - Bump JSON-LD softwareVersion on the marketing page to match the shipped npm release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@types/diff@8 is now a stub redirecting to the types bundled with diff@9 itself. npm install -g project-spine was emitting: npm warn deprecated @types/diff@8.0.0: This is a stub types definition. diff provides its own type definitions, so you do not need this installed. Also fixes an unrelated packaging mistake — it was declared under "dependencies" rather than "devDependencies", so the types were shipping to every global installer. Build + 121 tests green after removal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- hero.png (110 KB) — headless-Chromium capture of projectspine.dev above-the-fold, dev toolbar excluded. - demo.gif (1.1 MB, 1100x700) — copy of docs/demo/demo.gif; PH accepts animated GIFs in the gallery so no re-render needed. - drift-diff.png (42 KB) — VHS tape; shows `spine drift diff` output after a hand-edit to AGENTS.md. Tomorrow Night palette to match Ghostty's default. - tree.png (51 KB) — VHS tape; lists the 3 tool-discovery files at repo root + 11 compiled exports under .project-spine/exports/. Tape files are committed alongside so the terminal slots can be re-rendered deterministically whenever spine's output format changes. Slot 5 (AGENTS.md rendered in Claude Code / Cursor) still needs a real editor screenshot from your machine — documented in ph.md §3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The npm package now ships a second bin, spine-mcp, that speaks the
Model Context Protocol over stdio. Any MCP-speaking client (Claude
Code, Cursor, Continue, etc.) can drive Spine without the user
switching to a terminal.
Tools exposed:
- spine_compile (brief + repo + optional tokens/template)
- spine_drift_check (structured drift report; --json round-tripped
as structuredContent)
- spine_drift_diff (unified diffs of hand-edited exports)
- spine_init (scaffold brief.md from template)
- spine_tokens_pull (Figma Variables API; Enterprise-gated)
Resource exposed:
- spine://manifest (current cwd .project-spine/export-manifest.json)
Implementation is a thin wrapper: each tool shells out to the sibling
spine binary via process.execPath, so CLI behavior is preserved 1:1
including error messages, exit codes, and --json output. Resolution of
the CLI path falls through: installed-sibling, built-from-source,
SPINE_MCP_CLI override. In-memory transport tests cover tools/list and
two tool invocations (init happy path, drift-check on empty repo).
Dep added: @modelcontextprotocol/sdk@^1.29. All 124 tests green
(121 pre-existing + 3 new).
Setup notes: docs/mcp.md. README gets a short pointer section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Why your AI agent writes padding: 13px, and how to stop it." ~900-word article framing the two-tool pattern: Spine compiles the design-system contract into AGENTS.md / CLAUDE.md so agents know what the tokens mean; Rhythmguard enforces token usage at lint time so off-scale values fail CI. Canonical target: digitaltableteur.com/articles/agent-padding-13px Cross-post plan: dev.to (frontmatter included in file), LinkedIn (700-char cut included at the bottom). Markdown canonical form kept in this repo so it's versioned where the code lives; Petri will port to DT's TS blog-post module format at publish time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third-party consumers can now fail their own CI when AGENTS.md /
CLAUDE.md / copilot-instructions.md drift from the brief or tokens:
- uses: PetriLahdelma/project-spine/.github/actions/drift-check@v0.9.2-alpha.0
with:
fail-on: any
Composite action (no Docker). Sets up Node 20, installs
project-spine@<version> globally, runs spine drift check, exposes a
`clean` output. Inputs: version, node-version, repo-path, fail-on,
json. Full reference in the action's own README.
No workflow file added to this repo's own CI — per the "no actions
usage for myself" constraint, the Action ships for external use only.
Marketplace publication is deferred.
Also added: docs/launch/placements.md with prepared submission
content for the MCP servers registry, awesome-mcp-servers,
awesome-claude-code, cursor.directory, and awesome-copilot. Order-
of-operations note says to file these after the MCP server hits npm
in a public release.
README gets a short pointer section for the Action next to the MCP
section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lates
Replaces the VHS-simulated terminal frames with authentic Ghostty
captures via screencapture -l on the Ghostty window id. Content is
byte-identical in meaning but the frames now show real Ghostty
chrome (macOS traffic lights, real font rendering, the user's actual
terminal emulator) instead of a synthesised approximation.
Gallery is now 6 assets for 5 PH slots — pick 5:
- hero.png (unchanged — Chromium on projectspine.dev)
- demo.gif (unchanged — the 60-s VHS demo GIF)
- drift-diff.png (NEW, Ghostty — spine drift diff output)
- tree.png (NEW, Ghostty — 3 discovery files + 11 exports)
- templates.png (NEW, Ghostty — spine template list, 6 templates)
- changelog.png (NEW, Chromium — /changelog page)
- claude.png (NEW, Ghostty + claude CLI — "Read AGENTS.md
and summarise" round-tripping to a real
3-bullet answer; closes the brief → compile →
AGENTS.md → agent-consumes-it loop)
Recommended upload order documented in ph.md §3. claude.png is the
distinctive asset — it proves the downstream value instead of
describing it, which is the slot the gallery was missing.
Capture scripts are checked in next to their outputs
(capture-*.sh) so any slot can be regenerated deterministically.
The legacy VHS .tape and intermediate .gif files are removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Tagline: #5, "The missing context layer for software delivery" (48 chars, same as site tagline — keep voice consistent). - Mobile LCP on production: 2.6 s (2026-04-23), down from 3.5 s before PRs #39 + #41. 0.1 s over the 2.5 s Good cutoff but overall Performance 97, A11y 96, Best Practices 96, SEO 100. Shipping without the WebP fallback; revisit post-launch if needed. Open items in ph.md: version decision (alpha vs 0.10.0 vs 1.0.0-beta) and maker comment (A vs B). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Maker comment: Draft A (warm, ~155 words). Post within 15 min. - Version: cut 0.10.0 on launch-day morning via `npm version minor`, not before — keeps the readiness PRs simple and drops the alpha drag right when it matters. - Hunter: self-hunt. Matches OSS-only posture. - Target launch: Wed 2026-04-29, 00:01 PT. 6 days out, Tue/Wed is the highest-converting PH window. All §7 open questions resolved. ph.md is now ready-to-execute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe PR introduces a new MCP (Model Context Protocol) server that exposes Project Spine CLI operations as tools and resources, adds a GitHub Action for drift checking in CI workflows, and includes comprehensive documentation, tests, and launch materials for the new Changes
Sequence Diagram(s)sequenceDiagram
participant Client as MCP Client<br/>(Claude/Cursor)
participant MCPServer as MCP Server<br/>(spine-mcp)
participant SpineCLI as Spine CLI
participant FileSystem as File System
Client->>MCPServer: Call spine_compile tool
activate MCPServer
MCPServer->>SpineCLI: spawn("spine compile --repo ...")
activate SpineCLI
SpineCLI->>FileSystem: Read brief.md, tokens, repo
FileSystem-->>SpineCLI: File contents
SpineCLI->>FileSystem: Write compiled outputs to .project-spine/
FileSystem-->>SpineCLI: Success
SpineCLI-->>MCPServer: Exit code 0, stdout (--json payload)
deactivate SpineCLI
MCPServer->>MCPServer: shapeResult(): Parse JSON, set isError=false
MCPServer-->>Client: Response with structured + text content
deactivate MCPServer
Client->>MCPServer: Access spine://manifest resource
activate MCPServer
MCPServer->>FileSystem: Read .project-spine/export-manifest.json
FileSystem-->>MCPServer: JSON manifest
MCPServer-->>Client: Manifest content
deactivate MCPServer
Client->>MCPServer: Call spine_drift_check tool
activate MCPServer
MCPServer->>SpineCLI: spawn("spine drift check --repo ... --json --fail-on any")
activate SpineCLI
SpineCLI->>FileSystem: Read exports, compare to source
FileSystem-->>SpineCLI: File comparison result
SpineCLI-->>MCPServer: Exit code 1, stdout (drift report JSON)
deactivate SpineCLI
MCPServer->>MCPServer: shapeResult(): Parse JSON, set isError=true
MCPServer-->>Client: Response with drift errors + structured content
deactivate MCPServer
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes The MCP server implementation spans two files with 392 lines of new logic including MCP protocol integration, multiple tool definitions with JSON handling, resource routing, and error shaping. While documentation and shell scripts are straightforward, the server code requires careful review of MCP SDK usage, CLI argument assembly, process spawning/timeout enforcement, and test coverage validation. Moderate heterogeneity with concentrated complexity in the core MCP implementation. Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR bundles Product Hunt launch readiness updates together with a new MCP server surface and a reusable GitHub Action, expanding how Project Spine can be driven from AI clients and CI.
Changes:
- Add a new
spine-mcpstdio MCP server that wraps key CLI commands as MCP tools, with tests and docs. - Add a composite GitHub Action to run
spine drift checkin downstream CI. - Add launch kit docs/assets (PH plan, placements, gallery capture scripts) plus a marketing JSON-LD version bump and README updates.
Reviewed changes
Copilot reviewed 16 out of 24 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/mcp/spawn.ts |
Child-process runner for invoking the Spine CLI from the MCP server. |
src/mcp/server.ts |
MCP server implementation exposing Spine CLI capabilities as MCP tools/resources. |
src/mcp/server.test.ts |
Vitest smoke tests for MCP tool registration and a couple of core tool flows. |
package.json |
Adds spine-mcp bin and MCP SDK dependency; removes deprecated @types/diff. |
package-lock.json |
Lockfile updates for MCP SDK dependency tree. |
docs/mcp.md |
End-user setup and usage documentation for the MCP server. |
README.md |
Adds MCP server and drift-check GitHub Action documentation sections. |
.github/actions/drift-check/action.yml |
Composite Action definition for running spine drift check in CI. |
.github/actions/drift-check/README.md |
Usage docs for the drift-check composite action. |
site/app/(marketing)/page.tsx |
Bumps JSON-LD softwareVersion on the marketing landing page. |
docs/launch/ph.md |
Product Hunt launch kit planning doc (tagline, gallery plan, checklist, date). |
docs/launch/placements.md |
Prepared registry/awesome-list submission copy for post-release distribution. |
docs/launch/gallery/drift-diff.png |
New gallery asset for PH. |
docs/launch/gallery/capture-tree.sh |
Script to regenerate gallery “tree” capture. |
docs/launch/gallery/capture-templates.sh |
Script to regenerate template list capture. |
docs/launch/gallery/capture-drift-diff.sh |
Script to regenerate drift-diff capture. |
docs/launch/gallery/capture-claude.sh |
Script to regenerate Claude “money shot” capture. |
docs/articles/agent-padding-13px.md |
Draft article content for ecosystem marketing/cross-posting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| // Only run when invoked as a binary. Allows importing buildServer() from tests. | ||
| const invokedDirectly = import.meta.url === `file://${process.argv[1]}`; |
There was a problem hiding this comment.
The invokedDirectly check is comparing import.meta.url to a manually constructed file://${process.argv[1]} string. When process.argv[1] is an absolute path (typical), this produces file:////... (extra slash) and won’t match import.meta.url, so main() may never run when spine-mcp is invoked as a binary. Use fileURLToPath(import.meta.url) === process.argv[1] or compare against pathToFileURL(process.argv[1]).href instead.
| const invokedDirectly = import.meta.url === `file://${process.argv[1]}`; | |
| const invokedDirectly = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]; |
| if (parsed && typeof parsed === "object" && parsed !== null) { | ||
| block.structuredContent = parsed as Record<string, unknown>; | ||
| } |
There was a problem hiding this comment.
shapeResult only attaches structuredContent when parsed JSON is a non-null object. Valid JSON stdout can also be an array (or a primitive), and in those cases the parsed payload is silently dropped even when jsonStdout: true. Consider allowing arrays too (e.g., Array.isArray(parsed)), or attaching any successfully parsed JSON value.
| const timer = setTimeout(() => { | ||
| timedOut = true; | ||
| child.kill("SIGTERM"); | ||
| }, timeoutMs); |
There was a problem hiding this comment.
Timeout handling sets timedOut = true and sends SIGTERM, but the promise only rejects on the child's close event. If the child ignores SIGTERM (or is stuck), close may never fire and the MCP tool call can hang indefinitely despite the timeout. Consider rejecting immediately on timeout and/or escalating to SIGKILL after a short grace period to guarantee termination.
| FLAGS="--repo '${{ inputs.repo-path }}' --fail-on '${{ inputs.fail-on }}'" | ||
| if [ "${{ inputs.json }}" = "true" ]; then | ||
| FLAGS="$FLAGS --json" | ||
| fi | ||
| # shellcheck disable=SC2086 | ||
| eval spine drift check $FLAGS |
There was a problem hiding this comment.
This action builds a shell command via a string and executes it with eval. Because repo-path / fail-on / json are interpolated into that string, this pattern is vulnerable to argument-breaking and command injection if any input contains shell metacharacters (including single quotes). Prefer invoking spine directly with properly quoted arguments (and conditional flags) rather than using eval.
| FLAGS="--repo '${{ inputs.repo-path }}' --fail-on '${{ inputs.fail-on }}'" | |
| if [ "${{ inputs.json }}" = "true" ]; then | |
| FLAGS="$FLAGS --json" | |
| fi | |
| # shellcheck disable=SC2086 | |
| eval spine drift check $FLAGS | |
| if [ "${{ inputs.json }}" = "true" ]; then | |
| spine drift check --repo "${{ inputs.repo-path }}" --fail-on "${{ inputs.fail-on }}" --json | |
| else | |
| spine drift check --repo "${{ inputs.repo-path }}" --fail-on "${{ inputs.fail-on }}" | |
| fi |
| Fail your own CI when `AGENTS.md` / `CLAUDE.md` / `copilot-instructions.md` drift from the brief or tokens. No release to Marketplace yet — reference by full repo path: | ||
|
|
||
| ```yaml | ||
| - uses: PetriLahdelma/project-spine/.github/actions/drift-check@v0.9.2-alpha.0 | ||
| with: | ||
| fail-on: any | ||
| ``` |
There was a problem hiding this comment.
This README snippet pins the composite action reference to @v0.9.2-alpha.0, but the PR description states third-party consumers should reference it as .../drift-check@v0.10.0. Please align the documented ref with the intended release/tag strategy (or clarify why the README should point at 0.9.2).
| #!/bin/bash | ||
| # Gallery slot 4 — generated files, captured inside real Ghostty | ||
| set -e | ||
| export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH" |
There was a problem hiding this comment.
The capture script hardcodes a user-specific PATH (/Users/petrilahdelma/...). This makes regeneration non-portable for other contributors and leaks a local machine path into the repo. Prefer relying on the caller’s existing PATH (and documenting prerequisites), or resolve node/spine dynamically (e.g., command -v spine) without embedding an absolute home directory path.
| export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH" | |
| command -v spine >/dev/null 2>&1 || { | |
| echo "Error: 'spine' must be available on PATH." >&2 | |
| exit 1 | |
| } | |
| command -v node >/dev/null 2>&1 || { | |
| echo "Error: 'node' must be available on PATH." >&2 | |
| exit 1 | |
| } |
| #!/bin/bash | ||
| # Gallery — template catalog | ||
| set -e | ||
| export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH" |
There was a problem hiding this comment.
This capture script hardcodes a user-specific PATH (/Users/petrilahdelma/...). For reproducibility, avoid absolute home-directory paths and instead rely on the environment’s PATH (with documented prerequisites) or discover binaries dynamically.
| export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH" | |
| # Require `spine` to be available on PATH. | |
| command -v spine >/dev/null 2>&1 || { | |
| echo "Error: 'spine' must be installed and available on PATH." >&2 | |
| exit 1 | |
| } |
| export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:/Users/petrilahdelma/.local/bin:$PATH" | ||
|
|
||
| WORK=$(mktemp -d) | ||
| cd "$WORK" | ||
| spine init --template saas-marketing >/dev/null 2>&1 | ||
| printf '{"name":"acme-payroll","dependencies":{"next":"14.0.0"}}' > package.json | ||
| mkdir -p app && printf '{"compilerOptions":{"strict":true}}' > tsconfig.json | ||
| spine compile --brief ./brief.md --repo . --template saas-marketing >/dev/null 2>&1 | ||
|
|
||
| clear | ||
| # Show the prompt, then ask Claude — print mode keeps the output static | ||
| # so the terminal frame can be photographed cleanly. | ||
| echo 'acme-payroll $ claude -p "Read AGENTS.md and summarise what this project wants in 3 bullets."' | ||
| echo | ||
| claude -p "Read AGENTS.md in the current directory and summarise what this project wants in exactly 3 short bullets. Keep it under 60 words total. Start with 'Project wants to:'." |
There was a problem hiding this comment.
This capture script hardcodes a user-specific PATH (/Users/petrilahdelma/...). That makes the script non-portable and leaks a local machine path into the repo. Prefer relying on PATH + documented prerequisites, or dynamically locating spine/claude with command -v.
| export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:/Users/petrilahdelma/.local/bin:$PATH" | |
| WORK=$(mktemp -d) | |
| cd "$WORK" | |
| spine init --template saas-marketing >/dev/null 2>&1 | |
| printf '{"name":"acme-payroll","dependencies":{"next":"14.0.0"}}' > package.json | |
| mkdir -p app && printf '{"compilerOptions":{"strict":true}}' > tsconfig.json | |
| spine compile --brief ./brief.md --repo . --template saas-marketing >/dev/null 2>&1 | |
| clear | |
| # Show the prompt, then ask Claude — print mode keeps the output static | |
| # so the terminal frame can be photographed cleanly. | |
| echo 'acme-payroll $ claude -p "Read AGENTS.md and summarise what this project wants in 3 bullets."' | |
| echo | |
| claude -p "Read AGENTS.md in the current directory and summarise what this project wants in exactly 3 short bullets. Keep it under 60 words total. Start with 'Project wants to:'." | |
| SPINE_BIN=$(command -v spine) || { | |
| echo "Error: spine must be installed and available on PATH." >&2 | |
| exit 1 | |
| } | |
| CLAUDE_BIN=$(command -v claude) || { | |
| echo "Error: claude must be installed and available on PATH." >&2 | |
| exit 1 | |
| } | |
| WORK=$(mktemp -d) | |
| cd "$WORK" | |
| "$SPINE_BIN" init --template saas-marketing >/dev/null 2>&1 | |
| printf '{"name":"acme-payroll","dependencies":{"next":"14.0.0"}}' > package.json | |
| mkdir -p app && printf '{"compilerOptions":{"strict":true}}' > tsconfig.json | |
| "$SPINE_BIN" compile --brief ./brief.md --repo . --template saas-marketing >/dev/null 2>&1 | |
| clear | |
| # Show the prompt, then ask Claude — print mode keeps the output static | |
| # so the terminal frame can be photographed cleanly. | |
| echo 'acme-payroll $ claude -p "Read AGENTS.md and summarise what this project wants in 3 bullets."' | |
| echo | |
| "$CLAUDE_BIN" -p "Read AGENTS.md in the current directory and summarise what this project wants in exactly 3 short bullets. Keep it under 60 words total. Start with 'Project wants to:'." |
| #!/bin/bash | ||
| # Gallery slot 3 — drift diff, captured inside real Ghostty | ||
| set -e | ||
| export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH" |
There was a problem hiding this comment.
This capture script hardcodes a user-specific PATH (/Users/petrilahdelma/...). For reproducibility across machines/CI, avoid embedding absolute home paths; rely on the caller’s PATH (with prerequisites documented) or locate spine dynamically.
| export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH" |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
src/mcp/spawn.ts (1)
40-75: Two small runtime concerns on the child-process plumbing.
SpineRunOptions.envtypes values asstring | undefined, but the spread on L64 passes thoseundefinedvalues straight intospawn'senv. Node does not strip undefined keys from the child's env — depending on version/platform you get a literal"undefined"string. Either filter or narrow the type.- Timeout only sends
SIGTERM. If the spine CLI ever ignores or delays termination (e.g., stuck in a long sync step), the timer fires,timedOut=true, butclosemay never arrive promptly. An escalatingSIGKILLafter a grace window is cheap insurance.♻️ Proposed refactor
- const child = spawn(process.execPath, [SPINE_CLI, ...args], { - cwd: opts.cwd, - env: { ...process.env, ...opts.env }, - stdio: ["ignore", "pipe", "pipe"], - }); + const mergedEnv: Record<string, string> = { ...process.env } as Record<string, string>; + for (const [k, v] of Object.entries(opts.env ?? {})) { + if (v === undefined) delete mergedEnv[k]; + else mergedEnv[k] = v; + } + const child = spawn(process.execPath, [SPINE_CLI, ...args], { + cwd: opts.cwd, + env: mergedEnv, + stdio: ["ignore", "pipe", "pipe"], + }); @@ - const timer = setTimeout(() => { - timedOut = true; - child.kill("SIGTERM"); - }, timeoutMs); + let killTimer: NodeJS.Timeout | undefined; + const timer = setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + killTimer = setTimeout(() => child.kill("SIGKILL"), 5_000); + }, timeoutMs); @@ - child.on("close", (code) => { - clearTimeout(timer); + child.on("close", (code) => { + clearTimeout(timer); + if (killTimer) clearTimeout(killTimer);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp/spawn.ts` around lines 40 - 75, SpineRunOptions.env may contain undefined values which get passed through to spawn and can turn into the string "undefined", and runSpine's timeout only sends SIGTERM once; update runSpine to (1) sanitize opts.env by removing keys whose values are undefined before spreading into spawn (reference SpineRunOptions and the spawn call that builds env), and (2) implement an escalation: after the initial setTimeout handler sets timedOut and calls child.kill("SIGTERM"), start a short secondary timer (e.g., 1–5s grace) that calls child.kill("SIGKILL") if the process hasn't exited; ensure you clear both timers when the child exits/close and preserve existing stdout/stderr/resolve logic (reference variables timer, timedOut, and child.kill usage in runSpine).docs/launch/gallery/capture-claude.sh (1)
9-9: Hardcoded per-user absolute path makes the script non-portable.
/Users/petrilahdelma/.nvm/...and/Users/petrilahdelma/.local/bintie this to one workstation.ph.mddocuments that capture scripts are run locally by the maintainer, so this is intentional — but if anyone else (future you, a collaborator, CI smoke) tries to regenerate, it silently falls through to whateverspine/claudehappen to be on the default PATH. Consider dropping the hardcoded prefix and letting the invoking shell supply PATH, or guarding withcommand -v spine claude || { echo "…"; exit 1; }. Same comment applies tocapture-tree.shandcapture-drift-diff.sh.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/launch/gallery/capture-claude.sh` at line 9, The script export hardcodes per-user absolute paths (the export PATH line) which makes capture-claude.sh (and similarly capture-tree.sh and capture-drift-diff.sh) non-portable; remove the fixed /Users/petrilahdelma/... prefixes and either rely on the invoking shell's PATH or, if a specific user-local bin is required, reference $HOME (e.g., $HOME/.nvm/... or $HOME/.local/bin) instead of the literal path, and add a runtime guard at the script start (use command -v spine claude || { echo "spine/claude required"; exit 1; }) to fail fast when required binaries are missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/actions/drift-check/action.yml:
- Around line 43-64: Replace direct interpolation of GitHub inputs into the bash
script and remove eval: pass inputs via environment variables in the "Run spine
drift check" step (e.g., set env PROJECT_SPINE_VERSION, REPO_PATH, FAIL_ON,
JSON_FLAG) and reference them inside the script using "$PROJECT_SPINE_VERSION",
"$REPO_PATH", "$FAIL_ON", "$JSON_FLAG" to avoid code injection, update the
"Install project-spine" step to use the env var instead of `${{ inputs.version
}}`, build FLAGS using safe quoted variable expansions (e.g., FLAGS="--repo
\"$REPO_PATH\" --fail-on \"$FAIL_ON\"" and append --json only if "$JSON_FLAG" =
"true"), and invoke the command directly as spine drift check $FLAGS (no eval)
in the step with id "run" to eliminate the injection vector introduced by eval.
In `@docs/articles/agent-padding-13px.md`:
- Around line 105-108: The fenced code block showing the Cards.module.css lint
output triggers markdownlint MD040 because it lacks a language; update the block
to include a language token (e.g., change the opening triple backticks to
```text or ```console) so it becomes "```text" and keep the block contents
unchanged; ensure the same change is applied to any other similar unlabeled
fenced blocks in docs/articles/agent-padding-13px.md.
In `@docs/launch/gallery/capture-templates.sh`:
- Line 7: The export of HOME masks mktemp errors; change the single-line "export
HOME=$(mktemp -d)" to first run mktemp -d into a temp variable (e.g.,
tmp_dir=$(mktemp -d)) check its exit/status is non-empty, then assign and export
HOME (export HOME="$tmp_dir"); update the capture-templates.sh usage of HOME
accordingly and mirror this pattern where HOME is set to ensure failures aren't
silently ignored.
In `@docs/launch/ph.md`:
- Around line 54-66: Update the heading "Shipped — 6 assets for 5 slots" to
reflect the seven files listed in the table (hero.png, demo.gif, drift-diff.png,
tree.png, templates.png, changelog.png, claude.png); change the header text to
"Shipped — 7 assets for 5 slots" (or equivalent phrasing that includes
claude.png as the money-shot) so the header matches the table and PR
description.
In `@src/mcp/server.ts`:
- Around line 274-281: The direct-invocation check using import.meta.url ===
`file://${process.argv[1]}` is brittle on Windows, paths with spaces, and
symlinks; change the logic that computes invokedDirectly to resolve and compare
canonical file paths: convert import.meta.url to a path with
fileURLToPath(import.meta.url) and canonicalize both that path and
process.argv[1] with fs.realpathSync (or use import.meta.filename on Node>=22)
and then compare those resolved strings before calling main(). Ensure you update
the invokedDirectly variable and keep the existing main().catch(...) error
handling unchanged.
---
Nitpick comments:
In `@docs/launch/gallery/capture-claude.sh`:
- Line 9: The script export hardcodes per-user absolute paths (the export PATH
line) which makes capture-claude.sh (and similarly capture-tree.sh and
capture-drift-diff.sh) non-portable; remove the fixed /Users/petrilahdelma/...
prefixes and either rely on the invoking shell's PATH or, if a specific
user-local bin is required, reference $HOME (e.g., $HOME/.nvm/... or
$HOME/.local/bin) instead of the literal path, and add a runtime guard at the
script start (use command -v spine claude || { echo "spine/claude required";
exit 1; }) to fail fast when required binaries are missing.
In `@src/mcp/spawn.ts`:
- Around line 40-75: SpineRunOptions.env may contain undefined values which get
passed through to spawn and can turn into the string "undefined", and runSpine's
timeout only sends SIGTERM once; update runSpine to (1) sanitize opts.env by
removing keys whose values are undefined before spreading into spawn (reference
SpineRunOptions and the spawn call that builds env), and (2) implement an
escalation: after the initial setTimeout handler sets timedOut and calls
child.kill("SIGTERM"), start a short secondary timer (e.g., 1–5s grace) that
calls child.kill("SIGKILL") if the process hasn't exited; ensure you clear both
timers when the child exits/close and preserve existing stdout/stderr/resolve
logic (reference variables timer, timedOut, and child.kill usage in runSpine).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 19945593-fe90-4b51-880f-5d0e16de9d05
⛔ Files ignored due to path filters (8)
docs/launch/gallery/changelog.pngis excluded by!**/*.pngdocs/launch/gallery/claude.pngis excluded by!**/*.pngdocs/launch/gallery/demo.gifis excluded by!**/*.gifdocs/launch/gallery/drift-diff.pngis excluded by!**/*.pngdocs/launch/gallery/hero.pngis excluded by!**/*.pngdocs/launch/gallery/templates.pngis excluded by!**/*.pngdocs/launch/gallery/tree.pngis excluded by!**/*.pngpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (16)
.github/actions/drift-check/README.md.github/actions/drift-check/action.ymlREADME.mddocs/articles/agent-padding-13px.mddocs/launch/gallery/capture-claude.shdocs/launch/gallery/capture-drift-diff.shdocs/launch/gallery/capture-templates.shdocs/launch/gallery/capture-tree.shdocs/launch/ph.mddocs/launch/placements.mddocs/mcp.mdpackage.jsonsite/app/(marketing)/page.tsxsrc/mcp/server.test.tssrc/mcp/server.tssrc/mcp/spawn.ts
| - name: Install project-spine | ||
| shell: bash | ||
| run: npm install -g "project-spine@${{ inputs.version }}" | ||
|
|
||
| - name: Run spine drift check | ||
| id: run | ||
| shell: bash | ||
| run: | | ||
| set +e | ||
| FLAGS="--repo '${{ inputs.repo-path }}' --fail-on '${{ inputs.fail-on }}'" | ||
| if [ "${{ inputs.json }}" = "true" ]; then | ||
| FLAGS="$FLAGS --json" | ||
| fi | ||
| # shellcheck disable=SC2086 | ||
| eval spine drift check $FLAGS | ||
| EXIT_CODE=$? | ||
| if [ $EXIT_CODE -eq 0 ]; then | ||
| echo "clean=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "clean=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| exit $EXIT_CODE |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
GitHub Actions security hardening: script injection via ${{ }} expressions in run steps
💡 Result:
Script injection via ${{ }} expressions in GitHub Actions run steps occurs when untrusted inputs from contexts like github.event.pull_request.title are directly interpolated into shell scripts. GitHub Actions evaluates expressions before writing a temporary script file for execution, allowing attackers to inject commands. Vulnerable example: - name: Check PR title run: | title="${{ github.event.pull_request.title }}" if [[ $title =~ ^octocat ]]; then echo "Valid" fi An attacker sets PR title to a"; ls $GITHUB_WORKSPACE;#, injecting ls command. Hardening steps: 1. Use environment variables: Set untrusted values in env, reference with shell syntax (e.g.,
Citations:
- 1: https://docs.github.com/en/actions/concepts/security/script-injections
- 2: https://github.blog/2023-08-09-four-tips-to-keep-your-github-actions-workflows-secure
- 3: https://docs.github.com/en/actions/reference/security/secure-use
- 4: https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions?learn=getting_started&learnProduct=actions
Critical security issue: script injection via ${{ inputs.* }} directly into bash scripts.
GitHub Actions expands ${{ }} expressions before the script executes, so untrusted inputs containing quotes or shell metacharacters will break out and inject arbitrary commands. An attacker can pass version: "'; malicious_command; #" or repo-path: "'; rm -rf ~; '" to execute commands on the runner. This affects lines 45 (npm install), 52–57 (FLAGS construction), and 57 (eval call).
Additionally, eval is unnecessary here and amplifies the injection risk. Use environment variables to safely pass untrusted input, as documented in GitHub's security hardening guide for Actions.
🛡️ Proposed fix: pass inputs via env, drop eval
- name: Install project-spine
shell: bash
- run: npm install -g "project-spine@${{ inputs.version }}"
+ env:
+ SPINE_VERSION: ${{ inputs.version }}
+ run: npm install -g "project-spine@${SPINE_VERSION}"
- name: Run spine drift check
id: run
shell: bash
+ env:
+ REPO_PATH: ${{ inputs.repo-path }}
+ FAIL_ON: ${{ inputs.fail-on }}
+ EMIT_JSON: ${{ inputs.json }}
run: |
set +e
- FLAGS="--repo '${{ inputs.repo-path }}' --fail-on '${{ inputs.fail-on }}'"
- if [ "${{ inputs.json }}" = "true" ]; then
- FLAGS="$FLAGS --json"
+ args=(drift check --repo "$REPO_PATH" --fail-on "$FAIL_ON")
+ if [ "$EMIT_JSON" = "true" ]; then
+ args+=(--json)
fi
- # shellcheck disable=SC2086
- eval spine drift check $FLAGS
+ spine "${args[@]}"
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "clean=true" >> "$GITHUB_OUTPUT"
else
echo "clean=false" >> "$GITHUB_OUTPUT"
fi
exit $EXIT_CODE🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/actions/drift-check/action.yml around lines 43 - 64, Replace direct
interpolation of GitHub inputs into the bash script and remove eval: pass inputs
via environment variables in the "Run spine drift check" step (e.g., set env
PROJECT_SPINE_VERSION, REPO_PATH, FAIL_ON, JSON_FLAG) and reference them inside
the script using "$PROJECT_SPINE_VERSION", "$REPO_PATH", "$FAIL_ON",
"$JSON_FLAG" to avoid code injection, update the "Install project-spine" step to
use the env var instead of `${{ inputs.version }}`, build FLAGS using safe
quoted variable expansions (e.g., FLAGS="--repo \"$REPO_PATH\" --fail-on
\"$FAIL_ON\"" and append --json only if "$JSON_FLAG" = "true"), and invoke the
command directly as spine drift check $FLAGS (no eval) in the step with id "run"
to eliminate the injection vector introduced by eval.
| ``` | ||
| Cards.module.css:12:3 × Use a value on the rhythmic-4 scale. | ||
| Expected 12 or 16, got 13. Run `--fix` to normalize. | ||
| ``` |
There was a problem hiding this comment.
Add a language to the fenced code block.
markdownlint MD040 flags this block. Use text (or console) so cross-posts to dev.to render consistently.
📝 Proposed fix
-```
+```text
Cards.module.css:12:3 × Use a value on the rhythmic-4 scale.
Expected 12 or 16, got 13. Run `--fix` to normalize.🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 105-105: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/articles/agent-padding-13px.md` around lines 105 - 108, The fenced code
block showing the Cards.module.css lint output triggers markdownlint MD040
because it lacks a language; update the block to include a language token (e.g.,
change the opening triple backticks to ```text or ```console) so it becomes
"```text" and keep the block contents unchanged; ensure the same change is
applied to any other similar unlabeled fenced blocks in
docs/articles/agent-padding-13px.md.
| export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH" | ||
| # Use a clean HOME so user-local templates under ~/.project-spine/templates | ||
| # don't leak into the capture. | ||
| export HOME=$(mktemp -d) |
There was a problem hiding this comment.
SC2155: mktemp -d exit status is masked by export.
Because export always returns 0, a failure of mktemp -d would silently set HOME to empty and defeat set -e. Split the assignment.
📝 Proposed fix
-export HOME=$(mktemp -d)
+HOME=$(mktemp -d)
+export HOMEThe same pattern applies in capture-claude.sh / capture-tree.sh (WORK=$(mktemp -d) there is already separate, so fine — only HOME needs fixing here).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export HOME=$(mktemp -d) | |
| HOME=$(mktemp -d) | |
| export HOME |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 7-7: Declare and assign separately to avoid masking return values.
(SC2155)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/launch/gallery/capture-templates.sh` at line 7, The export of HOME masks
mktemp errors; change the single-line "export HOME=$(mktemp -d)" to first run
mktemp -d into a temp variable (e.g., tmp_dir=$(mktemp -d)) check its
exit/status is non-empty, then assign and export HOME (export HOME="$tmp_dir");
update the capture-templates.sh usage of HOME accordingly and mirror this
pattern where HOME is set to ensure failures aren't silently ignored.
| ### Shipped — 6 assets for 5 slots | ||
|
|
||
| All 1270×760. Terminal slots captured in real Ghostty (not simulated) via `screencapture -l <windowId>`; hero and changelog via headless Chromium on the live production site. Pick the 5 strongest for the PH upload. | ||
|
|
||
| | File | What it shows | Size | Source | | ||
| |---|---|---|---| | ||
| | [`hero.png`](gallery/hero.png) | Site hero: wordmark + "The context layer your coding agents are missing." | 108 KB | Headless Chromium on `projectspine.dev` | | ||
| | [`demo.gif`](gallery/demo.gif) | 60-s CLI demo: init → compile → drift check → drift diff | 1.1 MB, 1100×700 | `docs/demo/demo.tape` (VHS) | | ||
| | [`drift-diff.png`](gallery/drift-diff.png) | `spine drift diff` showing the `+# hand edit` that slipped in | 60 KB | Ghostty, `capture-drift-diff.sh` | | ||
| | [`tree.png`](gallery/tree.png) | The 3 tool-discovery files + 11 compiled exports (the "19 files per compile" proof) | 128 KB | Ghostty, `capture-tree.sh` | | ||
| | [`templates.png`](gallery/templates.png) | `spine template list` — all 6 bundled templates with descriptions | 136 KB | Ghostty, `capture-templates.sh` | | ||
| | [`changelog.png`](gallery/changelog.png) | `/changelog` page: "What shipped, when, and what changed" with the 0.9.x-alpha aside | 112 KB | Headless Chromium | | ||
| | [`claude.png`](gallery/claude.png) | **The money shot.** `claude -p "Read AGENTS.md and summarise…"` answering with a 3-bullet summary pulled from the Spine-generated AGENTS.md. Closes the loop: brief → compile → AGENTS.md → real Claude consumes it. | 128 KB | Ghostty + claude CLI, `capture-claude.sh` | |
There was a problem hiding this comment.
Asset count mismatch: header says 6, table lists 7.
The heading "Shipped — 6 assets for 5 slots" conflicts with the table, which enumerates 7 files (hero, demo.gif, drift-diff, tree, templates, changelog, claude). The PR description also calls out 6 + claude.png as the money-shot, so the table is correct — update the heading.
📝 Proposed fix
-### Shipped — 6 assets for 5 slots
+### Shipped — 7 assets for 5 slots📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### Shipped — 6 assets for 5 slots | |
| All 1270×760. Terminal slots captured in real Ghostty (not simulated) via `screencapture -l <windowId>`; hero and changelog via headless Chromium on the live production site. Pick the 5 strongest for the PH upload. | |
| | File | What it shows | Size | Source | | |
| |---|---|---|---| | |
| | [`hero.png`](gallery/hero.png) | Site hero: wordmark + "The context layer your coding agents are missing." | 108 KB | Headless Chromium on `projectspine.dev` | | |
| | [`demo.gif`](gallery/demo.gif) | 60-s CLI demo: init → compile → drift check → drift diff | 1.1 MB, 1100×700 | `docs/demo/demo.tape` (VHS) | | |
| | [`drift-diff.png`](gallery/drift-diff.png) | `spine drift diff` showing the `+# hand edit` that slipped in | 60 KB | Ghostty, `capture-drift-diff.sh` | | |
| | [`tree.png`](gallery/tree.png) | The 3 tool-discovery files + 11 compiled exports (the "19 files per compile" proof) | 128 KB | Ghostty, `capture-tree.sh` | | |
| | [`templates.png`](gallery/templates.png) | `spine template list` — all 6 bundled templates with descriptions | 136 KB | Ghostty, `capture-templates.sh` | | |
| | [`changelog.png`](gallery/changelog.png) | `/changelog` page: "What shipped, when, and what changed" with the 0.9.x-alpha aside | 112 KB | Headless Chromium | | |
| | [`claude.png`](gallery/claude.png) | **The money shot.** `claude -p "Read AGENTS.md and summarise…"` answering with a 3-bullet summary pulled from the Spine-generated AGENTS.md. Closes the loop: brief → compile → AGENTS.md → real Claude consumes it. | 128 KB | Ghostty + claude CLI, `capture-claude.sh` | | |
| ### Shipped — 7 assets for 5 slots | |
| All 1270×760. Terminal slots captured in real Ghostty (not simulated) via `screencapture -l <windowId>`; hero and changelog via headless Chromium on the live production site. Pick the 5 strongest for the PH upload. | |
| | File | What it shows | Size | Source | | |
| |---|---|---|---| | |
| | [`hero.png`](gallery/hero.png) | Site hero: wordmark + "The context layer your coding agents are missing." | 108 KB | Headless Chromium on `projectspine.dev` | | |
| | [`demo.gif`](gallery/demo.gif) | 60-s CLI demo: init → compile → drift check → drift diff | 1.1 MB, 1100×700 | `docs/demo/demo.tape` (VHS) | | |
| | [`drift-diff.png`](gallery/drift-diff.png) | `spine drift diff` showing the `+# hand edit` that slipped in | 60 KB | Ghostty, `capture-drift-diff.sh` | | |
| | [`tree.png`](gallery/tree.png) | The 3 tool-discovery files + 11 compiled exports (the "19 files per compile" proof) | 128 KB | Ghostty, `capture-tree.sh` | | |
| | [`templates.png`](gallery/templates.png) | `spine template list` — all 6 bundled templates with descriptions | 136 KB | Ghostty, `capture-templates.sh` | | |
| | [`changelog.png`](gallery/changelog.png) | `/changelog` page: "What shipped, when, and what changed" with the 0.9.x-alpha aside | 112 KB | Headless Chromium | | |
| | [`claude.png`](gallery/claude.png) | **The money shot.** `claude -p "Read AGENTS.md and summarise…"` answering with a 3-bullet summary pulled from the Spine-generated AGENTS.md. Closes the loop: brief → compile → AGENTS.md → real Claude consumes it. | 128 KB | Ghostty + claude CLI, `capture-claude.sh` | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/launch/ph.md` around lines 54 - 66, Update the heading "Shipped — 6
assets for 5 slots" to reflect the seven files listed in the table (hero.png,
demo.gif, drift-diff.png, tree.png, templates.png, changelog.png, claude.png);
change the header text to "Shipped — 7 assets for 5 slots" (or equivalent
phrasing that includes claude.png as the money-shot) so the header matches the
table and PR description.
| // Only run when invoked as a binary. Allows importing buildServer() from tests. | ||
| const invokedDirectly = import.meta.url === `file://${process.argv[1]}`; | ||
| if (invokedDirectly) { | ||
| main().catch((err) => { | ||
| process.stderr.write(`[spine-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`); | ||
| process.exit(1); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Node.js ESM detect if file is main module import.meta.url process.argv[1] robust
💡 Result:
In Node.js ES modules (ESM), the robust and official way to detect if the current file is the main module (entry point run directly via node filename.js) is to use import.meta.main, available since Node.js v24 (merged May 2025, stable by 2026). Usage: if (import.meta.main) { // This module is the main entry point. main; } This is the direct ESM equivalent of CommonJS's require.main === module. It evaluates to true only for the process's entry point module, false otherwise (including imported modules and workers in some cases). For older Node.js versions or maximum compatibility, use the path comparison method: import { fileURLToPath } from 'node:url'; import { pathToFileURL } from 'node:url'; const currentPath = fileURLToPath(import.meta.url); if (process.argv[1] === currentPath) { // Main module. } With import.meta.filename (stable since v22): if (process.argv[1] === import.meta.filename) { // Main module. } The import.meta.main approach is preferred as it's official, concise, and handles edge cases like workers and data URLs correctly. The path comparison fails if the main script is invoked without extension (e.g., node script vs node script.js) or across symlinks without realpath normalization. Third-party libraries like es-main exist but are unnecessary with native support.
Citations:
- 1: nodejs/node@430e66b9b8
- 2: esm: implement import.meta.main nodejs/node#57804
- 3: https://nodejs.org/docs/latest-v22.x/api/esm.html
- 4: https://2ality.com/2022/07/nodejs-esm-main.html
- 5: https://stackoverflow.com/questions/64180480/how-do-i-detect-if-an-es-module-is-the-main-module
- 6: https://stackoverflow.com/questions/45136831/node-js-require-main-module
🏁 Script executed:
# Check package.json for Node.js version requirements
if [ -f package.json ]; then
cat package.json | jq -r '.engines.node // "No explicit requirement"' 2>/dev/null || cat package.json | grep -A 5 '"engines"' || echo "Checking for engine field..."
fi
# Also check if there's a .nvmrc or similar
[ -f .nvmrc ] && echo ".nvmrc: $(cat .nvmrc)" || echo "No .nvmrc"
[ -f .node-version ] && echo ".node-version: $(cat .node-version)" || echo "No .node-version"Repository: PetriLahdelma/project-spine
Length of output: 106
Direct-invocation check fails on Windows and paths with spaces.
The comparison import.meta.url === \file://${process.argv[1]}`` is fragile:
- Paths with spaces / non-ASCII —
import.meta.urlpercent-encodes them (%20), but the template literal does not. - Windows —
import.meta.urluses forward slashes and proper URL format (file:///C:/...), but the template produces backslashes (file://C:\...). - symlink edge cases —
argv[1]may not be canonicalized, diverging from the resolvedimport.meta.url.
When the check fails, spine-mcp silently exits without starting the server, breaking the binary entrypoint. The proposed fix using fileURLToPath and realpathSync handles these cases correctly for Node.js >=20.
Alternatively, Node.js >=22 users can use import.meta.filename, which is simpler: if (process.argv[1] === import.meta.filename) { main().catch(...) }.
🔧 Proposed fix
+import { realpathSync } from "node:fs";
+import { fileURLToPath } from "node:url";
// ...
-// Only run when invoked as a binary. Allows importing buildServer() from tests.
-const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
+// Only run when invoked as a binary. Allows importing buildServer() from tests.
+let invokedDirectly = false;
+try {
+ invokedDirectly =
+ fileURLToPath(import.meta.url) === realpathSync(process.argv[1] ?? "");
+} catch {
+ invokedDirectly = false;
+}
if (invokedDirectly) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp/server.ts` around lines 274 - 281, The direct-invocation check using
import.meta.url === `file://${process.argv[1]}` is brittle on Windows, paths
with spaces, and symlinks; change the logic that computes invokedDirectly to
resolve and compare canonical file paths: convert import.meta.url to a path with
fileURLToPath(import.meta.url) and canonicalize both that path and
process.argv[1] with fs.realpathSync (or use import.meta.filename on Node>=22)
and then compare those resolved strings before calling main(). Ensure you update
the invokedDirectly variable and keep the existing main().catch(...) error
handling unchanged.
Summary
Public launch readiness pass for the OSS CLI, marketing assets, MCP surface, GitHub Action, and supporting article/docs.
Validation