@jmfederico/pi-web 1.202607.3 → 1.202608.1

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 (236) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +69 -7
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/{CodeViewer-CDVbMiN9.js → CodeViewer-mEiBZFA5.js} +2 -2
  5. package/dist/client/assets/{TerminalPanel-CTS1CgqF.js → TerminalPanel-O9381Eme.js} +6 -6
  6. package/dist/client/assets/index-uI2AGFUy.js +4091 -0
  7. package/dist/client/index.html +1 -1
  8. package/dist/config.js +109 -73
  9. package/dist/config.js.map +1 -1
  10. package/dist/docker/piWebDockerCommandPlan.js +4 -3
  11. package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
  12. package/dist/nativeServices/servicePlan.js +1 -1
  13. package/dist/nativeServices/servicePlan.js.map +1 -1
  14. package/dist/pi-web-plugins/git/browser/git-contract.js +108 -0
  15. package/dist/pi-web-plugins/git/browser/git-panel.js +609 -0
  16. package/dist/pi-web-plugins/git/browser/gitFileList.js +43 -0
  17. package/dist/pi-web-plugins/git/browser/gitFileShared.js +16 -0
  18. package/dist/pi-web-plugins/git/browser/gitFileTree.js +81 -0
  19. package/dist/pi-web-plugins/git/browser/gitFileViewPreference.js +35 -0
  20. package/dist/pi-web-plugins/git/browser/gitRoute.js +42 -0
  21. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +10 -0
  22. package/dist/pi-web-plugins/git/browser/unifiedDiff.js +301 -0
  23. package/dist/{server/git/gitService.js → pi-web-plugins/git/git-backend.js} +209 -60
  24. package/dist/pi-web-plugins/git/package.json +16 -0
  25. package/dist/pi-web-plugins/git/server-plugin.js +176 -0
  26. package/dist/pi-web-plugins/info/infoInternals.js +1 -2
  27. package/dist/pi-web-plugins/info/package.json +1 -1
  28. package/dist/pi-web-plugins/info/pi-web-plugin.js +2 -2
  29. package/dist/pi-web-plugins/relays/package.json +1 -1
  30. package/dist/pi-web-plugins/relays/pi-web-plugin.js +3 -3
  31. package/dist/pi-web-plugins/relays/relayDiscovery.js +159 -27
  32. package/dist/pi-web-plugins/relays/relaysPanelElement.js +173 -16
  33. package/dist/pi-web-plugins/updates/package.json +1 -1
  34. package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
  35. package/dist/pi-web-plugins/workspace-tasks/package.json +1 -1
  36. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +3 -3
  37. package/dist/plugin-api.d.ts +33 -16
  38. package/dist/pluginRecoveryCli.js +172 -0
  39. package/dist/pluginRecoveryCli.js.map +1 -0
  40. package/dist/server/activity/workspaceActivityService.js +27 -19
  41. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  42. package/dist/server/app.js +35 -37
  43. package/dist/server/app.js.map +1 -1
  44. package/dist/server/browserMessageProjection.js +0 -2
  45. package/dist/server/browserMessageProjection.js.map +1 -1
  46. package/dist/server/configRoutes.js +4 -31
  47. package/dist/server/configRoutes.js.map +1 -1
  48. package/dist/server/machines/machineClient.js +23 -4
  49. package/dist/server/machines/machineClient.js.map +1 -1
  50. package/dist/server/machines/machinePluginProxyRoutes.js +74 -17
  51. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  52. package/dist/server/machines/machineProxyRoutes.js +167 -21
  53. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  54. package/dist/server/machines/machineService.js +22 -0
  55. package/dist/server/machines/machineService.js.map +1 -1
  56. package/dist/server/piWebPluginCatalog.js +584 -0
  57. package/dist/server/piWebPluginCatalog.js.map +1 -0
  58. package/dist/server/piWebPluginLifecycle.js +162 -0
  59. package/dist/server/piWebPluginLifecycle.js.map +1 -0
  60. package/dist/server/piWebPluginService.js +159 -261
  61. package/dist/server/piWebPluginService.js.map +1 -1
  62. package/dist/server/piWebStatus.js +40 -45
  63. package/dist/server/piWebStatus.js.map +1 -1
  64. package/dist/server/plugins/pluginBackendProxyRoutes.js +69 -0
  65. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -0
  66. package/dist/server/plugins/serverPluginExec.js +184 -0
  67. package/dist/server/plugins/serverPluginExec.js.map +1 -0
  68. package/dist/server/plugins/serverPluginRuntime.js +480 -0
  69. package/dist/server/plugins/serverPluginRuntime.js.map +1 -0
  70. package/dist/server/realtime/sessionEventHub.js +26 -11
  71. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  72. package/dist/server/requestCancellation.js +32 -0
  73. package/dist/server/requestCancellation.js.map +1 -0
  74. package/dist/server/sessiond/agentHttpDispatcher.js +108 -0
  75. package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -0
  76. package/dist/server/sessiond/agentProcessEnvironment.js +61 -0
  77. package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -0
  78. package/dist/server/sessiond/pluginBackendRoutes.js +64 -0
  79. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -0
  80. package/dist/server/sessiond/sessionDaemonShutdown.js +24 -0
  81. package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -0
  82. package/dist/server/sessiond/sessionProxyRoutes.js +2 -2
  83. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  84. package/dist/server/sessiond/sessionServiceDependencies.js +3 -1
  85. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  86. package/dist/server/sessiond/sessiondStateOwnership.js +235 -0
  87. package/dist/server/sessiond/sessiondStateOwnership.js.map +1 -0
  88. package/dist/server/sessiond/workspaceCatalogRoutes.js +31 -0
  89. package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -0
  90. package/dist/server/sessiond/workspaceRemovalRoutes.js +41 -0
  91. package/dist/server/sessiond/workspaceRemovalRoutes.js.map +1 -0
  92. package/dist/server/sessiond.js +283 -96
  93. package/dist/server/sessiond.js.map +1 -1
  94. package/dist/server/sessions/askUserTool.js +0 -3
  95. package/dist/server/sessions/askUserTool.js.map +1 -1
  96. package/dist/server/sessions/authRoutes.js +0 -10
  97. package/dist/server/sessions/authRoutes.js.map +1 -1
  98. package/dist/server/sessions/authService.js +15 -78
  99. package/dist/server/sessions/authService.js.map +1 -1
  100. package/dist/server/sessions/dockerEnvironmentFacts.js +254 -0
  101. package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -0
  102. package/dist/server/sessions/messagePaging.js +2 -2
  103. package/dist/server/sessions/messagePaging.js.map +1 -1
  104. package/dist/server/sessions/modelCatalogRefresher.js +4 -3
  105. package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
  106. package/dist/server/sessions/oauthLoginFlowService.js +2 -5
  107. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
  108. package/dist/server/sessions/pendingAskStore.js +0 -3
  109. package/dist/server/sessions/pendingAskStore.js.map +1 -1
  110. package/dist/server/sessions/piSessionManagerGateway.js +134 -3
  111. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  112. package/dist/server/sessions/piSessionService.js +318 -253
  113. package/dist/server/sessions/piSessionService.js.map +1 -1
  114. package/dist/server/sessions/sessionCommandService.js +24 -1
  115. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  116. package/dist/server/sessions/sessionEnvironmentFacts.js +48 -0
  117. package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -0
  118. package/dist/server/sessions/sessionFileFormat.js +27 -0
  119. package/dist/server/sessions/sessionFileFormat.js.map +1 -0
  120. package/dist/server/sessions/sessionFileHeader.js +82 -23
  121. package/dist/server/sessions/sessionFileHeader.js.map +1 -1
  122. package/dist/server/sessions/sessionRoutes.js +88 -56
  123. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  124. package/dist/server/sessions/sessionSummaryScanner.js +491 -0
  125. package/dist/server/sessions/sessionSummaryScanner.js.map +1 -0
  126. package/dist/server/sessions/spawnSessionTool.js +8 -1
  127. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  128. package/dist/server/sessions/spawnSubsessionTool.js +11 -9
  129. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  130. package/dist/server/sessions/spawnTargetResolver.js +1 -1
  131. package/dist/server/status/machineStatusRoutes.js +8 -0
  132. package/dist/server/status/machineStatusRoutes.js.map +1 -0
  133. package/dist/server/status/machineStatusService.js +181 -0
  134. package/dist/server/status/machineStatusService.js.map +1 -0
  135. package/dist/server/status/workspaceAttribution.js +87 -0
  136. package/dist/server/status/workspaceAttribution.js.map +1 -0
  137. package/dist/server/terminalProxyRoutes.js +2 -1
  138. package/dist/server/terminalProxyRoutes.js.map +1 -1
  139. package/dist/server/workspaceExplorerRoutes.js +20 -13
  140. package/dist/server/workspaceExplorerRoutes.js.map +1 -1
  141. package/dist/server/workspaces/effectivePathAccess.js +0 -19
  142. package/dist/server/workspaces/effectivePathAccess.js.map +1 -1
  143. package/dist/server/workspaces/fileContentService.js +18 -9
  144. package/dist/server/workspaces/fileContentService.js.map +1 -1
  145. package/dist/server/workspaces/filePreviewResponseHeaders.js +18 -0
  146. package/dist/server/workspaces/filePreviewResponseHeaders.js.map +1 -0
  147. package/dist/server/workspaces/filePreviewResponsePolicy.js +55 -0
  148. package/dist/server/workspaces/filePreviewResponsePolicy.js.map +1 -0
  149. package/dist/server/workspaces/filePreviewService.js +81 -0
  150. package/dist/server/workspaces/filePreviewService.js.map +1 -0
  151. package/dist/server/workspaces/projectWorkspaceCwds.js +0 -5
  152. package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -1
  153. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +402 -0
  154. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -0
  155. package/dist/server/workspaces/workspaceCatalog.js +47 -0
  156. package/dist/server/workspaces/workspaceCatalog.js.map +1 -0
  157. package/dist/server/workspaces/workspaceContext.js +1 -3
  158. package/dist/server/workspaces/workspaceContext.js.map +1 -1
  159. package/dist/server/workspaces/workspaceDeletionRoutes.js +39 -103
  160. package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
  161. package/dist/server/workspaces/workspaceProviderRegistry.js +651 -0
  162. package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -0
  163. package/dist/server/workspaces/workspaceRemovalService.js +256 -0
  164. package/dist/server/workspaces/workspaceRemovalService.js.map +1 -0
  165. package/dist/server/workspaces/workspaceRouteErrors.js +7 -0
  166. package/dist/server/workspaces/workspaceRouteErrors.js.map +1 -0
  167. package/dist/server/workspaces/worktreePreRemoveHook.js +39 -0
  168. package/dist/server/workspaces/worktreePreRemoveHook.js.map +1 -0
  169. package/dist/server-plugin-api.d.ts +140 -0
  170. package/dist/server-plugin-api.js +2 -0
  171. package/dist/server-plugin-api.js.map +1 -0
  172. package/dist/serverPluginRecovery.js +138 -0
  173. package/dist/serverPluginRecovery.js.map +1 -0
  174. package/dist/sessiond/activeAgentProfile.js +3 -22
  175. package/dist/sessiond/activeAgentProfile.js.map +1 -1
  176. package/dist/sessiond/config.js +13 -2
  177. package/dist/sessiond/config.js.map +1 -1
  178. package/dist/sessiond/sessionDaemonClient.js +11 -9
  179. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  180. package/dist/shared/activeAgentProfile.js +2 -41
  181. package/dist/shared/activeAgentProfile.js.map +1 -1
  182. package/dist/shared/activity.js +0 -3
  183. package/dist/shared/activity.js.map +1 -1
  184. package/dist/shared/apiTypes.js +7 -14
  185. package/dist/shared/apiTypes.js.map +1 -1
  186. package/dist/shared/capabilities.js +8 -40
  187. package/dist/shared/capabilities.js.map +1 -1
  188. package/dist/shared/federatedRoutes.js +28 -8
  189. package/dist/shared/federatedRoutes.js.map +1 -1
  190. package/dist/shared/machinePluginIds.js +4 -2
  191. package/dist/shared/machinePluginIds.js.map +1 -1
  192. package/dist/shared/machineStatus.js +94 -0
  193. package/dist/shared/machineStatus.js.map +1 -0
  194. package/dist/shared/piWebStatusParsing.js +29 -0
  195. package/dist/shared/piWebStatusParsing.js.map +1 -1
  196. package/dist/shared/pluginApiTypes.d.ts +148 -0
  197. package/dist/shared/pluginApiTypes.js +3 -0
  198. package/dist/shared/pluginApiTypes.js.map +1 -0
  199. package/dist/shared/pluginBackendProtocol.js +110 -0
  200. package/dist/shared/pluginBackendProtocol.js.map +1 -0
  201. package/dist/shared/pluginIds.js +5 -0
  202. package/dist/shared/pluginIds.js.map +1 -1
  203. package/dist/shared/pluginRecoveryCommands.js +14 -0
  204. package/dist/shared/pluginRecoveryCommands.js.map +1 -0
  205. package/dist/shared/workspaceFiles.js +41 -2
  206. package/dist/shared/workspaceFiles.js.map +1 -1
  207. package/dist/shared/workspaceRemovalProtocol.js +24 -0
  208. package/dist/shared/workspaceRemovalProtocol.js.map +1 -0
  209. package/docs/config.md +140 -47
  210. package/docs/plugins.md +386 -137
  211. package/examples/workspace-provider-plugin/README.md +52 -0
  212. package/examples/workspace-provider-plugin/package.json +25 -0
  213. package/examples/workspace-provider-plugin/src/browser/index.ts +69 -0
  214. package/examples/workspace-provider-plugin/src/server.ts +64 -0
  215. package/examples/workspace-provider-plugin/tsconfig.json +21 -0
  216. package/package.json +32 -12
  217. package/server-plugin-api.d.ts +1 -0
  218. package/dist/client/assets/UnifiedDiffViewer-Cqke12ZX.js +0 -39
  219. package/dist/client/assets/index-LME0LfPb.js +0 -4068
  220. package/dist/plugin-api/unstable.d.ts +0 -22
  221. package/dist/server/activity/workspaceActivityRoutes.js +0 -4
  222. package/dist/server/activity/workspaceActivityRoutes.js.map +0 -1
  223. package/dist/server/git/gitService.js.map +0 -1
  224. package/dist/server/gitRoutes.js +0 -23
  225. package/dist/server/gitRoutes.js.map +0 -1
  226. package/dist/server/sessions/parentSessionLocator.js +0 -75
  227. package/dist/server/sessions/parentSessionLocator.js.map +0 -1
  228. package/dist/server/workspaces/gitWorktreeDiscovery.js +0 -43
  229. package/dist/server/workspaces/gitWorktreeDiscovery.js.map +0 -1
  230. package/dist/server/workspaces/imagePreviewService.js +0 -40
  231. package/dist/server/workspaces/imagePreviewService.js.map +0 -1
  232. package/dist/server/workspaces/workspaceService.js +0 -52
  233. package/dist/server/workspaces/workspaceService.js.map +0 -1
  234. package/dist/shared/apiTypes.d.ts +0 -1234
  235. package/dist/shared/thinkingLevels.d.ts +0 -27
  236. package/plugin-api/unstable.d.ts +0 -1
@@ -1,16 +1,41 @@
1
+ // Generated from pi-web-plugins/git/git-backend.ts. Do not edit directly.
1
2
  import { createHash } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
- import { existsSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { normalizeRelativePath } from "../workspaces/pathSafety.js";
6
- import { sanitizedGitEnv } from "./gitEnv.js";
7
- const MAX_OUTPUT = 2 * 1024 * 1024;
8
- export async function gitStatus(cwd) {
3
+ import { realpath } from "node:fs/promises";
4
+ import { isAbsolute, join, relative, sep } from "node:path";
5
+ import { GIT_DIFF_OPERATION, GIT_STATUS_OPERATION, } from "./browser/git-contract.js";
6
+ export { GIT_DIFF_OPERATION, GIT_STATUS_OPERATION } from "./browser/git-contract.js";
7
+ const GIT_COMMAND_TIMEOUT_MS = 10_000;
8
+ const GIT_LOCAL_ENV_VARS = Object.freeze([
9
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
10
+ "GIT_COMMON_DIR",
11
+ "GIT_DIR",
12
+ "GIT_INDEX_FILE",
13
+ "GIT_OBJECT_DIRECTORY",
14
+ "GIT_PREFIX",
15
+ "GIT_QUARANTINE_PATH",
16
+ "GIT_WORK_TREE",
17
+ ]);
18
+ /** Dispatch the Git-owned status/diff schema through the provider's public request seam. */
19
+ export async function requestGitBackend(activationContext, request) {
20
+ const runGit = createGitRunner(activationContext, request.signal);
21
+ if (request.operation === GIT_STATUS_OPERATION) {
22
+ requireStatusInput(request.input);
23
+ return statusProviderResponse(await gitStatusWithRunner(runGit, request.workspace.path));
24
+ }
25
+ if (request.operation === GIT_DIFF_OPERATION) {
26
+ return diffProviderResponse(await gitDiffWithRunner(runGit, request.workspace.path, parseDiffInput(request.input)));
27
+ }
28
+ throw new Error(`Unsupported Git workspace backend operation: ${request.operation}`);
29
+ }
30
+ export async function gitStatus(context, cwd, signal) {
31
+ return gitStatusWithRunner(createGitRunner(context, signal), cwd);
32
+ }
33
+ async function gitStatusWithRunner(runGit, cwd) {
9
34
  const result = await runGit(cwd, ["status", "--porcelain=v2", "--branch", "--untracked-files=all", "-z"]);
10
35
  if (result.code !== 0)
11
36
  return { isGitRepo: false, hash: hash(result.stdout + result.stderr), files: [], submodules: [] };
12
37
  const parsed = parseStatus(result.stdout, { deferSubmodules: true });
13
- return expandSubmodules(cwd, parsed, result.stdout);
38
+ return expandSubmodules(runGit, cwd, parsed, result.stdout);
14
39
  }
15
40
  /**
16
41
  * Merge each dirty submodule's own changes into the flat file list. A moved
@@ -19,11 +44,17 @@ export async function gitStatus(cwd) {
19
44
  * entries under `<submodule>/<inner path>`. A plain `-dirty` pointer (commit
20
45
  * unchanged) is intentionally not surfaced as a pointer entry.
21
46
  */
22
- async function expandSubmodules(cwd, parsed, topRaw) {
47
+ async function expandSubmodules(runGit, cwd, parsed, topRaw) {
23
48
  // Fan out concurrently — one `git status` per dirty submodule plus one
24
49
  // `git rev-parse` per unstaged pointer move — then concatenate in input
25
50
  // order so the file list and hash are identical to a serial pass.
26
- const expanded = await Promise.all(parsed.submodules.map(async (sub) => ({ path: sub.path, ...(await expandSubmodule(cwd, sub)) })));
51
+ const canonicalRoot = parsed.submodules.length === 0 ? undefined : await canonicalPath(cwd);
52
+ const expanded = await Promise.all(parsed.submodules.map(async (sub) => {
53
+ const location = canonicalRoot === undefined
54
+ ? undefined
55
+ : await validatedSubmodule(runGit, cwd, canonicalRoot, sub.path);
56
+ return { path: sub.path, ...(await expandSubmodule(runGit, sub, location)) };
57
+ }));
27
58
  const files = [...parsed.files];
28
59
  const dirtySubmodulePaths = [];
29
60
  let extraForHash = "";
@@ -44,7 +75,7 @@ async function expandSubmodules(cwd, parsed, topRaw) {
44
75
  };
45
76
  }
46
77
  /** Expand one dirty submodule: the pointer entry first, then its inner files. */
47
- async function expandSubmodule(cwd, sub) {
78
+ async function expandSubmodule(runGit, sub, location) {
48
79
  const files = [];
49
80
  let extraForHash = "";
50
81
  if (sub.commitChanged) {
@@ -53,11 +84,11 @@ async function expandSubmodule(cwd, sub) {
53
84
  index: sub.index,
54
85
  workingTree: sub.workingTree,
55
86
  submoduleFromCommit: displayFromCommit(sub.headOid),
56
- submoduleToCommit: short(await resolveSubmoduleToCommit(cwd, sub)),
87
+ submoduleToCommit: short(await resolveSubmoduleToCommit(runGit, location?.cwd, sub)),
57
88
  });
58
89
  }
59
- if (sub.hasModifiedContent || sub.hasUntrackedContent) {
60
- const inner = await runGit(join(cwd, sub.path), ["status", "--porcelain=v2", "--untracked-files=all", "-z"]);
90
+ if ((sub.hasModifiedContent || sub.hasUntrackedContent) && location !== undefined) {
91
+ const inner = await runGit(location.cwd, ["status", "--porcelain=v2", "--untracked-files=all", "-z"]);
61
92
  if (inner.code === 0) {
62
93
  extraForHash = `\0${sub.path}\0${inner.stdout}`;
63
94
  const innerFiles = parseStatus(inner.stdout, { deferSubmodules: false }).files;
@@ -73,24 +104,28 @@ async function expandSubmodule(cwd, sub) {
73
104
  }
74
105
  return { files, extraForHash };
75
106
  }
76
- async function resolveSubmoduleToCommit(cwd, sub) {
107
+ async function resolveSubmoduleToCommit(runGit, cwd, sub) {
77
108
  // Staged pointer moves already expose the new commit as the index OID; an
78
- // unstaged move only records the old OID, so read the submodule's HEAD.
79
- if (sub.indexOid !== sub.headOid)
109
+ // unstaged move only records the old OID, so read the validated submodule's
110
+ // HEAD. An unavailable checkout cannot safely supply a different pointer.
111
+ if (sub.indexOid !== sub.headOid || cwd === undefined)
80
112
  return sub.indexOid;
81
- const head = await runGit(join(cwd, sub.path), ["rev-parse", "HEAD"]);
113
+ const head = await runGit(cwd, ["rev-parse", "HEAD"]);
82
114
  const resolved = head.stdout.trim();
83
115
  return head.code === 0 && resolved !== "" ? resolved : sub.indexOid;
84
116
  }
85
- export async function gitDiff(cwd, options) {
117
+ export async function gitDiff(context, cwd, options, signal) {
118
+ return gitDiffWithRunner(createGitRunner(context, signal), cwd, options);
119
+ }
120
+ async function gitDiffWithRunner(runGit, cwd, options) {
86
121
  const staged = options.staged === true;
87
122
  let path;
88
123
  if (options.path !== undefined && options.path !== "")
89
124
  path = normalizeRelativePath(options.path);
90
125
  if (path !== undefined) {
91
- const owner = await submoduleForPath(cwd, path);
126
+ const owner = await submoduleForPath(runGit, cwd, path);
92
127
  if (owner !== undefined)
93
- return submoduleDiff(cwd, owner, path, staged);
128
+ return submoduleDiff(runGit, owner, path, staged);
94
129
  }
95
130
  const args = ["diff", "--no-ext-diff", "--color=never"];
96
131
  if (staged)
@@ -100,7 +135,7 @@ export async function gitDiff(cwd, options) {
100
135
  const result = await runGit(cwd, args);
101
136
  if (result.code !== 0)
102
137
  throw new Error(result.stderr.trim() || "git diff failed");
103
- if (!staged && path !== undefined && result.stdout === "" && await isUntracked(cwd, path)) {
138
+ if (!staged && path !== undefined && result.stdout === "" && await isUntracked(runGit, cwd, path)) {
104
139
  const untracked = await runGit(cwd, ["diff", "--no-ext-diff", "--color=never", "--no-index", "/dev/null", "--", path]);
105
140
  if (untracked.code !== 0 && untracked.code !== 1)
106
141
  throw new Error(untracked.stderr.trim() || "git diff failed");
@@ -114,9 +149,9 @@ export async function gitDiff(cwd, options) {
114
149
  * The response path stays the full superproject-relative path so the viewer and
115
150
  * the selected row line up.
116
151
  */
117
- async function submoduleDiff(cwd, owner, path, staged) {
118
- const subCwd = join(cwd, owner);
119
- const rel = normalizeRelativePath(path.slice(owner.length + 1));
152
+ async function submoduleDiff(runGit, owner, path, staged) {
153
+ const subCwd = owner.cwd;
154
+ const rel = normalizeRelativePath(path.slice(owner.path.length + 1));
120
155
  const args = ["diff", "--no-ext-diff", "--color=never"];
121
156
  if (staged)
122
157
  args.push("--cached");
@@ -124,7 +159,7 @@ async function submoduleDiff(cwd, owner, path, staged) {
124
159
  const result = await runGit(subCwd, args);
125
160
  if (result.code !== 0)
126
161
  throw new Error(result.stderr.trim() || "git diff failed");
127
- if (!staged && result.stdout === "" && await isUntracked(subCwd, rel)) {
162
+ if (!staged && result.stdout === "" && await isUntracked(runGit, subCwd, rel)) {
128
163
  const untracked = await runGit(subCwd, ["diff", "--no-ext-diff", "--color=never", "--no-index", "/dev/null", "--", rel]);
129
164
  if (untracked.code !== 0 && untracked.code !== 1)
130
165
  throw new Error(untracked.stderr.trim() || "git diff failed");
@@ -132,12 +167,12 @@ async function submoduleDiff(cwd, owner, path, staged) {
132
167
  }
133
168
  return { path, staged, hash: hash(result.stdout), diff: result.stdout, truncated: result.truncated };
134
169
  }
135
- async function isUntracked(cwd, path) {
170
+ async function isUntracked(runGit, cwd, path) {
136
171
  const result = await runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--", path]);
137
172
  return result.code === 0 && result.stdout.split("\0").includes(path);
138
173
  }
139
174
  /** Configured direct-submodule paths (depth 1), read from `.gitmodules`. */
140
- async function configuredSubmodulePaths(cwd) {
175
+ async function configuredSubmodulePaths(runGit, cwd) {
141
176
  // `-z` emits `<key>\n<value>\0` records; keys may themselves contain spaces
142
177
  // (`submodule.my sub.path`), so splitting lines at the first space mangles
143
178
  // paths with spaces in them.
@@ -151,26 +186,73 @@ async function configuredSubmodulePaths(cwd) {
151
186
  const newlineAt = record.indexOf("\n");
152
187
  if (newlineAt === -1)
153
188
  continue;
154
- paths.push(record.slice(newlineAt + 1));
189
+ try {
190
+ const path = normalizeRelativePath(record.slice(newlineAt + 1));
191
+ if (path !== "")
192
+ paths.push(path);
193
+ }
194
+ catch {
195
+ // A malformed repository path is not eligible for submodule routing.
196
+ }
155
197
  }
156
- return paths;
198
+ return [...new Set(paths)];
157
199
  }
158
- /** The submodule that strictly contains `path`, if any (longest match wins). */
159
- async function submoduleForPath(cwd, path) {
160
- // Cheap bail-outs before spawning `git config`: a path strictly inside a
161
- // submodule always contains `/`, and without `.gitmodules` there are no
162
- // configured submodules to look up (every diff call used to pay this spawn).
200
+ /** The validated gitlink that strictly contains `path`, if any (longest match wins). */
201
+ async function submoduleForPath(runGit, cwd, path) {
163
202
  if (!path.includes("/"))
164
203
  return undefined;
165
- if (!existsSync(join(cwd, ".gitmodules")))
204
+ const candidates = (await configuredSubmodulePaths(runGit, cwd))
205
+ .filter((sub) => path.startsWith(`${sub}/`))
206
+ .sort((left, right) => right.length - left.length);
207
+ if (candidates.length === 0)
208
+ return undefined;
209
+ const canonicalRoot = await canonicalPath(cwd);
210
+ if (canonicalRoot === undefined)
211
+ return undefined;
212
+ for (const candidate of candidates) {
213
+ const validated = await validatedSubmodule(runGit, cwd, canonicalRoot, candidate);
214
+ if (validated !== undefined)
215
+ return validated;
216
+ }
217
+ return undefined;
218
+ }
219
+ /**
220
+ * Confirm that repository-controlled `.gitmodules` data names an index
221
+ * gitlink and that its checkout resolves strictly below the workspace root.
222
+ * Commands use the resolved checkout rather than following the configured
223
+ * path as a symlink.
224
+ */
225
+ async function validatedSubmodule(runGit, cwd, canonicalRoot, path) {
226
+ const index = await runGit(cwd, ["ls-files", "--stage", "-z", "--", path]);
227
+ if (index.code !== 0 || !hasGitlink(index.stdout, path))
228
+ return undefined;
229
+ const candidate = await canonicalPath(join(cwd, path));
230
+ if (candidate === undefined || !isStrictDescendant(canonicalRoot, candidate))
231
+ return undefined;
232
+ return { path, cwd: candidate };
233
+ }
234
+ function hasGitlink(raw, path) {
235
+ return raw.split("\0").some((record) => {
236
+ const separator = record.indexOf("\t");
237
+ if (separator === -1 || record.slice(separator + 1) !== path)
238
+ return false;
239
+ return record.slice(0, separator).split(" ")[0] === "160000";
240
+ });
241
+ }
242
+ async function canonicalPath(path) {
243
+ try {
244
+ return await realpath(path);
245
+ }
246
+ catch {
166
247
  return undefined;
167
- const subs = await configuredSubmodulePaths(cwd);
168
- let best;
169
- for (const sub of subs) {
170
- if (sub !== "" && path.startsWith(`${sub}/`) && (best === undefined || sub.length > best.length))
171
- best = sub;
172
248
  }
173
- return best;
249
+ }
250
+ function isStrictDescendant(root, candidate) {
251
+ const relation = relative(root, candidate);
252
+ return relation !== ""
253
+ && relation !== ".."
254
+ && !relation.startsWith(`..${sep}`)
255
+ && !isAbsolute(relation);
174
256
  }
175
257
  function parseStatus(raw, options) {
176
258
  const records = raw.split("\0").filter((record) => record !== "");
@@ -269,22 +351,89 @@ function displayFromCommit(headOid) {
269
351
  function hash(value) {
270
352
  return createHash("sha1").update(value).digest("hex");
271
353
  }
272
- async function runGit(cwd, args) {
273
- return new Promise((resolve, reject) => {
274
- const child = spawn("git", args, { cwd, env: sanitizedGitEnv(), stdio: ["ignore", "pipe", "pipe"] });
275
- const timer = setTimeout(() => { child.kill("SIGKILL"); }, 10000);
276
- let stdout = Buffer.alloc(0);
277
- let stderr = Buffer.alloc(0);
278
- let truncated = false;
279
- child.stdout.on("data", (chunk) => {
280
- if (stdout.length + chunk.length > MAX_OUTPUT)
281
- truncated = true;
282
- if (stdout.length < MAX_OUTPUT)
283
- stdout = Buffer.concat([stdout, chunk]).subarray(0, MAX_OUTPUT);
284
- });
285
- child.stderr.on("data", (chunk) => { stderr = Buffer.concat([stderr, chunk]).subarray(0, 64 * 1024); });
286
- child.on("error", (error) => { clearTimeout(timer); reject(error); });
287
- child.on("close", (code) => { clearTimeout(timer); resolve({ code: code ?? 1, stdout: stdout.toString("utf8"), stderr: stderr.toString("utf8"), truncated }); });
288
- });
354
+ function statusProviderResponse(status) {
355
+ return {
356
+ isGitRepo: status.isGitRepo,
357
+ hash: status.hash,
358
+ ...(status.branch === undefined ? {} : { branch: status.branch }),
359
+ ...(status.upstream === undefined ? {} : { upstream: status.upstream }),
360
+ ...(status.ahead === undefined ? {} : { ahead: status.ahead }),
361
+ ...(status.behind === undefined ? {} : { behind: status.behind }),
362
+ files: status.files.map((file) => ({
363
+ path: file.path,
364
+ ...(file.oldPath === undefined ? {} : { oldPath: file.oldPath }),
365
+ index: file.index,
366
+ workingTree: file.workingTree,
367
+ ...(file.submoduleFromCommit === undefined ? {} : { submoduleFromCommit: file.submoduleFromCommit }),
368
+ ...(file.submoduleToCommit === undefined ? {} : { submoduleToCommit: file.submoduleToCommit }),
369
+ })),
370
+ submodules: status.submodules,
371
+ };
372
+ }
373
+ function diffProviderResponse(diff) {
374
+ return {
375
+ ...(diff.path === undefined ? {} : { path: diff.path }),
376
+ staged: diff.staged,
377
+ hash: diff.hash,
378
+ diff: diff.diff,
379
+ truncated: diff.truncated,
380
+ };
381
+ }
382
+ function createGitRunner(context, signal) {
383
+ return async (cwd, args) => commandResult(await context.execFile({
384
+ file: "git",
385
+ args,
386
+ cwd,
387
+ unsetEnv: GIT_LOCAL_ENV_VARS,
388
+ timeoutMs: GIT_COMMAND_TIMEOUT_MS,
389
+ signal,
390
+ }), args);
391
+ }
392
+ function commandResult(result, args) {
393
+ const command = `git ${args.join(" ")}`;
394
+ if (result.signal !== null)
395
+ throw new Error(`${command} ended from signal ${result.signal}`);
396
+ if (result.exitCode === null)
397
+ throw new Error(`${command} ended without an exit code`);
398
+ return {
399
+ code: result.exitCode,
400
+ stdout: result.stdout,
401
+ stderr: result.stderr,
402
+ truncated: result.stdoutTruncated,
403
+ };
404
+ }
405
+ function requireStatusInput(input) {
406
+ if (input !== null)
407
+ throw new Error("Git status input must be null");
408
+ }
409
+ function parseDiffInput(input) {
410
+ if (!isRecord(input))
411
+ throw new Error("Git diff input must be an object");
412
+ const unsupported = Object.keys(input).find((key) => key !== "path" && key !== "staged");
413
+ if (unsupported !== undefined)
414
+ throw new Error(`Git diff input contains an unsupported field: ${unsupported}`);
415
+ const path = input["path"];
416
+ const staged = input["staged"];
417
+ if (path !== undefined && typeof path !== "string")
418
+ throw new Error("Git diff input path must be a string");
419
+ if (staged !== undefined && typeof staged !== "boolean")
420
+ throw new Error("Git diff input staged must be a boolean");
421
+ return {
422
+ ...(path === undefined ? {} : { path }),
423
+ ...(staged === undefined ? {} : { staged }),
424
+ };
425
+ }
426
+ function normalizeRelativePath(input) {
427
+ const value = input ?? "";
428
+ if (value === "" || value === ".")
429
+ return "";
430
+ if (isAbsolute(value))
431
+ throw new Error("Absolute paths are not allowed");
432
+ const parts = value.split(/[\\/]+/u).filter((part) => part !== "" && part !== ".");
433
+ if (parts.some((part) => part === ".."))
434
+ throw new Error("Path traversal is not allowed");
435
+ return parts.join("/");
436
+ }
437
+ function isRecord(value) {
438
+ return typeof value === "object" && value !== null && !Array.isArray(value);
289
439
  }
290
- //# sourceMappingURL=gitService.js.map
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@pi-web/git-plugin",
3
+ "private": true,
4
+ "type": "module",
5
+ "piWeb": {
6
+ "plugins": [
7
+ {
8
+ "id": "git",
9
+ "browserRoot": "browser",
10
+ "module": "browser/pi-web-plugin.js",
11
+ "serverModule": "server-plugin.js",
12
+ "machineSpecific": true
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,176 @@
1
+ // Generated from pi-web-plugins/git/server-plugin.ts. Do not edit directly.
2
+ import { basename, resolve } from "node:path";
3
+ import { requestGitBackend } from "./git-backend.js";
4
+ const GIT_LOCAL_ENV_VARS = Object.freeze([
5
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
6
+ "GIT_COMMON_DIR",
7
+ "GIT_DIR",
8
+ "GIT_INDEX_FILE",
9
+ "GIT_OBJECT_DIRECTORY",
10
+ "GIT_PREFIX",
11
+ "GIT_QUARANTINE_PATH",
12
+ "GIT_WORK_TREE",
13
+ ]);
14
+ const plugin = {
15
+ apiVersion: 1,
16
+ name: "Git",
17
+ activate(context) {
18
+ return { workspaceProvider: createGitWorkspaceProvider(context) };
19
+ },
20
+ };
21
+ export default plugin;
22
+ export function createGitWorkspaceProvider(context) {
23
+ return Object.freeze({
24
+ fallback: true,
25
+ async probe(project, signal) {
26
+ const result = await runGit(context, project.path, ["rev-parse", "--is-inside-work-tree"], signal);
27
+ if (result.signal !== null)
28
+ throw new Error(`git repository probe ended from signal ${result.signal}`);
29
+ if (result.exitCode !== 0)
30
+ return "pass";
31
+ return result.stdout.trim() === "true" ? "claim" : "pass";
32
+ },
33
+ async list(project, signal) {
34
+ const rootResult = await requireGit(runGit(context, project.path, ["rev-parse", "--show-toplevel"], signal), "resolve the Git worktree root");
35
+ const mainRootOutput = rootResult.stdout.trim();
36
+ if (mainRootOutput === "")
37
+ throw new Error("Git returned an empty worktree root");
38
+ const mainRoot = resolve(mainRootOutput);
39
+ const commonDirectoryResult = await requireGit(runGit(context, project.path, ["rev-parse", "--git-common-dir"], signal), "resolve the Git common directory");
40
+ const commonDirectoryOutput = commonDirectoryResult.stdout.trim();
41
+ if (commonDirectoryOutput === "")
42
+ throw new Error("Git returned an empty common directory");
43
+ const commonDirectory = resolve(project.path, commonDirectoryOutput);
44
+ const listResult = await requireGit(runGit(context, project.path, ["worktree", "list", "--porcelain", "-z"], signal), "list Git worktrees");
45
+ const worktrees = parseGitWorktreeList(listResult.stdout)
46
+ .filter((worktree) => worktree.bare !== true)
47
+ .map((worktree) => {
48
+ const path = resolve(worktree.path);
49
+ return { worktree: { ...worktree, path }, path };
50
+ });
51
+ // Prefer the checkout Git identifies for the registered project. A
52
+ // submodule is the exception: its sole worktree record points at common
53
+ // storage under the superproject instead of at --show-toplevel.
54
+ const mainWorkspacePath = worktrees.some(({ path }) => path === mainRoot)
55
+ ? mainRoot
56
+ : commonDirectory;
57
+ const selectable = worktrees
58
+ .map(({ worktree, path }) => ({ worktree, path, isMain: path === mainWorkspacePath }))
59
+ .filter(({ worktree, path, isMain }) => worktree.prunable !== true || isMain || path === project.path);
60
+ if (selectable.length === 0)
61
+ return [singleGitWorkspace(project)];
62
+ return selectable.map(({ worktree, path, isMain }) => {
63
+ const label = worktree.branch ?? (worktree.detached === true ? "detached" : basename(worktree.path) || worktree.path);
64
+ return {
65
+ key: path,
66
+ path,
67
+ label,
68
+ isMain,
69
+ data: {
70
+ worktreePath: worktree.path,
71
+ ...(worktree.branch === undefined ? {} : { branch: worktree.branch }),
72
+ },
73
+ publicMetadata: {
74
+ isGitRepo: true,
75
+ isGitWorktree: true,
76
+ ...(worktree.branch === undefined ? {} : { branch: worktree.branch }),
77
+ ...(worktree.detached === undefined ? {} : { detached: worktree.detached }),
78
+ },
79
+ ...(isMain ? {} : { removal: gitRemovalPresentation(label, path) }),
80
+ };
81
+ });
82
+ },
83
+ request: (request) => requestGitBackend(context, request),
84
+ async prepareRemove({ project, workspace, signal }) {
85
+ const privatePath = gitPrivateWorktreePath(workspace);
86
+ if (resolve(privatePath) !== workspace.path) {
87
+ throw new Error("Git workspace removal data no longer matches the current workspace path");
88
+ }
89
+ const listResult = await requireGit(runGit(context, project.path, ["worktree", "list", "--porcelain", "-z"], signal), "validate the Git worktree before removal");
90
+ const current = parseGitWorktreeList(listResult.stdout)
91
+ .find((worktree) => resolve(worktree.path) === workspace.path);
92
+ if (current === undefined || current.prunable === true) {
93
+ throw new Error("Git worktree is no longer available for removal");
94
+ }
95
+ if (current.bare === true)
96
+ throw new Error("A bare Git workspace cannot be removed as a linked worktree");
97
+ return {
98
+ title: `Delete workspace: ${workspace.label}`,
99
+ command: `git worktree remove ${shellQuote(workspace.path)}`,
100
+ };
101
+ },
102
+ });
103
+ }
104
+ /** Parse `git worktree list --porcelain -z` without path quoting or space loss. */
105
+ export function parseGitWorktreeList(stdout) {
106
+ return stdout.split("\0\0").flatMap((record) => {
107
+ if (record === "")
108
+ return [];
109
+ const info = { path: "" };
110
+ for (const field of record.split("\0")) {
111
+ const separator = field.indexOf(" ");
112
+ const key = separator === -1 ? field : field.slice(0, separator);
113
+ const value = separator === -1 ? "" : field.slice(separator + 1);
114
+ if (key === "worktree")
115
+ info.path = value;
116
+ else if (key === "branch")
117
+ info.branch = value.replace(/^refs\/heads\//u, "");
118
+ else if (key === "bare")
119
+ info.bare = true;
120
+ else if (key === "detached")
121
+ info.detached = true;
122
+ else if (key === "prunable")
123
+ info.prunable = true;
124
+ }
125
+ return info.path === "" ? [] : [info];
126
+ });
127
+ }
128
+ function singleGitWorkspace(project) {
129
+ return {
130
+ key: project.path,
131
+ path: project.path,
132
+ label: project.name,
133
+ isMain: true,
134
+ data: { worktreePath: project.path },
135
+ publicMetadata: { isGitRepo: true, isGitWorktree: false },
136
+ };
137
+ }
138
+ function gitRemovalPresentation(label, path) {
139
+ return {
140
+ actionLabel: "Delete workspace",
141
+ confirmation: `Delete workspace ${label}?\n\nThis will run git worktree remove and delete:\n${path}\n\nThe Git branch will not be deleted.`,
142
+ };
143
+ }
144
+ function gitPrivateWorktreePath(workspace) {
145
+ const data = workspace.data;
146
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
147
+ throw new Error("Git workspace removal data is unavailable");
148
+ }
149
+ const path = Reflect.get(data, "worktreePath");
150
+ if (typeof path !== "string" || path === "")
151
+ throw new Error("Git worktree path is unavailable for removal");
152
+ return path;
153
+ }
154
+ function shellQuote(value) {
155
+ return `'${value.replaceAll("'", "'\\''")}'`;
156
+ }
157
+ async function runGit(context, cwd, args, signal) {
158
+ const result = await context.execFile({
159
+ file: "git",
160
+ args: ["-C", cwd, ...args],
161
+ unsetEnv: GIT_LOCAL_ENV_VARS,
162
+ signal,
163
+ });
164
+ if (result.stdoutTruncated || result.stderrTruncated) {
165
+ throw new Error(`git ${args.join(" ")} exceeded the host output limit`);
166
+ }
167
+ return result;
168
+ }
169
+ async function requireGit(resultPromise, action) {
170
+ const result = await resultPromise;
171
+ if (result.signal === null && result.exitCode === 0)
172
+ return result;
173
+ const detail = result.stderr.trim();
174
+ const outcome = result.signal === null ? `exit ${String(result.exitCode)}` : `signal ${result.signal}`;
175
+ throw new Error(`Unable to ${action} (${outcome})${detail === "" ? "" : `: ${detail}`}`);
176
+ }
@@ -62,8 +62,7 @@ export function componentDetails(component) {
62
62
  }
63
63
  export function workspaceFlags(workspace) {
64
64
  return [
65
- workspace.branch === undefined || workspace.branch === "" ? undefined : `branch ${workspace.branch}`,
66
- workspace.isGitWorktree ? "git worktree" : workspace.isGitRepo ? "git repo" : "not a git repo",
65
+ workspace.provider === undefined ? "folder workspace" : `provider: ${workspace.provider.pluginId}`,
67
66
  workspace.isMain ? "main workspace" : undefined,
68
67
  ].filter((flag) => flag !== undefined);
69
68
  }
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "piWeb": {
5
5
  "plugins": [
6
- { "id": "info", "module": "pi-web-plugin.js" }
6
+ { "id": "info", "browserRoot": ".", "module": "pi-web-plugin.js" }
7
7
  ]
8
8
  }
9
9
  }
@@ -7,7 +7,7 @@
7
7
  // the internals for your own.
8
8
  import { copyDiagnostics, renderInfoPanel } from "./infoInternals.js";
9
9
  const plugin = {
10
- apiVersion: 1,
10
+ apiVersion: 2,
11
11
  name: "Info Plugin",
12
12
  activate: ({ html, svg }) => ({
13
13
  contributions: {
@@ -24,7 +24,7 @@ const plugin = {
24
24
  {
25
25
  id: "workspace.kind-label",
26
26
  order: 100,
27
- items: (context) => [{ type: "text", text: context.workspace.isGitRepo ? "git" : "folder", title: context.workspace.path }],
27
+ items: (context) => [{ type: "text", text: context.workspace.provider?.pluginId ?? "folder", title: context.workspace.path }],
28
28
  },
29
29
  ],
30
30
  workspacePanels: [
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "piWeb": {
5
5
  "plugins": [
6
- { "id": "relays", "module": "pi-web-plugin.js" }
6
+ { "id": "relays", "browserRoot": ".", "module": "pi-web-plugin.js" }
7
7
  ]
8
8
  }
9
9
  }
@@ -2,9 +2,9 @@
2
2
  import { RELAYS_ROOT } from "./relayDiscovery.js";
3
3
  import { defineRelaysPanelElement } from "./relaysPanelElement.js";
4
4
  const plugin = {
5
- apiVersion: 1,
5
+ apiVersion: 2,
6
6
  name: "Relays",
7
- activate: ({ pluginId, html, svg }) => {
7
+ activate: ({ runtimePluginId, html, svg }) => {
8
8
  defineRelaysPanelElement();
9
9
  return {
10
10
  contributions: {
@@ -18,7 +18,7 @@ const plugin = {
18
18
  run: (context) => {
19
19
  if (context.state.selectedWorkspace === undefined)
20
20
  return;
21
- context.selectWorkspaceTool(`${pluginId}:workspace.relays`);
21
+ context.selectWorkspaceTool(`${runtimePluginId}:workspace.relays`);
22
22
  },
23
23
  },
24
24
  ],