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
31 changes: 27 additions & 4 deletions AGENT-INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ For a standalone site made of HTML, CSS, and browser JavaScript, with no package

Report this as **disclosure widget installed**, with any remaining preview or publishing step. This path does not inventory local JavaScript files or scripts loaded from a CDN, scan npm dependencies, or install runtime exploit protection. External APIs used by the page require their own server-side integration.

### JS/Node applications — the usual path

1. Check what is already done with `npx @patchstack/connect guide` (read-only). If the project is already provisioned, reuse it — see "Before you start — never install twice".
2. Install `@patchstack/connect` as a runtime dependency with the project's package manager.
3. Run `npx @patchstack/connect setup`. In a hosted builder, scope `PATCHSTACK_ENVIRONMENT=sandbox` to that command — see "Automated setup".
4. Finish any step the checklist at the end of `setup` still lists. "Automated setup" names each one.
5. Tell the person the dashboard link, which parts are active and which are not, to refresh their preview, and to deploy when they are ready.

If your tool will not run the command, see "When your tool will not run this CLI". The sections below describe what each command reads, writes, and sends.

## Command reference

Every command at a glance — what it does, whether it reads your source, what it writes, and what leaves your machine. Full behavior, flags, and edge cases follow in the sections below.
Expand Down Expand Up @@ -108,13 +118,26 @@ This is a request, not a mechanism: nothing in the install depends on it. Do it

**If the request you were given includes a claim token**, pass it exactly as given: `npx @patchstack/connect setup --claim-token <token>`. The site is then created in the person's own Patchstack account and `setup` prints their dashboard link for it — there is no sign-in step to relay. The token comes only from the person's Patchstack dashboard; never invent one, never write it to a file, never print it back. If `setup` reports that the token had expired or was not recognised, the site is not connected: hand over the dashboard link it prints instead, and tell the person they can copy a fresh prompt from the dashboard.

In a hosted builder, run setup with `PATCHSTACK_ENVIRONMENT=sandbox` scoped to the workspace process/command, ensure the CLI's on-disk edits are adopted into the platform's persisted project state, then restart any already-running preview/server process so it loads the guard. Do not persist `"environment": "sandbox"` in `.patchstackrc.json`: deployed builds use the same committed files and report `production` only when the build platform's own variables say so — its tier (Vercel, Netlify, Render, Railway, GitLab CI) or a branch named `main`, `master`, `production`, `prod`, `release` or `live` (Cloudflare Pages and Workers Builds, AWS Amplify, GitHub Actions, GitLab CI without a tier); a preview, pull request or other branch there reports `sandbox`, as does the Replit workspace, while a Replit Deployment reports `production`. A build in a project the builder generated and builds for itself (Lovable, Replit) reports `production` without an override, because the edit preview is a dev server and a build is the publish step — which is exactly why the sandbox label belongs in the workspace process and not in a file. A scan on a developer's machine, in a CI runner this does not know (`CI=true` alone), or on a platform with no such signal reports `local` on its own, and the dashboard shows that app as configured, not deployed. A positively identified static build reports runtime protection as not applicable. A bundler-only project, including plain Vite, can remain runtime unknown and receive a generic scaffold with incomplete wiring. Report that limitation; do not add an artificial server merely to make the check pass, and never describe a widget or an unwired scaffold as runtime protection.
**In a hosted builder:**

- Run setup with `PATCHSTACK_ENVIRONMENT=sandbox` scoped to the workspace process/command.
- Ensure the CLI's on-disk edits are adopted into the platform's persisted project state.
- Restart any already-running preview/server process so it loads the guard.
- Do not persist `"environment": "sandbox"` in `.patchstackrc.json`: deployed builds use the same committed files and report `production` only when the build platform's own variables say so.

**How each environment is labelled:**

- A build platform's tier (Vercel, Netlify, Render, Railway, GitLab CI) or a branch named `main`, `master`, `production`, `prod`, `release` or `live` (Cloudflare Pages and Workers Builds, AWS Amplify, GitHub Actions, GitLab CI without a tier) reports `production`; a preview, pull request or other branch there reports `sandbox`, as does the Replit workspace, while a Replit Deployment reports `production`.
- A build in a project the builder generated and builds for itself (Lovable, Replit) reports `production` without an override, because the edit preview is a dev server and a build is the publish step — which is exactly why the sandbox label belongs in the workspace process and not in a file.
- A scan on a developer's machine, in a CI runner this does not know (`CI=true` alone), or on a platform with no such signal reports `local` on its own, and the dashboard shows that app as configured, not deployed.

**What runtime protection can report:** a positively identified static build reports runtime protection as not applicable. A bundler-only project, including plain Vite, can remain runtime unknown and receive a generic scaffold with incomplete wiring. Report that limitation; do not add an artificial server merely to make the check pass, and never describe a widget or an unwired scaffold as runtime protection.

**A step the checklist still lists is yours to finish, not a result to report.** `setup` applies what it can apply safely and prints the exact edit for anything it would have had to overwrite user code to do. They are: moving `@patchstack/connect` out of `devDependencies`, the widget tag in a root layout `setup` could not edit, the production marker on a server-rendered root, and wiring a generic guard into the server entry. The last three are steps 3 and 4 of "Manual setup" below; after the guard one, `npx @patchstack/connect protect --check` must exit 0.

**A tick is "nothing owed here", not "this part is on".** The checklist marks steps this project still owes, so a part it cannot carry — or one that is switched off — is green with nothing outstanding. `No build script to integrate`, `Disclosure widget disabled by config` and `Runtime protection: not applicable` all read that way. So report what the project ended up with by name — dependency scans, the disclosure widget, the build hooks, runtime protection — and say which of them are not active and why, rather than reporting an empty checklist as a finished install. Of those, the widget is the one that can be off by setting rather than by the shape of the project: if `.patchstackrc.json` carries `"widget": false` and the person did not ask for that, tell them it is off and ask whether they want it on.

**Finish by telling the user to refresh their preview.** The widget's "Report a vulnerability" button loads with the page, so a preview that was already open still shows the HTML from before setup — the button is missing there until it reloads. Nothing in the CLI can reach the user's browser, so relaying this is your job. Phrase it as a check rather than a required step: a builder that hot reloads, or a preview server you restarted, may have refreshed it already.
**Finish by telling the user to refresh their preview.** The widget loads with the page, so a preview that was already open still shows the HTML from before setup — the widget is missing there until it reloads. Tell them what to expect after the refresh: a site that is not yet connected to an account shows the "Connect this website" panel, and the "Report a vulnerability" button takes its place once the site is claimed. A freshly set up site is unclaimed unless setup ran with a claim token. Nothing in the CLI can reach the user's browser, so relaying this is your job. Phrase it as a check rather than a required step: a builder that hot reloads, or a preview server you restarted, may have refreshed it already.

**Then tell them to deploy.** Setup changes source files, and the deployed site keeps serving its previous build until the next deploy — so visitors get no widget, and on a server-rendered root no production marker, until the user deploys (or hits Publish) again. Say it as a reminder; do not deploy anything yourself.

Expand Down Expand Up @@ -186,7 +209,7 @@ Handle it in this order:
npx @patchstack/connect scan
```

It prints a dashboard link but never opens it. Open that link in a browser to view reports. It also prints what it did about the widget — if it added the tag, reload the preview and confirm the "Report a vulnerability" button appears.
It prints a dashboard link but never opens it. Open that link in a browser to view reports. It also prints what it did about the widget — if it added the tag, reload the preview and confirm the widget appears: the "Connect this website" panel while the site is unclaimed, the "Report a vulnerability" button once it is claimed.

2. **Wire builds** in `package.json`:

Expand Down Expand Up @@ -568,7 +591,7 @@ Two more endpoints the package can call, for completeness:
- `npx @patchstack/connect status` re-prints the site UUID and dashboard URL, and checks whether the site still exists on Patchstack (`Site status: active / removed / could not be verified`).
- `npx @patchstack/connect protect --check` verifies from the source that the runtime guard is connected to the request path. It does not run the app.
- `npx @patchstack/connect protect --check --runtime` additionally **starts the app** on a loopback port and sends it one request, to establish that a request reaches the guard seam. Opt-in, and the only command that runs the application; exit `0`/`1`/`2` as described in step 4.
- Load the site in a browser — the "Report a vulnerability" button should appear. Refresh a page that was already open before the tag was added: the button only loads with the page.
- Load the site in a browser — the widget should appear: the "Connect this website" panel while the site is unclaimed, the "Report a vulnerability" button once it is claimed. Refresh a page that was already open before the tag was added: the widget only loads with the page.
- On the deployed site, the button appears only after a deploy that includes these source changes.

## Answering "is Patchstack installed?" / "is Patchstack removed?"
Expand Down
4 changes: 2 additions & 2 deletions GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ For an existing JS/Node project on a platform that can install npm packages and

When setup finishes it shows you a **dashboard URL**. Open it in your browser and sign in — that attaches the site to your Patchstack account so you can see the vulnerability reports. That's the only manual step.

Then look at your preview. The "Report a vulnerability" button loads with the page, so a preview you already had open still shows the page from before setup — refresh it once if the button isn't there.
Then look at your preview. The widget loads with the page, so a preview you already had open still shows the page from before setup — refresh it once if the widget isn't there. Until the site is attached to your account it shows a "Connect this website" panel; once it is, that becomes the "Report a vulnerability" button.

When you are happy with it, deploy (or hit Publish). Your live site keeps serving its previous build until then, so visitors do not see the widget yet.

Expand Down Expand Up @@ -45,7 +45,7 @@ Run commands from the application's package directory. In a hosted workspace, se
- `npx @patchstack/connect status` prints a site UUID and dashboard URL.
- You've opened the dashboard URL in your browser and the site shows in your Patchstack dashboard.
- `npx @patchstack/connect guide` reports the expected build hooks and widget, and `npx @patchstack/connect protect --check` confirms the guard's source wiring. A client-only or static project can report runtime protection as not applicable; describe it as dependency monitoring and a disclosure widget, not runtime protection. A source check alone does not prove deployed traffic reaches the guard.
- Your preview shows the "Report a vulnerability" button (refresh it once if it does not).
- Your preview shows the widget (refresh it once if it does not): the "Connect this website" panel before the site is attached to your account, the "Report a vulnerability" button after.
- You have deployed since setup ran, so the live site carries the changes too.
- `.patchstackrc.json`, `package.json`, the package manager's lockfile, and the generated guard/framework and widget source changes are saved in the platform's persisted project state and committed, so teammates and CI receive the same setup.
- `.patchstackrc.local.json` is **not** committed. It holds the API key; setup adds it to `.gitignore`. Teammates and CI get the credential from `PATCHSTACK_API_KEY` instead.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ That's it. `setup`:
2. POSTs the package list to Patchstack with **no** UUID.
3. Patchstack provisions a fresh site and returns its UUID.
4. The connector writes the UUID to `.patchstackrc.json` so the next `scan` targets the same site.
5. The connector installs the disclosure widget's `<script>` tag into your root HTML shell (see *The disclosure widget* below) so the "Report a vulnerability" button shows up on the next preview reload. On a server-rendered root it also adds the production marker, which is what tells the widget to switch from build mode to visitor report intake on the published site.
5. The connector installs the disclosure widget's `<script>` tag into your root HTML shell (see *The disclosure widget* below) so the widget shows up on the next preview reload — as the "Connect this website" panel until the site is claimed, then as the "Report a vulnerability" button. On a server-rendered root it also adds the production marker, which is what tells the widget to switch from build mode to visitor report intake on the published site.
6. Installs the runtime guard after provisioning, bakes the site UUID into it, and verifies the framework seam. Known server stacks are auto-wired; unmatched or conflicting layouts get a generic scaffold and exact manual checks.
7. Adds `postinstall: patchstack-connect scan`, preserving any existing command, so dependencies added during a sandbox session and build-less production installs are reported immediately.
8. Wires `scan` before builds and `mark-build` after builds, preserving existing commands and using direct build chaining for Bun.
9. Prints a dashboard link — open it in a browser to attach the new site to your Patchstack account. You can re-display it any time with `npx @patchstack/connect status`.

Then **refresh your preview**. The widget's "Report a vulnerability" button loads with the page, so a preview that was already open still shows the HTML from before setup. Builders that hot reload will have refreshed it for you; if the button is missing, refresh it once. `setup` prints the same reminder, and the CLI has no way to reload a browser itself.
Then **refresh your preview**. The widget loads with the page, so a preview that was already open still shows the HTML from before setup. Builders that hot reload will have refreshed it for you; if the widget is missing, refresh it once. Until the site is claimed it shows the "Connect this website" panel; the "Report a vulnerability" button takes its place once it is. `setup` prints the same reminder, and the CLI has no way to reload a browser itself.

Then **deploy**. These are source changes, so your live site keeps serving its previous build — visitors get the widget, and a server-rendered root gets the production marker, only after the next deploy.

Expand Down
14 changes: 14 additions & 0 deletions src/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,20 @@ export function renderGuideChecklist(state: GuideState, useColor: boolean): stri
lines.push('');
}

// Until the first scan, `setup` covers every step listed below in one bounded command, so the
// checklist names it first rather than leaving the agent to assemble the steps by hand.
if (state.siteUuid === null) {
lines.push(` ${paint(ANSI.cyan, '➜')} ${paint(ANSI.bold, 'Nothing is set up yet — two commands cover the steps below:')}`);
if (state.installed?.section !== 'dependencies') {
lines.push(` ${installCommand(state.packageManager)}`);
}
lines.push(' npx @patchstack/connect setup');
lines.push(' setup provisions the site, adds the widget, installs and verifies runtime protection,');
lines.push(' and wires the install and build scans, then prints this checklist again. Anything');
lines.push(' still listed there is a step setup could not apply safely, and is yours to finish.');
lines.push('');
}

// 1. Install
if (state.installed?.section === 'dependencies') {
lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
Expand Down
29 changes: 29 additions & 0 deletions tests/guide.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,35 @@ describe('guide', () => {
expect(output).toContain('the first scan does this for you');
});

it('names setup as the command that covers every step on an unprovisioned project', async () => {
writeJson('package.json', { name: 'fresh-app' });

const output = renderGuideChecklist(await collectGuideState(cwd), false);
const lead = output.indexOf('Nothing is set up yet');

expect(lead).toBeGreaterThan(-1);
expect(lead).toBeLessThan(output.indexOf('Install @patchstack/connect as a runtime dependency'));
expect(output.slice(lead)).toContain(`${installCommand('npm')}\n npx @patchstack/connect setup`);
});

it('leaves the install command out of the setup lead once the package is a runtime dependency', async () => {
writeJson('package.json', { name: 'fresh-app', dependencies: { '@patchstack/connect': '^0.5.0' } });

const output = renderGuideChecklist(await collectGuideState(cwd), false);

expect(output).toContain('Nothing is set up yet');
expect(output).not.toContain(installCommand('npm'));
});

it('drops the setup lead once the site is provisioned', async () => {
writeJson('package.json', { name: 'fresh-app', dependencies: { '@patchstack/connect': '^0.5.0' } });
writeJson('.patchstackrc.json', { siteUuid: VALID_UUID });

const output = renderGuideChecklist(await collectGuideState(cwd), false);

expect(output).not.toContain('Nothing is set up yet');
});

it('points at the project root when package.json is missing', async () => {
const output = renderGuideChecklist(await collectGuideState(cwd), false);
expect(output).toContain('No package.json found');
Expand Down
Loading