@opengsd/gsd-pi 1.2.0-dev.d6c5343c → 1.2.0-dev.e8563f58

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.
Files changed (132) hide show
  1. package/dist/mcp-server.js +2 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/orchestrator.js +28 -10
  4. package/dist/resources/extensions/gsd/auto-model-selection.js +11 -7
  5. package/dist/resources/extensions/gsd/auto.js +7 -0
  6. package/dist/resources/extensions/gsd/blocked-models.js +28 -0
  7. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +26 -6
  8. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  9. package/dist/resources/extensions/gsd/closeout-wizard.js +92 -0
  10. package/dist/resources/extensions/gsd/commands-handlers.js +46 -3
  11. package/dist/resources/extensions/gsd/consent-question.js +16 -0
  12. package/dist/resources/extensions/gsd/doctor-git-checks.js +2 -18
  13. package/dist/resources/extensions/gsd/gsd-command-home.js +22 -12
  14. package/dist/resources/extensions/gsd/gsd-db.js +2 -1
  15. package/dist/resources/extensions/gsd/milestone-closeout.js +73 -2
  16. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +67 -2
  17. package/dist/resources/extensions/shared/gsd-browser-cli.js +21 -2
  18. package/dist/resources/shared/gsd-browser-path-sync.js +214 -0
  19. package/dist/resources/shared/package-manager-detection.js +1 -1
  20. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  21. package/dist/update-check.d.ts +2 -0
  22. package/dist/update-check.js +24 -1
  23. package/dist/update-cmd.js +20 -3
  24. package/dist/web/standalone/.next/BUILD_ID +1 -1
  25. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  26. package/dist/web/standalone/.next/build-manifest.json +2 -2
  27. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  28. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.html +1 -1
  45. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  52. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  53. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  55. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  56. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  57. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  58. package/package.json +1 -1
  59. package/packages/cloud-mcp-gateway/package.json +2 -2
  60. package/packages/contracts/package.json +1 -1
  61. package/packages/daemon/package.json +4 -4
  62. package/packages/gsd-agent-core/package.json +5 -5
  63. package/packages/gsd-agent-modes/package.json +7 -7
  64. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts +29 -0
  65. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts.map +1 -0
  66. package/packages/mcp-server/dist/moonshot-tool-schema.js +50 -0
  67. package/packages/mcp-server/dist/moonshot-tool-schema.js.map +1 -0
  68. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  69. package/packages/mcp-server/dist/server.js +4 -0
  70. package/packages/mcp-server/dist/server.js.map +1 -1
  71. package/packages/mcp-server/dist/workflow-tools.d.ts +18 -18
  72. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  73. package/packages/mcp-server/dist/workflow-tools.js +99 -38
  74. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  75. package/packages/mcp-server/package.json +5 -4
  76. package/packages/native/package.json +1 -1
  77. package/packages/pi-agent-core/package.json +1 -1
  78. package/packages/pi-ai/dist/index.d.ts +2 -0
  79. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  80. package/packages/pi-ai/dist/index.js +2 -0
  81. package/packages/pi-ai/dist/index.js.map +1 -1
  82. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  83. package/packages/pi-ai/dist/providers/anthropic.js +12 -7
  84. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  85. package/packages/pi-ai/dist/providers/google-shared.d.ts +5 -0
  86. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  87. package/packages/pi-ai/dist/providers/google-shared.js +12 -3
  88. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  89. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  90. package/packages/pi-ai/dist/providers/openai-completions.js +7 -3
  91. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  92. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts +9 -0
  93. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts.map +1 -0
  94. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js +34 -0
  95. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js.map +1 -0
  96. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  97. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +6 -2
  98. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  99. package/packages/pi-ai/package.json +1 -1
  100. package/packages/pi-coding-agent/package.json +7 -7
  101. package/packages/pi-tui/package.json +2 -2
  102. package/packages/rpc-client/package.json +2 -2
  103. package/pkg/package.json +1 -1
  104. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +11 -0
  105. package/src/resources/extensions/gsd/auto/orchestrator.ts +28 -10
  106. package/src/resources/extensions/gsd/auto-model-selection.ts +16 -7
  107. package/src/resources/extensions/gsd/auto.ts +8 -0
  108. package/src/resources/extensions/gsd/blocked-models.ts +49 -0
  109. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +34 -5
  110. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  111. package/src/resources/extensions/gsd/closeout-wizard.ts +102 -0
  112. package/src/resources/extensions/gsd/commands-handlers.ts +46 -3
  113. package/src/resources/extensions/gsd/consent-question.ts +15 -0
  114. package/src/resources/extensions/gsd/doctor-git-checks.ts +2 -19
  115. package/src/resources/extensions/gsd/gsd-command-home.ts +13 -3
  116. package/src/resources/extensions/gsd/gsd-db.ts +4 -3
  117. package/src/resources/extensions/gsd/milestone-closeout.ts +97 -2
  118. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +69 -0
  119. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +97 -0
  120. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +19 -0
  121. package/src/resources/extensions/gsd/tests/consent-question.test.ts +15 -0
  122. package/src/resources/extensions/gsd/tests/doctor-git-checks-terminal.test.ts +73 -0
  123. package/src/resources/extensions/gsd/tests/gsd-command-home.test.ts +120 -0
  124. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +95 -4
  125. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
  126. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +273 -38
  127. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +81 -2
  128. package/src/resources/extensions/shared/gsd-browser-cli.ts +23 -2
  129. package/src/resources/shared/gsd-browser-path-sync.ts +273 -0
  130. package/src/resources/shared/package-manager-detection.ts +1 -1
  131. /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → LDHRKiRBIVZmiuMjrL1Vy}/_buildManifest.js +0 -0
  132. /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → LDHRKiRBIVZmiuMjrL1Vy}/_ssgManifest.js +0 -0
@@ -19,9 +19,10 @@ import {
19
19
  } from "../gsd-db.js";
20
20
  import { GATE_REGISTRY } from "../gate-registry.js";
21
21
  import { generateRequirementsMd, saveArtifactToDb } from "../db-writer.js";
22
- import { clearPathCache, relSliceFile, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
22
+ import { clearPathCache, normalizeRealPath, relSliceFile, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
23
23
  import { saveFile, clearParseCache } from "../files.js";
24
24
  import { unlinkSync } from "node:fs";
25
+ import { hostname } from "node:os";
25
26
  import { join } from "node:path";
26
27
  import type { CompleteMilestoneParams } from "./complete-milestone.js";
27
28
  import { handleCompleteMilestone } from "./complete-milestone.js";
@@ -48,13 +49,21 @@ import { logError, logWarning } from "../workflow-logger.js";
48
49
  import { invalidateStateCache } from "../state.js";
49
50
  import { loadEffectiveGSDPreferences } from "../preferences.js";
50
51
  import { parseProject } from "../schemas/parsers.js";
51
- import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
52
+ import { autoSession, getAutoRuntimeSnapshot, isAutoActive } from "../auto-runtime-state.js";
52
53
  import { renderPlanFromDb } from "../markdown-renderer.js";
53
54
  import {
54
55
  prepareUatRun,
55
56
  saveUatAttemptArtifact,
56
57
  type UatResultSaveParams,
57
58
  } from "../uat-run.js";
59
+ import { registerAutoWorker, markWorkerStopping, getAutoWorker } from "../db/auto-workers.js";
60
+ import {
61
+ claimMilestoneLease,
62
+ releaseMilestoneLease,
63
+ getMilestoneLease,
64
+ refreshMilestoneLease,
65
+ milestoneLeaseTtlSeconds,
66
+ } from "../db/milestone-leases.js";
58
67
  export type {
59
68
  UatCheckResultInput,
60
69
  UatEvidenceRef,
@@ -105,6 +114,24 @@ function blockIfWrongAutoUnit(requiredUnitType: string, operation: string): Tool
105
114
  };
106
115
  }
107
116
 
117
+ function milestoneLeaseConflictResult(
118
+ milestoneId: string,
119
+ byWorker: string,
120
+ expiresAt: string,
121
+ ): ToolExecutionResult {
122
+ return {
123
+ content: [{ type: "text", text: `Milestone ${milestoneId} is currently leased by ${byWorker}. Retry after ${expiresAt}.` }],
124
+ details: {
125
+ operation: "plan_milestone",
126
+ error: "milestone_lease_conflict",
127
+ milestoneId,
128
+ byWorker,
129
+ expiresAt,
130
+ },
131
+ isError: true,
132
+ };
133
+ }
134
+
108
135
  export interface SummarySaveParams {
109
136
  milestone_id?: string;
110
137
  slice_id?: string;
@@ -1244,7 +1271,48 @@ export async function executePlanMilestone(
1244
1271
  isError: true,
1245
1272
  };
1246
1273
  }
1274
+ let workerId: string | null = null;
1275
+ let acquiredToken: number | null = null;
1276
+ let leaseRefreshTimer: ReturnType<typeof setInterval> | undefined;
1247
1277
  try {
1278
+ // Re-read at the gate so a peer-created milestone is not treated as fresh.
1279
+ const milestoneExists = getMilestone(params.milestoneId) !== null;
1280
+ if (milestoneExists) {
1281
+ const heldLease = getMilestoneLease(params.milestoneId);
1282
+ if (heldLease?.status === "held" && Date.parse(heldLease.expires_at) > Date.now()) {
1283
+ const holder = getAutoWorker(heldLease.worker_id);
1284
+ // Let the one-shot claim path recover stale same-process worker rows.
1285
+ const projectRoot = normalizeRealPath(basePath);
1286
+ const isOurAutoLease = isAutoActive() && heldLease.worker_id === autoSession.workerId;
1287
+ const holderIsOneShotReentrantPeer = !isAutoActive()
1288
+ && !!holder
1289
+ && holder.host === hostname()
1290
+ && holder.pid === process.pid
1291
+ && holder.project_root_realpath === projectRoot;
1292
+ if (holder?.status === "active" && !isOurAutoLease && !holderIsOneShotReentrantPeer) {
1293
+ return milestoneLeaseConflictResult(params.milestoneId, heldLease.worker_id, heldLease.expires_at);
1294
+ }
1295
+ }
1296
+ }
1297
+
1298
+ // Fresh creation cannot claim a lease because the FK row does not exist.
1299
+ // In-process auto already owns its lease; re-claiming would bump its token.
1300
+ if (!isAutoActive() && milestoneExists) {
1301
+ workerId = registerAutoWorker({ projectRootRealpath: normalizeRealPath(basePath) });
1302
+ const lease = claimMilestoneLease(workerId, params.milestoneId);
1303
+ if (!lease.ok) {
1304
+ return milestoneLeaseConflictResult(params.milestoneId, lease.byWorker, lease.expiresAt);
1305
+ }
1306
+ acquiredToken = lease.token;
1307
+
1308
+ const leaseRefreshMs = (milestoneLeaseTtlSeconds() / 2) * 1000;
1309
+ leaseRefreshTimer = setInterval(() => {
1310
+ if (acquiredToken !== null && workerId !== null) {
1311
+ refreshMilestoneLease(workerId, params.milestoneId, acquiredToken);
1312
+ }
1313
+ }, leaseRefreshMs);
1314
+ }
1315
+
1248
1316
  const result = await handlePlanMilestone(params, basePath);
1249
1317
  if ("error" in result) {
1250
1318
  return {
@@ -1270,6 +1338,17 @@ export async function executePlanMilestone(
1270
1338
  isError: true,
1271
1339
  };
1272
1340
  }
1341
+ finally {
1342
+ if (leaseRefreshTimer !== undefined) {
1343
+ clearInterval(leaseRefreshTimer);
1344
+ }
1345
+ if (workerId !== null && acquiredToken !== null) {
1346
+ releaseMilestoneLease(workerId, params.milestoneId, acquiredToken);
1347
+ }
1348
+ if (workerId !== null) {
1349
+ markWorkerStopping(workerId);
1350
+ }
1351
+ }
1273
1352
  }
1274
1353
 
1275
1354
  export async function executePlanSlice(
@@ -55,6 +55,24 @@ function parseGsdBrowserVersion(output: string): string | null {
55
55
  return output.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
56
56
  }
57
57
 
58
+ function splitCommandLine(commandLine: string): string[] {
59
+ const parts = commandLine.match(/(?:"[^"]*"|'[^']*'|[^\s"']+)/g) ?? [];
60
+ return parts.map((part) => {
61
+ const quote = part[0];
62
+ if ((quote === '"' || quote === "'") && part.endsWith(quote)) {
63
+ return part.slice(1, -1);
64
+ }
65
+ return part;
66
+ });
67
+ }
68
+
69
+ function buildPathGsdBrowserVersionInvocation(platform: NodeJS.Platform): { command: string; args: string[] } {
70
+ if (platform === "win32") {
71
+ return { command: "cmd", args: ["/d", "/s", "/c", "gsd-browser", "--version"] };
72
+ }
73
+ return { command: "gsd-browser", args: ["--version"] };
74
+ }
75
+
58
76
  function isRecord(value: unknown): value is Record<string, unknown> {
59
77
  return !!value && typeof value === "object" && !Array.isArray(value);
60
78
  }
@@ -86,7 +104,8 @@ function resolvePathGsdBrowserVersion(env: NodeJS.ProcessEnv): string | null {
86
104
  if (cachedPathProbeVersion !== undefined) return cachedPathProbeVersion;
87
105
 
88
106
  try {
89
- cachedPathProbeVersion = parseGsdBrowserVersion(execFileSync("gsd-browser", ["--version"], {
107
+ const invocation = buildPathGsdBrowserVersionInvocation(process.platform);
108
+ cachedPathProbeVersion = parseGsdBrowserVersion(execFileSync(invocation.command, invocation.args, {
90
109
  encoding: "utf-8",
91
110
  env,
92
111
  stdio: ["ignore", "pipe", "ignore"],
@@ -216,7 +235,8 @@ export function resolveGsdBrowserMcpLaunchConfig(
216
235
  const serverName = env.GSD_BROWSER_MCP_NAME?.trim() || GSD_BROWSER_MCP_SERVER_NAME;
217
236
  const explicitArgs = parseJsonEnv<unknown>(env, "GSD_BROWSER_MCP_ARGS");
218
237
  const explicitEnv = parseJsonEnv<Record<string, string>>(env, "GSD_BROWSER_MCP_ENV");
219
- const explicitCommand = env.GSD_BROWSER_MCP_COMMAND?.trim();
238
+ const explicitCommandLine = env.GSD_BROWSER_MCP_COMMAND?.trim();
239
+ const [explicitCommand, ...explicitCommandArgs] = explicitCommandLine ? splitCommandLine(explicitCommandLine) : [];
220
240
  const explicitCliPath = resolveExplicitGsdBrowserCliPath(env);
221
241
  const preferPathCli = !explicitCommand && !explicitCliPath && shouldPreferPathGsdBrowser(env);
222
242
  const bundledCliPath = !explicitCommand && !explicitCliPath && !preferPathCli
@@ -241,6 +261,7 @@ export function resolveGsdBrowserMcpLaunchConfig(
241
261
  const args = Array.isArray(explicitArgs) && explicitArgs.length > 0
242
262
  ? explicitArgs.map(String)
243
263
  : [
264
+ ...explicitCommandArgs,
244
265
  ...(bundledCliPath ? [bundledCliPath] : []),
245
266
  "mcp",
246
267
  "--session",
@@ -0,0 +1,273 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { chmodSync, copyFileSync, existsSync, lstatSync, readlinkSync, realpathSync } from 'node:fs'
3
+ import { homedir } from 'node:os'
4
+ import { dirname, isAbsolute, join, delimiter as pathDelimiter, resolve as resolvePath } from 'node:path'
5
+ import { isPnpmInstall, pathStartsWith } from './package-manager-detection.js'
6
+
7
+ export interface GsdBrowserPathReconcileResult {
8
+ action: 'none' | 'synced' | 'shadowed'
9
+ pathCli?: string
10
+ installedCli?: string
11
+ syncTarget?: string
12
+ message?: string
13
+ }
14
+
15
+ function isBunGlobalInstall(argv1: string | undefined, env: NodeJS.ProcessEnv): boolean {
16
+ if ('bun' in process.versions) return true
17
+ if (!argv1) return false
18
+
19
+ const bunBinDirs: string[] = []
20
+ if (env.BUN_INSTALL) bunBinDirs.push(join(env.BUN_INSTALL, 'bin'))
21
+ bunBinDirs.push(join(homedir(), '.bun', 'bin'))
22
+
23
+ return bunBinDirs.some((dir) => pathStartsWith(argv1, dir))
24
+ }
25
+
26
+ function gsdBrowserBinaryName(platform: NodeJS.Platform): string {
27
+ return platform === 'win32' ? 'gsd-browser.cmd' : 'gsd-browser'
28
+ }
29
+
30
+ function tryResolveFromBinDir(binDir: string, platform: NodeJS.Platform): string | null {
31
+ const primary = join(binDir, gsdBrowserBinaryName(platform))
32
+ if (existsSync(primary)) return primary
33
+
34
+ if (platform === 'win32') {
35
+ const fallback = join(binDir, 'gsd-browser')
36
+ if (existsSync(fallback)) return fallback
37
+ }
38
+
39
+ return null
40
+ }
41
+
42
+ function tryResolveFromPackageRoot(
43
+ rootDir: string,
44
+ platform: NodeJS.Platform,
45
+ ): string | null {
46
+ const candidate = join(rootDir, '@opengsd', 'gsd-browser', 'bin', gsdBrowserBinaryName(platform))
47
+ if (existsSync(candidate)) return candidate
48
+
49
+ if (platform === 'win32') {
50
+ const fallback = join(rootDir, '@opengsd', 'gsd-browser', 'bin', 'gsd-browser')
51
+ if (existsSync(fallback)) return fallback
52
+ }
53
+
54
+ return null
55
+ }
56
+
57
+ function tryExecLookup(
58
+ command: string,
59
+ args: string[],
60
+ env: NodeJS.ProcessEnv,
61
+ platform: NodeJS.Platform,
62
+ resolve: (dir: string, platform: NodeJS.Platform) => string | null,
63
+ ): string | null {
64
+ try {
65
+ const dir = execFileSync(command, args, {
66
+ encoding: 'utf-8',
67
+ env,
68
+ stdio: ['ignore', 'pipe', 'ignore'],
69
+ timeout: 5000,
70
+ }).trim()
71
+ return resolve(dir, platform)
72
+ } catch {
73
+ return null
74
+ }
75
+ }
76
+
77
+ function resolvePathBinary(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string | null {
78
+ if (platform === 'win32') {
79
+ try {
80
+ const out = execFileSync('where', ['gsd-browser'], {
81
+ encoding: 'utf-8',
82
+ env,
83
+ stdio: ['ignore', 'pipe', 'ignore'],
84
+ timeout: 5000,
85
+ }).trim()
86
+ const first = out.split(/\r?\n/).map((line) => line.trim()).find(Boolean)
87
+ return first && existsSync(first) ? first : null
88
+ } catch {
89
+ return null
90
+ }
91
+ }
92
+
93
+ for (const entry of (env.PATH ?? '').split(pathDelimiter)) {
94
+ if (!entry) continue
95
+ const candidate = join(entry, 'gsd-browser')
96
+ if (existsSync(candidate)) return candidate
97
+ }
98
+
99
+ return null
100
+ }
101
+
102
+ function resolveRealPath(pathValue: string): string {
103
+ try {
104
+ return realpathSync(pathValue)
105
+ } catch {
106
+ return resolvePath(pathValue)
107
+ }
108
+ }
109
+
110
+ function resolveSymlinkTarget(pathCli: string): string {
111
+ try {
112
+ const stat = lstatSync(pathCli)
113
+ if (!stat.isSymbolicLink()) return pathCli
114
+
115
+ const target = readlinkSync(pathCli)
116
+ return isAbsolute(target) ? target : resolvePath(dirname(pathCli), target)
117
+ } catch {
118
+ // PATH entry vanished or is inaccessible between resolution and sync.
119
+ // Fall back to the original path; subsequent sync will surface a useful
120
+ // error rather than escaping as an unhandled throw.
121
+ return pathCli
122
+ }
123
+ }
124
+
125
+ function resolveHomeDir(env: NodeJS.ProcessEnv): string {
126
+ const fromEnv = env.HOME?.trim() || env.USERPROFILE?.trim()
127
+ return resolvePath(fromEnv || homedir())
128
+ }
129
+
130
+ function canAutoSyncTarget(targetPath: string, env: NodeJS.ProcessEnv): boolean {
131
+ const home = resolveHomeDir(env)
132
+ const resolved = resolvePath(targetPath)
133
+ return pathStartsWith(resolved, home)
134
+ }
135
+
136
+ function syncBinary(installedCli: string, targetPath: string, platform: NodeJS.Platform): void {
137
+ const source = resolveRealPath(installedCli)
138
+ copyFileSync(source, targetPath)
139
+ if (platform !== 'win32') {
140
+ chmodSync(targetPath, 0o755)
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Resolve the gsd-browser binary installed by the active global package manager.
146
+ */
147
+ export function resolveGlobalGsdBrowserCliPath(
148
+ options: { env?: NodeJS.ProcessEnv; argv1?: string; platform?: NodeJS.Platform } = {},
149
+ ): string | null {
150
+ const env = options.env ?? process.env
151
+ const argv1 = options.argv1 ?? process.argv[1]
152
+ const platform = options.platform ?? process.platform
153
+
154
+ if (isBunGlobalInstall(argv1, env)) {
155
+ return (
156
+ tryExecLookup('bun', ['pm', 'bin', '-g'], env, platform, tryResolveFromBinDir)
157
+ ?? (env.BUN_INSTALL ? tryResolveFromBinDir(join(env.BUN_INSTALL, 'bin'), platform) : null)
158
+ ?? tryResolveFromBinDir(join(homedir(), '.bun', 'bin'), platform)
159
+ )
160
+ }
161
+
162
+ if (isPnpmInstall(argv1, env)) {
163
+ return (
164
+ tryExecLookup('pnpm', ['bin', '-g'], env, platform, tryResolveFromBinDir)
165
+ ?? tryExecLookup('pnpm', ['root', '-g'], env, platform, tryResolveFromPackageRoot)
166
+ )
167
+ }
168
+
169
+ return (
170
+ tryExecLookup('npm', ['bin', '-g'], env, platform, tryResolveFromBinDir)
171
+ ?? tryExecLookup('npm', ['root', '-g'], env, platform, tryResolveFromPackageRoot)
172
+ )
173
+ }
174
+
175
+ /**
176
+ * Resolve the gsd-browser binary that wins on PATH (`command -v` / `where`).
177
+ */
178
+ export function resolveGsdBrowserOnPath(
179
+ env: NodeJS.ProcessEnv = process.env,
180
+ platform: NodeJS.Platform = process.platform,
181
+ ): string | null {
182
+ return resolvePathBinary(env, platform)
183
+ }
184
+
185
+ /**
186
+ * After a global gsd-browser install, ensure the PATH-resolved binary matches
187
+ * the freshly installed global binary when an older copy is shadowing it.
188
+ */
189
+ export function reconcileGsdBrowserPathAfterInstall(
190
+ options: {
191
+ latestVersion: string
192
+ compareSemver: (a: string, b: string) => number
193
+ resolvePathVersion: (env: NodeJS.ProcessEnv) => string | null
194
+ env?: NodeJS.ProcessEnv
195
+ argv1?: string
196
+ platform?: NodeJS.Platform
197
+ },
198
+ ): GsdBrowserPathReconcileResult {
199
+ const env = options.env ?? process.env
200
+ const argv1 = options.argv1 ?? process.argv[1]
201
+ const platform = options.platform ?? process.platform
202
+
203
+ const installedCli = resolveGlobalGsdBrowserCliPath({ env, argv1, platform })
204
+ if (!installedCli) {
205
+ return { action: 'none' }
206
+ }
207
+
208
+ const pathCli = resolveGsdBrowserOnPath(env, platform)
209
+ const installedReal = resolveRealPath(installedCli)
210
+ if (pathCli && resolveRealPath(pathCli) === installedReal) {
211
+ return { action: 'none', pathCli, installedCli }
212
+ }
213
+
214
+ const pathVersion = options.resolvePathVersion(env)
215
+ if (pathVersion && options.compareSemver(pathVersion, options.latestVersion) >= 0) {
216
+ return { action: 'none', pathCli: pathCli ?? undefined, installedCli }
217
+ }
218
+
219
+ if (!pathCli) {
220
+ return {
221
+ action: 'shadowed',
222
+ installedCli,
223
+ message:
224
+ 'Installed gsd-browser globally, but no gsd-browser was found on PATH. Add your package manager global bin directory to PATH.',
225
+ }
226
+ }
227
+
228
+ const syncTarget = resolveSymlinkTarget(pathCli)
229
+ if (!canAutoSyncTarget(syncTarget, env)) {
230
+ return {
231
+ action: 'shadowed',
232
+ pathCli,
233
+ installedCli,
234
+ syncTarget,
235
+ message:
236
+ `PATH resolves gsd-browser to ${pathCli}, but the updated global install is at ${installedCli}. ` +
237
+ 'Move your package manager global bin directory ahead of the stale location on PATH, or update the stale binary manually.',
238
+ }
239
+ }
240
+
241
+ let syncSucceeded = false
242
+ try {
243
+ syncBinary(installedCli, syncTarget, platform)
244
+ syncSucceeded = true
245
+ } catch {
246
+ // Fall through to shadowed guidance.
247
+ }
248
+
249
+ if (syncSucceeded) {
250
+ const refreshedVersion = options.resolvePathVersion(env)
251
+ const verified = refreshedVersion !== null
252
+ && options.compareSemver(refreshedVersion, options.latestVersion) >= 0
253
+ return {
254
+ action: 'synced',
255
+ pathCli,
256
+ installedCli,
257
+ syncTarget,
258
+ message: verified
259
+ ? `Synced PATH-resolved gsd-browser at ${syncTarget} to the updated global install.`
260
+ : `Synced PATH-resolved gsd-browser at ${syncTarget} to the updated global install. Could not verify the new version on PATH; restart your shell or rerun if it still reports the old version.`,
261
+ }
262
+ }
263
+
264
+ return {
265
+ action: 'shadowed',
266
+ pathCli,
267
+ installedCli,
268
+ syncTarget,
269
+ message:
270
+ `PATH resolves gsd-browser to ${pathCli}, but the updated global install is at ${installedCli}. ` +
271
+ 'Move your package manager global bin directory ahead of the stale location on PATH, or update the stale binary manually.',
272
+ }
273
+ }
@@ -12,7 +12,7 @@ function hasPnpmPath(value: string | undefined): boolean {
12
12
  )
13
13
  }
14
14
 
15
- function pathStartsWith(pathValue: string | undefined, dir: string): boolean {
15
+ export function pathStartsWith(pathValue: string | undefined, dir: string): boolean {
16
16
  if (!pathValue) return false
17
17
  const resolvedPath = resolvePath(pathValue)
18
18
  const resolvedDir = resolvePath(dir)