@patchstack/connect 0.3.26 → 0.3.27

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/AGENT-INSTALL.md CHANGED
@@ -139,14 +139,29 @@ npx @patchstack/connect login
139
139
 
140
140
  The command asks Patchstack for a short code, prints a link, and polls until the site's **owner approves it in the dashboard**. On approval it writes the new credential into `.patchstackrc.json` and exits. The link opens the approval page with the code already filled in, so the person only has to confirm.
141
141
 
142
- ### What you must do, as the agent
142
+ ### What you must do, as the agent — two commands, not one
143
143
 
144
- 1. **Run the command and surface the link and code to the user verbatim.** They must open it themselves — approval requires their signed-in Patchstack account, which you do not have and must not ask for.
145
- 2. **Leave the command running.** It polls until approved or the code expires (10 minutes). Do not kill it and retry; each run issues a different code and invalidates the one already on screen.
146
- 3. **Report the outcome.** On success, tell them the credential was restored *and* that the previous one no longer works — see the warning below.
144
+ **The command exits immediately when you run it.** It detects that its output is being captured rather than watched by a person, prints the link, and returns. It does **not** block waiting for approval, because you would not see the link until it exited — by which time the code would have expired, and it would look like the command had hung.
145
+
146
+ ```
147
+ 1. npx @patchstack/connect login → prints the link, exits straight away
148
+ 2. give the user the link, verbatim → they approve it in the browser
149
+ 3. npx @patchstack/connect login → the SAME command again, after they confirm.
150
+ It resumes the request and finishes the flow
151
+ ```
152
+
153
+ - **Never wrap step 1 in a timeout or kill it** — it returns on its own. If you find yourself waiting on it, something else is wrong.
154
+ - **Step 3 is the same command.** While a request is still valid it resumes rather than restarting, so running `login` again never invalidates the link the user is looking at. If they have not approved yet it tells you so, with the time remaining, and exits.
155
+ - **Nothing changes until step 3 runs.** Approving only marks the request; the credential is rotated and written when the CLI redeems it. So an abandoned flow is harmless — the site keeps working — but the credential is not restored until you come back.
156
+ - **Surface the link verbatim.** Approval requires the user's signed-in Patchstack account, which you do not have and must never ask for.
157
+ - **Report the outcome.** On success, say the credential was restored *and* that the previous one no longer works — see the warning below.
158
+
159
+ `login --wait` is the blocking variant: it polls until approved instead of returning. Prefer the plain re-run — it keeps each command short, which is what fits a conversation.
147
160
 
148
161
  You cannot complete this alone. It is deliberately a human-in-the-loop step: starting the flow proves nothing about who is running it, so the only authorisation is an owner approving in the browser.
149
162
 
163
+ (In an interactive terminal the same command prints the link and then waits, since a person can watch it stream. You get the two-step form; a human at a shell gets the one-step form.)
164
+
150
165
  ### Consequences to tell the user about
151
166
 
152
167
  **Approving rotates the credential — the old one stops working immediately.** Anywhere it was configured needs the new value: CI secrets, hosting-platform env vars, preview environments, other developers' checkouts. Say this before they approve, not after.
@@ -158,7 +173,8 @@ You cannot complete this alone. It is deliberately a human-in-the-loop step: sta
158
173
  | Site was never claimed | `409` — no owner exists to approve | Ask the user to claim the site in the dashboard first, or, if the site is disposable, delete `.patchstackrc.json` and `scan` to provision a fresh one |
159
174
  | Running in CI | Refuses to start | CI takes its credential from `PATCHSTACK_PULSE_AUTH`; `login` is for a developer machine |
160
175
  | No `siteUuid` configured | Refuses to start | There is no site to recover — run `scan` |
161
- | Code expired | Poll ends after 10 minutes | Run the command again for a new code |
176
+ | Code expired | `--wait` ends after 10 minutes | Start again from step 1 for a new code |
177
+ | `--wait` with nothing pending | "No login is waiting for approval" | Run step 1 first; `--wait` resumes a request, it does not start one |
162
178
 
163
179
  ## Uninstalling
164
180
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
4
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
5
5
 
6
6
  // src/parsers/index.ts
7
7
  import { access } from "fs/promises";
@@ -931,8 +931,8 @@ function computeManifestChecksum(packages) {
931
931
  var TOKEN_SKEW_MS = 6e4;
932
932
  function buildTokenUrl(manifestEndpoint) {
933
933
  const url = new URL(manifestEndpoint);
934
- const path11 = url.pathname.replace(/\/$/, "");
935
- url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
934
+ const path12 = url.pathname.replace(/\/$/, "");
935
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
936
936
  url.search = "";
937
937
  url.hash = "";
938
938
  return url.toString();
@@ -1018,8 +1018,8 @@ function buildEndpointUrl(base, siteUuid) {
1018
1018
  }
1019
1019
  function buildRulesUrl(manifestEndpoint, siteUuid) {
1020
1020
  const url = new URL(manifestEndpoint);
1021
- const path11 = url.pathname.replace(/\/$/, "");
1022
- url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1021
+ const path12 = url.pathname.replace(/\/$/, "");
1022
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1023
1023
  url.search = "";
1024
1024
  url.hash = "";
1025
1025
  return url.toString();
@@ -1034,16 +1034,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
1034
1034
  }
1035
1035
  function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1036
1036
  const url = new URL(manifestEndpoint);
1037
- const path11 = url.pathname.replace(/\/$/, "");
1038
- url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1037
+ const path12 = url.pathname.replace(/\/$/, "");
1038
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1039
1039
  url.search = "";
1040
1040
  url.hash = "";
1041
1041
  return url.toString();
1042
1042
  }
1043
1043
  function buildInputMapUrl(manifestEndpoint, siteUuid) {
1044
1044
  const url = new URL(manifestEndpoint);
1045
- const path11 = url.pathname.replace(/\/$/, "");
1046
- url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1045
+ const path12 = url.pathname.replace(/\/$/, "");
1046
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1047
1047
  url.search = "";
1048
1048
  url.hash = "";
1049
1049
  return url.toString();
@@ -2949,17 +2949,40 @@ function renderGuideChecklist(state, useColor) {
2949
2949
  }
2950
2950
 
2951
2951
  // src/login.ts
2952
+ import { mkdtempSync, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync6 } from "fs";
2953
+ import { tmpdir } from "os";
2954
+ import path10 from "path";
2955
+ import { createHash as createHash2 } from "crypto";
2952
2956
  function baseFrom(manifestEndpoint) {
2953
2957
  const url = new URL(manifestEndpoint);
2954
- const path11 = url.pathname.replace(/\/$/, "");
2955
- url.pathname = path11.endsWith("/manifest") ? path11.slice(0, -"/manifest".length) : "/monitor/pulse";
2958
+ const p = url.pathname.replace(/\/$/, "");
2959
+ url.pathname = p.endsWith("/manifest") ? p.slice(0, -"/manifest".length) : "/monitor/pulse";
2956
2960
  url.search = "";
2957
2961
  url.hash = "";
2958
2962
  return url.toString().replace(/\/$/, "");
2959
2963
  }
2960
- async function login(config, onPrompt, deps = {}) {
2964
+ function pendingPath(siteUuid) {
2965
+ const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
2966
+ return path10.join(tmpdir(), `patchstack-login-${key}.json`);
2967
+ }
2968
+ function savePendingLogin(siteUuid, pending) {
2969
+ writeFileSync6(pendingPath(siteUuid), JSON.stringify(pending), { encoding: "utf8", mode: 384 });
2970
+ }
2971
+ function readPendingLogin(siteUuid) {
2972
+ try {
2973
+ return JSON.parse(readFileSync6(pendingPath(siteUuid), "utf8"));
2974
+ } catch {
2975
+ return null;
2976
+ }
2977
+ }
2978
+ function clearPendingLogin(siteUuid) {
2979
+ try {
2980
+ rmSync(pendingPath(siteUuid));
2981
+ } catch {
2982
+ }
2983
+ }
2984
+ async function startLogin(config, deps = {}) {
2961
2985
  const fetchImpl = deps.fetchImpl ?? fetch;
2962
- const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2963
2986
  const now = deps.now ?? (() => Date.now());
2964
2987
  if (config.siteUuid === null) {
2965
2988
  return { status: "failed", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
@@ -2982,28 +3005,72 @@ async function login(config, onPrompt, deps = {}) {
2982
3005
  if (!started.ok) {
2983
3006
  return { status: "failed", message: `Could not start the login (HTTP ${started.status}).` };
2984
3007
  }
2985
- const { device_code: deviceCode, user_code: userCode, expires_in: expiresIn, interval } = await started.json();
2986
- const verificationUri = `${base}/device?code=${encodeURIComponent(userCode)}`;
2987
- onPrompt(userCode, verificationUri);
2988
- const deadline = now() + expiresIn * 1e3;
2989
- const intervalMs = Math.max(1, interval) * 1e3;
3008
+ const body = await started.json();
3009
+ const pending = {
3010
+ deviceCode: body.device_code,
3011
+ userCode: body.user_code,
3012
+ // Points at the API, which redirects to the dashboard SPA — the CLI only
3013
+ // knows the API origin, and the approval page lives on the app.
3014
+ verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
3015
+ expiresAt: now() + body.expires_in * 1e3,
3016
+ intervalMs: Math.max(1, body.interval) * 1e3
3017
+ };
3018
+ savePendingLogin(config.siteUuid, pending);
3019
+ return { status: "started", pending };
3020
+ }
3021
+ async function redeemIfApproved(config, pending, deps = {}) {
3022
+ const fetchImpl = deps.fetchImpl ?? fetch;
3023
+ const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
3024
+ method: "POST",
3025
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
3026
+ body: JSON.stringify({ device_code: pending.deviceCode })
3027
+ });
3028
+ if (polled.status === 428) return "pending";
3029
+ if (!polled.ok) {
3030
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3031
+ return "expired";
3032
+ }
3033
+ const { api_key: apiKey } = await polled.json();
3034
+ if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
3035
+ await persistApiKey(process.cwd(), apiKey);
3036
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3037
+ return "approved";
3038
+ }
3039
+ async function waitForApproval(config, pending, deps = {}) {
3040
+ const fetchImpl = deps.fetchImpl ?? fetch;
3041
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3042
+ const now = deps.now ?? (() => Date.now());
3043
+ const deadline = Math.min(pending.expiresAt, deps.until ?? pending.expiresAt);
3044
+ const expired = { status: "expired", message: "The login request expired. Run the command again." };
2990
3045
  while (now() < deadline) {
2991
- await sleep(intervalMs);
2992
- const polled = await fetchImpl(`${base}/device/token`, {
3046
+ await sleep(pending.intervalMs);
3047
+ const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
2993
3048
  method: "POST",
2994
3049
  headers: { "Content-Type": "application/json", Accept: "application/json" },
2995
- body: JSON.stringify({ device_code: deviceCode })
3050
+ body: JSON.stringify({ device_code: pending.deviceCode })
2996
3051
  });
2997
3052
  if (polled.status === 428) continue;
2998
- if (!polled.ok) return { status: "expired", message: "The login request expired. Run the command again." };
3053
+ if (!polled.ok) {
3054
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3055
+ return expired;
3056
+ }
2999
3057
  const { api_key: apiKey } = await polled.json();
3000
3058
  if (typeof apiKey !== "string" || apiKey.length === 0) {
3001
3059
  return { status: "failed", message: "Patchstack approved the request but returned no credential." };
3002
3060
  }
3003
3061
  await persistApiKey(process.cwd(), apiKey);
3004
- return { status: "approved", userCode, verificationUri };
3062
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3063
+ return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
3005
3064
  }
3006
- return { status: "expired", message: "The login request expired. Run the command again." };
3065
+ return expired;
3066
+ }
3067
+ async function login(config, onPrompt, deps = {}) {
3068
+ const started = await startLogin(config, deps);
3069
+ if (started.status !== "started" || started.pending === void 0) {
3070
+ return { status: started.status === "started" ? "failed" : started.status, message: started.message };
3071
+ }
3072
+ onPrompt(started.pending.userCode, started.pending.verificationUri);
3073
+ return waitForApproval(config, started.pending, deps);
3007
3074
  }
3008
3075
 
3009
3076
  // src/map/ts-loader.ts
@@ -3027,8 +3094,8 @@ async function loadTypeScript(cwd) {
3027
3094
  }
3028
3095
 
3029
3096
  // src/map/extract.ts
3030
- import { readFileSync as readFileSync10, realpathSync as realpathSync3 } from "fs";
3031
- import { createHash as createHash3 } from "crypto";
3097
+ import { readFileSync as readFileSync11, realpathSync as realpathSync3 } from "fs";
3098
+ import { createHash as createHash4 } from "crypto";
3032
3099
  import { relative as relative4 } from "path";
3033
3100
 
3034
3101
  // src/map/ast.ts
@@ -3283,7 +3350,7 @@ function npmPackageOf(spec) {
3283
3350
  }
3284
3351
 
3285
3352
  // src/map/sources.ts
3286
- import { readFileSync as readFileSync6, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
3353
+ import { readFileSync as readFileSync7, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
3287
3354
  import { join as join12, relative as relative2, isAbsolute } from "path";
3288
3355
 
3289
3356
  // src/map/routes.ts
@@ -3368,7 +3435,7 @@ function hasEntrySignal(text) {
3368
3435
  }
3369
3436
  function detectFramework(cwd) {
3370
3437
  try {
3371
- const pkg = JSON.parse(readFileSync6(join12(cwd, "package.json"), "utf8"));
3438
+ const pkg = JSON.parse(readFileSync7(join12(cwd, "package.json"), "utf8"));
3372
3439
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
3373
3440
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
3374
3441
  if (d["next"]) return "next";
@@ -3496,10 +3563,10 @@ function detectDeploymentShapes(cwd, opts = {}) {
3496
3563
  boundary = realpathSync(boundary);
3497
3564
  } catch {
3498
3565
  }
3499
- const inProject = (path11) => {
3566
+ const inProject = (path12) => {
3500
3567
  if (opts.followOutside) return true;
3501
3568
  try {
3502
- return isInside(realpathSync(path11), boundary);
3569
+ return isInside(realpathSync(path12), boundary);
3503
3570
  } catch {
3504
3571
  return false;
3505
3572
  }
@@ -3565,7 +3632,7 @@ function holdsSourceFile(dir) {
3565
3632
  }
3566
3633
 
3567
3634
  // src/map/surface.ts
3568
- import { readFileSync as readFileSync7 } from "fs";
3635
+ import { readFileSync as readFileSync8 } from "fs";
3569
3636
  import { join as join13 } from "path";
3570
3637
  var STATIC_GENERATORS = [
3571
3638
  { dep: "vite", label: "vite" },
@@ -3604,7 +3671,7 @@ var SHAPE_SIGNAL = {
3604
3671
  };
3605
3672
  function readManifest(cwd) {
3606
3673
  try {
3607
- const parsed = JSON.parse(readFileSync7(join13(cwd, "package.json"), "utf8"));
3674
+ const parsed = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
3608
3675
  return typeof parsed === "object" && parsed !== null ? parsed : null;
3609
3676
  } catch {
3610
3677
  return null;
@@ -3617,7 +3684,7 @@ function nextExportsStatically(cwd, manifest, ts) {
3617
3684
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
3618
3685
  let text;
3619
3686
  try {
3620
- text = readFileSync7(join13(cwd, file), "utf8");
3687
+ text = readFileSync8(join13(cwd, file), "utf8");
3621
3688
  } catch {
3622
3689
  continue;
3623
3690
  }
@@ -3745,7 +3812,7 @@ function surfaceNote(surface) {
3745
3812
  }
3746
3813
 
3747
3814
  // src/map/sinks.ts
3748
- import { createHash as createHash2 } from "crypto";
3815
+ import { createHash as createHash3 } from "crypto";
3749
3816
  var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
3750
3817
  var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
3751
3818
  var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
@@ -4001,7 +4068,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
4001
4068
  return table?.[index] ?? "unknown";
4002
4069
  }
4003
4070
  function sinkId(s, owner) {
4004
- return createHash2("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
4071
+ return createHash3("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
4005
4072
  }
4006
4073
  function dedupeSinks(sinks, owner) {
4007
4074
  const seen = /* @__PURE__ */ new Set();
@@ -4017,7 +4084,7 @@ function dedupeSinks(sinks, owner) {
4017
4084
  }
4018
4085
 
4019
4086
  // src/map/module-graph.ts
4020
- import { readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
4087
+ import { readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
4021
4088
  import { dirname as dirname4, join as join14, relative as relative3, resolve as resolvePath } from "path";
4022
4089
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
4023
4090
  function createModuleGraph(ts, opts) {
@@ -4026,7 +4093,7 @@ function createModuleGraph(ts, opts) {
4026
4093
  if (cache.has(file)) return cache.get(file) ?? null;
4027
4094
  let entry = null;
4028
4095
  try {
4029
- const text = readFileSync8(file, "utf8");
4096
+ const text = readFileSync9(file, "utf8");
4030
4097
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
4031
4098
  const bindings = buildModuleBindings(sf, ts);
4032
4099
  entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
@@ -4125,8 +4192,8 @@ function collectCallees(sf, ts) {
4125
4192
  }
4126
4193
 
4127
4194
  // src/map/coordinates.ts
4128
- function runtimeCoordinate(source, path11) {
4129
- if (/\[\d*\]/.test(path11)) {
4195
+ function runtimeCoordinate(source, path12) {
4196
+ if (/\[\d*\]/.test(path12)) {
4130
4197
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
4131
4198
  }
4132
4199
  switch (source) {
@@ -4135,15 +4202,15 @@ function runtimeCoordinate(source, path11) {
4135
4202
  case "multipart":
4136
4203
  case "body":
4137
4204
  case "server-fn-data":
4138
- return { runtimeParameter: `post.${path11}` };
4205
+ return { runtimeParameter: `post.${path12}` };
4139
4206
  case "query":
4140
- return { runtimeParameter: `get.${path11}` };
4207
+ return { runtimeParameter: `get.${path12}` };
4141
4208
  case "cookie":
4142
- return { runtimeParameter: `cookie.${path11}` };
4209
+ return { runtimeParameter: `cookie.${path12}` };
4143
4210
  case "file":
4144
- return { runtimeParameter: `files.${path11}` };
4211
+ return { runtimeParameter: `files.${path12}` };
4145
4212
  case "header":
4146
- return { runtimeParameter: `server.HTTP_${path11.toUpperCase().replace(/-/g, "_")}` };
4213
+ return { runtimeParameter: `server.HTTP_${path12.toUpperCase().replace(/-/g, "_")}` };
4147
4214
  case "route-param":
4148
4215
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
4149
4216
  default:
@@ -4175,8 +4242,8 @@ function addressSpaceOf(source) {
4175
4242
  function isProvenFlow(confidence) {
4176
4243
  return confidence === "exact-local" || confidence === "transformed-local";
4177
4244
  }
4178
- function inputIdOf(source, path11) {
4179
- return `${addressSpaceOf(source)}:${path11}`;
4245
+ function inputIdOf(source, path12) {
4246
+ return `${addressSpaceOf(source)}:${path12}`;
4180
4247
  }
4181
4248
  function withCoordinates(fields, source) {
4182
4249
  return fields.map((f) => {
@@ -4375,8 +4442,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
4375
4442
  if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
4376
4443
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
4377
4444
  const rootPath = /* @__PURE__ */ new Map();
4378
- const addRoot = (name, path11, space) => {
4379
- if (!rootPath.has(name)) rootPath.set(name, { path: path11, space });
4445
+ const addRoot = (name, path12, space) => {
4446
+ if (!rootPath.has(name)) rootPath.set(name, { path: path12, space });
4380
4447
  };
4381
4448
  for (const p of params ?? []) {
4382
4449
  if (!p?.name) continue;
@@ -4521,8 +4588,8 @@ function dedupeLimitations(list) {
4521
4588
  function join22(base, seg) {
4522
4589
  return base ? `${base}.${seg}` : seg;
4523
4590
  }
4524
- function normalizePath(path11) {
4525
- return path11.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
4591
+ function normalizePath(path12) {
4592
+ return path12.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
4526
4593
  }
4527
4594
  function fluentChainCalls(call, ts) {
4528
4595
  let root = call;
@@ -4768,7 +4835,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
4768
4835
  }
4769
4836
 
4770
4837
  // src/map/imports.ts
4771
- import { readFileSync as readFileSync9 } from "fs";
4838
+ import { readFileSync as readFileSync10 } from "fs";
4772
4839
  import { join as join15 } from "path";
4773
4840
  var MAX_SITES = 5;
4774
4841
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
@@ -4780,7 +4847,7 @@ function readPathAliases(cwd) {
4780
4847
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
4781
4848
  let paths;
4782
4849
  try {
4783
- const raw = readFileSync9(join15(cwd, name), "utf8");
4850
+ const raw = readFileSync10(join15(cwd, name), "utf8");
4784
4851
  const parsed = JSON.parse(stripJsonComments(raw));
4785
4852
  paths = parsed.compilerOptions?.paths;
4786
4853
  } catch {
@@ -5159,7 +5226,7 @@ async function extractInputMap(cwd, ts, options = {}) {
5159
5226
  const hopTargets = /* @__PURE__ */ new Set();
5160
5227
  for (const file of files) {
5161
5228
  try {
5162
- const text = readFileSync10(file, "utf8");
5229
+ const text = readFileSync11(file, "utf8");
5163
5230
  const relFile = relative4(cwd, file);
5164
5231
  sourceBytes += text.length;
5165
5232
  if (!hasEntrySignal(text)) {
@@ -5172,7 +5239,7 @@ async function extractInputMap(cwd, ts, options = {}) {
5172
5239
  }
5173
5240
  parsed++;
5174
5241
  entryFiles.add(file);
5175
- const fingerprint = createHash3("sha256").update(text).digest("hex").slice(0, 16);
5242
+ const fingerprint = createHash4("sha256").update(text).digest("hex").slice(0, 16);
5176
5243
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
5177
5244
  const bindings = buildModuleBindings(sf, ts);
5178
5245
  for (const specifier of bindings.imports) {
@@ -5214,7 +5281,7 @@ async function extractInputMap(cwd, ts, options = {}) {
5214
5281
  for (const target of hopTargets) {
5215
5282
  if (entryFiles.has(target)) continue;
5216
5283
  try {
5217
- const text = readFileSync10(target, "utf8");
5284
+ const text = readFileSync11(target, "utf8");
5218
5285
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
5219
5286
  const bindings = buildModuleBindings(sf, ts);
5220
5287
  const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative4(cwd, target), graph });
@@ -5345,8 +5412,8 @@ async function buildInputMap(cwd, options = {}) {
5345
5412
  }
5346
5413
 
5347
5414
  // src/setup.ts
5348
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
5349
- import path10 from "path";
5415
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
5416
+ import path11 from "path";
5350
5417
  var SCAN_COMMAND = "patchstack-connect scan";
5351
5418
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
5352
5419
  function setupProtection(cwd) {
@@ -5364,8 +5431,8 @@ function appendHook(existing, command) {
5364
5431
  return `${existing} && ${command}`;
5365
5432
  }
5366
5433
  function wireBuildScripts(cwd, packageManager) {
5367
- const target = path10.join(cwd, "package.json");
5368
- const raw = readFileSync11(target, "utf8");
5434
+ const target = path11.join(cwd, "package.json");
5435
+ const raw = readFileSync12(target, "utf8");
5369
5436
  const pkg = JSON.parse(raw);
5370
5437
  const scripts = pkg.scripts ?? {};
5371
5438
  const build = scripts.build;
@@ -5414,7 +5481,7 @@ function wireBuildScripts(cwd, packageManager) {
5414
5481
  const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
5415
5482
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
5416
5483
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
5417
- writeFileSync6(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
5484
+ writeFileSync7(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
5418
5485
  if (build === void 0 || build.trim().length === 0) {
5419
5486
  return {
5420
5487
  changed: true,
@@ -5492,10 +5559,17 @@ Usage:
5492
5559
  what's missing, with tailored commands), then
5493
5560
  print the full setup guide. --full prints the
5494
5561
  guide even when setup is complete
5495
- patchstack-connect login [options] Recover this site's credential when
5562
+ patchstack-connect login [--wait] Recover this site's credential when
5496
5563
  .patchstackrc.json has been lost. Prints a link
5497
- for the site's OWNER to approve in the dashboard,
5498
- then waits (10 min). Use this instead of deleting
5564
+ for the site's OWNER to approve in the dashboard.
5565
+ In a terminal it then waits. When the output is
5566
+ piped or captured \u2014 an assistant running it \u2014 it
5567
+ prints the link and EXITS, so the link is visible
5568
+ immediately. Run it AGAIN once the user confirms
5569
+ they approved: it resumes the same request rather
5570
+ than starting a new one, and finishes the flow.
5571
+ --wait blocks instead of returning. Use this
5572
+ instead of deleting
5499
5573
  .patchstackrc.json and re-scanning, which would
5500
5574
  provision a second site. Approving ROTATES the
5501
5575
  credential: CI, deploys and other machines using
@@ -5600,7 +5674,26 @@ async function runLogin(args) {
5600
5674
  cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
5601
5675
  cliEndpoint: getStringFlag(args.flags, "endpoint")
5602
5676
  });
5603
- const result = await login(config, (userCode, verificationUri) => {
5677
+ const approved = () => {
5678
+ console.log("\n \u2713 Credential restored and saved to .patchstackrc.json.");
5679
+ console.log(" The previous credential no longer works. Update it anywhere else it was set:");
5680
+ console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
5681
+ return 0;
5682
+ };
5683
+ if (args.flags.has("wait")) {
5684
+ const pending = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
5685
+ if (pending === null) {
5686
+ console.error("\n No login is waiting for approval. Run `patchstack-connect login` first.\n");
5687
+ return 1;
5688
+ }
5689
+ const resumed = await waitForApproval(config, pending);
5690
+ if (resumed.status === "approved") return approved();
5691
+ console.error(`
5692
+ ${resumed.message ?? "Login failed."}
5693
+ `);
5694
+ return 1;
5695
+ }
5696
+ const prompt = (userCode, verificationUri) => {
5604
5697
  console.log(`
5605
5698
  Your code: ${userCode}`);
5606
5699
  console.log(` Approve at: ${verificationUri}
@@ -5608,14 +5701,39 @@ async function runLogin(args) {
5608
5701
  console.log(" Open that link and approve it as the site's owner. Approving issues a new");
5609
5702
  console.log(" credential and stops the current one working \u2014 CI, deploys and any other");
5610
5703
  console.log(" machine using it will need the new value.\n");
5611
- console.log(" Waiting for approval (the code expires in 10 minutes)\u2026");
5612
- });
5613
- if (result.status === "approved") {
5614
- console.log("\n \u2713 Credential restored and saved to .patchstackrc.json.");
5615
- console.log(" The previous credential no longer works. Update it anywhere else it was set:");
5616
- console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
5704
+ };
5705
+ if (process.stdout.isTTY !== true) {
5706
+ const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
5707
+ if (existing !== null && Date.now() < existing.expiresAt) {
5708
+ const outcome = await redeemIfApproved(config, existing);
5709
+ if (outcome === "approved") return approved();
5710
+ if (outcome === "pending") {
5711
+ const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
5712
+ console.log(`
5713
+ Still waiting for approval of code ${existing.userCode}.`);
5714
+ console.log(` Approve at: ${existing.verificationUri}`);
5715
+ console.log(` (valid for another ${secondsLeft}s \u2014 run this again once the user confirms)
5716
+ `);
5717
+ return 0;
5718
+ }
5719
+ }
5720
+ const started = await startLogin(config);
5721
+ if (started.status !== "started" || started.pending === void 0) {
5722
+ console.error(`
5723
+ ${started.message ?? "Login failed."}
5724
+ `);
5725
+ return 1;
5726
+ }
5727
+ prompt(started.pending.userCode, started.pending.verificationUri);
5728
+ console.log(" Give that link to the user. When they confirm they have approved it, run");
5729
+ console.log(" this same command again (or `login --wait` to block until they do).\n");
5617
5730
  return 0;
5618
5731
  }
5732
+ const result = await login(config, (userCode, verificationUri) => {
5733
+ prompt(userCode, verificationUri);
5734
+ console.log(" Waiting for approval (the code expires in 10 minutes)\u2026");
5735
+ });
5736
+ if (result.status === "approved") return approved();
5619
5737
  console.error(`
5620
5738
  ${result.message ?? "Login failed."}
5621
5739
  `);
@@ -5664,7 +5782,7 @@ async function runMap(args) {
5664
5782
  const json = JSON.stringify(map, null, 2);
5665
5783
  const out = getStringFlag(args.flags, "out");
5666
5784
  if (out) {
5667
- writeFileSync7(out, json);
5785
+ writeFileSync8(out, json);
5668
5786
  console.error(`patchstack: wrote ${out}`);
5669
5787
  } else if (args.flags.get("upload") !== true) {
5670
5788
  console.log(json);
@@ -5950,7 +6068,7 @@ async function runGuide(args) {
5950
6068
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
5951
6069
  console.log("");
5952
6070
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
5953
- console.log(readFileSync12(guidePath, "utf8"));
6071
+ console.log(readFileSync13(guidePath, "utf8"));
5954
6072
  return 0;
5955
6073
  }
5956
6074
  async function runSetup(args) {
@@ -6131,7 +6249,7 @@ async function runMarkBuild(args) {
6131
6249
  let marked = 0;
6132
6250
  let widgetTouched = 0;
6133
6251
  for (const file of files) {
6134
- const before = readFileSync12(file, "utf8");
6252
+ const before = readFileSync13(file, "utf8");
6135
6253
  let after = injectMarker(before, snippet);
6136
6254
  if (widgetUuid !== null) {
6137
6255
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -6141,7 +6259,7 @@ async function runMarkBuild(args) {
6141
6259
  after = ensured.html;
6142
6260
  }
6143
6261
  if (after !== before) {
6144
- writeFileSync7(file, after);
6262
+ writeFileSync8(file, after);
6145
6263
  marked += 1;
6146
6264
  }
6147
6265
  }