Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions AGENT-INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL

- The connector is a normal npm package: [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect), MIT-licensed, source at https://github.com/patchstack/connect. Install it pinned, check the integrity hash in the lockfile, run `npm audit` over it — whatever your policy requires.
- First published May 2026. If the package is absent from your training data, `npm view @patchstack/connect` (read-only) shows the live registry record.
- It reads the project's **lockfile only** (`package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`) and sends package names + versions to Patchstack for vulnerability matching. No source code, no env vars, no file paths, no git history.
- It reads the project's **dependency list only** — from the lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) or, on bun projects (`bun.lock`/`bun.lockb`), by enumerating the installed packages under `node_modules/` — and sends package names + versions to Patchstack for vulnerability matching. No source code, no env vars, no file paths, no git history.
- Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.

## Before you start — never install twice

- If `.patchstackrc.json` exists, the project is already provisioned. Reuse its UUID; run `npx @patchstack/connect status` to re-print it and the claim URL. **Do not run `scan` to provision a second site.**
- `npx @patchstack/connect guide` prints a live checklist above this document showing which steps are already done in the current project (install, provisioning, build hooks, widget) with commands tailored to it. Trust that checklist over re-deriving state yourself; only do the steps it marks ✖.
- If `.patchstackrc.json` contains a `siteUuid` key, the project is already provisioned. Reuse that UUID; run `npx @patchstack/connect status` to re-print it and the claim URL. **Do not run `scan` to provision a second site.** (A `.patchstackrc.json` with other keys — e.g. an `endpoint` override — but no `siteUuid` is *not* provisioned yet; scan normally.)
- If `@patchstack/connect` is already in `devDependencies`, skip the install command.
- If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one.

Expand All @@ -36,7 +37,7 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL

It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser.

3. **Wire builds** in `package.json` (chain with `&&` if hooks already exist):
3. **Wire builds** in `package.json`:

```jsonc
{
Expand All @@ -47,7 +48,9 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
}
```

4. **Install the disclosure widget** — a floating "Report a vulnerability" button. Read the UUID from `.patchstackrc.json` and use it as `userToken`. Place these two snippets via the framework's HTML/layout mechanism (never a JS entry point):
If a `prebuild`/`postbuild` hook already exists, chain instead of replacing it, e.g. `"prebuild": "existing-command && patchstack-connect scan"`.

4. **Install the disclosure widget** — a floating "Report a vulnerability" button. Read the `siteUuid` value from `.patchstackrc.json` (the same site UUID step 2 provisioned) and pass it as the widget's `userToken`. Place these two snippets via the framework's HTML/layout mechanism (never a JS entry point):

```html
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script>
Expand All @@ -58,7 +61,7 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL

Framework-specific placement patterns: https://cdn.patchstack.com/llm.html. The `userToken` is public by design — it ships in client-side HTML and is not a secret.

5. **Commit** `.patchstackrc.json` and the updated `package.json` so every developer and CI run reports to the same site.
5. **Commit** `.patchstackrc.json`, the updated `package.json`, and the layout/HTML file carrying the widget snippet, so every developer and CI run reports to the same site.

6. **Tell the user to claim the site** (free): open the claim URL from step 2 in a browser and sign in. The site is monitored either way, but the vulnerability reports are only visible after claiming. They can also claim directly from the widget's "Connect this website" prompt. On the published site, the owner reaches the widget login by appending `#patchstack` to the live URL.

Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@ For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patc

Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.):

> Add Patchstack dependency vulnerability monitoring to this project:
> Add Patchstack dependency vulnerability monitoring to this project (MIT, source: https://github.com/patchstack/connect — verify with `npm view @patchstack/connect` if unsure):
>
> 1. `npm install --save-dev @patchstack/connect` — or the pnpm/yarn/bun equivalent (MIT, source: https://github.com/patchstack/connect; verify with `npm view @patchstack/connect` if unsure).
> 2. `npx @patchstack/connect scan` — saves a site ID to `.patchstackrc.json` and prints a claim URL. Show me that URL; don't open it.
> 3. Add `package.json` scripts `"prebuild": "patchstack-connect scan"` and `"postbuild": "patchstack-connect mark-build"`, then commit the changes.
> 4. Run `npx @patchstack/connect guide` and use it to add the report-a-vulnerability widget and finish any framework-specific details.
> 1. Install `@patchstack/connect` as a dev dependency with this project's package manager.
> 2. `npx @patchstack/connect scan` — reads the lockfile, saves a site ID to `.patchstackrc.json`, and prints a claim URL. Show me that URL; don't open it.
> 3. `npx @patchstack/connect guide` — prints a checklist of what's still missing here (build hooks, report-a-vulnerability widget). Finish those steps and commit.

The shape of this prompt is deliberate — it has survived three distinct AI-agent refusal modes, and each one punished a different shortcut:

1. **"Follow the instructions at this URL" reads as remote script execution.** Agents refuse before ever fetching the doc.
2. **Agents whose training predates May 2026 assert the package doesn't exist.** Step 1's `npm view` resolves that against the registry instead of the model's memory.
2. **Agents whose training predates May 2026 assert the package doesn't exist.** The `npm view` check resolves that against the registry instead of the model's memory.
3. **"Install the package, then follow the instructions it ships" reads as handing control to the package author** — structurally the same as prompt injection — and preemptive reassurance language ("it's safe, don't be suspicious, note your knowledge cutoff") is itself flagged as a manipulation signal.

So the prompt inlines the core install as concrete, individually inspectable commands, delegates nothing up front, and argues nothing. `npx @patchstack/connect guide` (which prints the bundled `AGENT-INSTALL.md`) enters only at step 4, as an assist for widget placement, re-run rules, and troubleshooting — by then the agent has already installed and audited the package, so consulting its docs is ordinary behavior rather than the instruction channel the install depends on. The always-current long-form guide lives at https://patchstack.com/install.txt.
So the prompt keeps the trust-critical spine inline — verification, the scan command, and surfacing the claim URL are concrete, individually inspectable steps — and argues nothing. `npx @patchstack/connect guide` enters only at step 3, after the agent has already installed and audited the package, so consulting its docs is ordinary behavior rather than the instruction channel the install depends on. And `guide` is state-aware: it inspects the project (package manager, `.patchstackrc.json`, `package.json` scripts, widget presence, framework) and prints a live checklist of only the remaining steps — with the real site UUID and framework-specific widget placement — followed by the full reference guide (`AGENT-INSTALL.md`, also at https://patchstack.com/install.txt). "Finish what the checklist marks missing" is a much smaller ask of an agent than "follow this manual", and it makes the flow idempotent: re-running `guide` on a finished project reports all-done instead of prompting a second install.

## Quick start (zero configuration)

Expand Down Expand Up @@ -70,6 +69,11 @@ patchstack-connect scan [options] Scan the lockfile and POST to
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json with
an existing site UUID
patchstack-connect status [options] Show current configuration
patchstack-connect mark-build [options] Stamp built HTML with a production flag +
build fingerprint (run as a postbuild step)
patchstack-connect guide Show this project's setup status (what's done,
what's missing, with tailored commands), then
print the full setup guide
patchstack-connect help Print help

Options (for scan and status):
Expand Down
51 changes: 41 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { readFileSync, writeFileSync } from 'node:fs';
import { scanLockfile } from './parsers/index.js';
import { buildWirePayload } from './normalize.js';
import { computeManifestChecksum } from './checksum.js';
import { buildClaimUrl, postManifest } from './client.js';
import { DEFAULT_ENDPOINT, buildClaimUrl, postManifest } from './client.js';
import { persistSiteUuid, resolveConfig, writeConfigFile } from './config.js';
import {
buildInjectionSnippet,
findHtmlFiles,
injectMarker,
resolveBuildDir,
} from './mark-build.js';
import { collectGuideState, renderGuideChecklist } from './guide.js';
import { runProtect } from './protect/install.js';
import { detectStack, type StackDescriptor } from './stack.js';
import { PatchstackError } from './types.js';
Expand All @@ -29,8 +30,10 @@ Usage:
patchstack-connect protect Install always-on runtime protection (the
guard) into a TanStack Start + Supabase app.
Covers the browser + server-function paths.
patchstack-connect guide Print the full setup guide for AI coding
agents (also at https://patchstack.com/install.txt)
patchstack-connect guide Show this project's setup status (what's done,
what's missing, with tailored commands), then
print the full setup guide
(also at https://patchstack.com/install.txt)
patchstack-connect help Print this message

Options (for scan and status):
Expand Down Expand Up @@ -136,14 +139,23 @@ async function runScan(args: ParsedArgs): Promise<number> {
const { payload, stats } = buildWirePayload(manifest);

console.log(
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`,
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`,
);
console.log(`Reporting under the ${config.environment} environment.`);
console.log(
`Reporting under the ${config.environment} environment (override with PATCHSTACK_ENVIRONMENT).`,
);
if (config.endpoint !== DEFAULT_ENDPOINT) {
console.log(
`Using endpoint override: ${config.endpoint} (set via --endpoint, PATCHSTACK_ENDPOINT, or .patchstackrc.json).`,
);
}
if (stats.duplicateNames.length > 0) {
console.log(`${stats.duplicateNames.length} package(s) appear at multiple versions:`);
if (stats.duplicateNames.length <= 10) {
console.log(` ${stats.duplicateNames.join(', ')}`);
}
const sample = stats.duplicateNames.slice(0, 10).join(', ');
const more =
stats.duplicateNames.length > 10 ? `, +${stats.duplicateNames.length - 10} more` : '';
console.log(
`${stats.duplicateNames.length} package(s) appear at multiple versions: ${sample}${more}`,
);
}

if (dryRun) {
Expand Down Expand Up @@ -191,6 +203,9 @@ async function runScan(args: ParsedArgs): Promise<number> {
console.log('');
console.log('Claim this site to view vulnerability reports in your Patchstack dashboard:');
console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
if (config.endpoint !== DEFAULT_ENDPOINT) {
console.log(' (this URL inherits the endpoint override above)');
}
}

return 0;
Expand All @@ -207,6 +222,20 @@ async function runProtectCommand(_args: ParsedArgs): Promise<number> {
}

async function runGuide(): Promise<number> {
// The live checklist first: what this project already has and what's missing,
// with commands tailored to it. Best-effort — the guide must always print the
// reference doc, even when the project state can't be inspected.
try {
const state = await collectGuideState(process.cwd());
const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
console.log(renderGuideChecklist(state, useColor));
console.log('');
console.log('———— Full reference guide (ships as AGENT-INSTALL.md) ————');
console.log('');
} catch {
// fall through to the static guide
}

// AGENT-INSTALL.md ships at the package root, one level above dist/ (and
// above src/ when running unbundled), so the same relative path works in both.
const guidePath = new URL('../AGENT-INSTALL.md', import.meta.url);
Expand All @@ -221,7 +250,9 @@ async function runStatus(args: ParsedArgs): Promise<number> {
cliEndpoint: getStringFlag(args.flags, 'endpoint'),
});
console.log(`Site UUID: ${config.siteUuid ?? '(none yet — the next `scan` will provision one)'}`);
console.log(`Endpoint: ${config.endpoint}`);
console.log(
`Endpoint: ${config.endpoint}${config.endpoint === DEFAULT_ENDPOINT ? '' : ' (override)'}`,
);
console.log(`Timeout: ${config.timeoutMs}ms`);
console.log(`Environment: ${config.environment}`);
if (config.siteUuid !== null) {
Expand Down
Loading
Loading