toolhome 0.5.0

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/README.zh.md +281 -0
  4. package/dist/server/app.d.ts +16 -0
  5. package/dist/server/app.d.ts.map +1 -0
  6. package/dist/server/app.js +166 -0
  7. package/dist/server/app.js.map +1 -0
  8. package/dist/server/cli/main.d.ts +3 -0
  9. package/dist/server/cli/main.d.ts.map +1 -0
  10. package/dist/server/cli/main.js +587 -0
  11. package/dist/server/cli/main.js.map +1 -0
  12. package/dist/server/cli-plane/allow-list.d.ts +18 -0
  13. package/dist/server/cli-plane/allow-list.d.ts.map +1 -0
  14. package/dist/server/cli-plane/allow-list.js +28 -0
  15. package/dist/server/cli-plane/allow-list.js.map +1 -0
  16. package/dist/server/cli-plane/cli-service.d.ts +41 -0
  17. package/dist/server/cli-plane/cli-service.d.ts.map +1 -0
  18. package/dist/server/cli-plane/cli-service.js +282 -0
  19. package/dist/server/cli-plane/cli-service.js.map +1 -0
  20. package/dist/server/cli-plane/frames.d.ts +26 -0
  21. package/dist/server/cli-plane/frames.d.ts.map +1 -0
  22. package/dist/server/cli-plane/frames.js +12 -0
  23. package/dist/server/cli-plane/frames.js.map +1 -0
  24. package/dist/server/cli-plane/models.d.ts +174 -0
  25. package/dist/server/cli-plane/models.d.ts.map +1 -0
  26. package/dist/server/cli-plane/models.js +169 -0
  27. package/dist/server/cli-plane/models.js.map +1 -0
  28. package/dist/server/cli-plane/routes.d.ts +32 -0
  29. package/dist/server/cli-plane/routes.d.ts.map +1 -0
  30. package/dist/server/cli-plane/routes.js +94 -0
  31. package/dist/server/cli-plane/routes.js.map +1 -0
  32. package/dist/server/cli-plane/runner.d.ts +41 -0
  33. package/dist/server/cli-plane/runner.d.ts.map +1 -0
  34. package/dist/server/cli-plane/runner.js +163 -0
  35. package/dist/server/cli-plane/runner.js.map +1 -0
  36. package/dist/server/cli-plane/status.d.ts +12 -0
  37. package/dist/server/cli-plane/status.d.ts.map +1 -0
  38. package/dist/server/cli-plane/status.js +24 -0
  39. package/dist/server/cli-plane/status.js.map +1 -0
  40. package/dist/server/config/harness-import.d.ts +44 -0
  41. package/dist/server/config/harness-import.d.ts.map +1 -0
  42. package/dist/server/config/harness-import.js +166 -0
  43. package/dist/server/config/harness-import.js.map +1 -0
  44. package/dist/server/config.d.ts +20 -0
  45. package/dist/server/config.d.ts.map +1 -0
  46. package/dist/server/config.js +80 -0
  47. package/dist/server/config.js.map +1 -0
  48. package/dist/server/control/client.d.ts +24 -0
  49. package/dist/server/control/client.d.ts.map +1 -0
  50. package/dist/server/control/client.js +115 -0
  51. package/dist/server/control/client.js.map +1 -0
  52. package/dist/server/control/control-api.d.ts +39 -0
  53. package/dist/server/control/control-api.d.ts.map +1 -0
  54. package/dist/server/control/control-api.js +291 -0
  55. package/dist/server/control/control-api.js.map +1 -0
  56. package/dist/server/control/control-service.d.ts +713 -0
  57. package/dist/server/control/control-service.d.ts.map +1 -0
  58. package/dist/server/control/control-service.js +693 -0
  59. package/dist/server/control/control-service.js.map +1 -0
  60. package/dist/server/control/openapi.d.ts +2 -0
  61. package/dist/server/control/openapi.d.ts.map +1 -0
  62. package/dist/server/control/openapi.js +370 -0
  63. package/dist/server/control/openapi.js.map +1 -0
  64. package/dist/server/data-plane/data-plane.d.ts +20 -0
  65. package/dist/server/data-plane/data-plane.d.ts.map +1 -0
  66. package/dist/server/data-plane/data-plane.js +335 -0
  67. package/dist/server/data-plane/data-plane.js.map +1 -0
  68. package/dist/server/data-plane/gateway-server.d.ts +13 -0
  69. package/dist/server/data-plane/gateway-server.d.ts.map +1 -0
  70. package/dist/server/data-plane/gateway-server.js +796 -0
  71. package/dist/server/data-plane/gateway-server.js.map +1 -0
  72. package/dist/server/data-plane/legacy-session.d.ts +18 -0
  73. package/dist/server/data-plane/legacy-session.d.ts.map +1 -0
  74. package/dist/server/data-plane/legacy-session.js +137 -0
  75. package/dist/server/data-plane/legacy-session.js.map +1 -0
  76. package/dist/server/data-plane/projection.d.ts +18 -0
  77. package/dist/server/data-plane/projection.d.ts.map +1 -0
  78. package/dist/server/data-plane/projection.js +44 -0
  79. package/dist/server/data-plane/projection.js.map +1 -0
  80. package/dist/server/data-plane/registry.d.ts +31 -0
  81. package/dist/server/data-plane/registry.d.ts.map +1 -0
  82. package/dist/server/data-plane/registry.js +129 -0
  83. package/dist/server/data-plane/registry.js.map +1 -0
  84. package/dist/server/data-plane/task-extension.d.ts +7 -0
  85. package/dist/server/data-plane/task-extension.d.ts.map +1 -0
  86. package/dist/server/data-plane/task-extension.js +33 -0
  87. package/dist/server/data-plane/task-extension.js.map +1 -0
  88. package/dist/server/data-plane/virtualization.d.ts +35 -0
  89. package/dist/server/data-plane/virtualization.d.ts.map +1 -0
  90. package/dist/server/data-plane/virtualization.js +228 -0
  91. package/dist/server/data-plane/virtualization.js.map +1 -0
  92. package/dist/server/domain/errors.d.ts +9 -0
  93. package/dist/server/domain/errors.d.ts.map +1 -0
  94. package/dist/server/domain/errors.js +19 -0
  95. package/dist/server/domain/errors.js.map +1 -0
  96. package/dist/server/domain/models.d.ts +720 -0
  97. package/dist/server/domain/models.d.ts.map +1 -0
  98. package/dist/server/domain/models.js +327 -0
  99. package/dist/server/domain/models.js.map +1 -0
  100. package/dist/server/main.d.ts +2 -0
  101. package/dist/server/main.d.ts.map +1 -0
  102. package/dist/server/main.js +37 -0
  103. package/dist/server/main.js.map +1 -0
  104. package/dist/server/manage/management-mcp.d.ts +27 -0
  105. package/dist/server/manage/management-mcp.d.ts.map +1 -0
  106. package/dist/server/manage/management-mcp.js +431 -0
  107. package/dist/server/manage/management-mcp.js.map +1 -0
  108. package/dist/server/market/catalog.d.ts +111 -0
  109. package/dist/server/market/catalog.d.ts.map +1 -0
  110. package/dist/server/market/catalog.js +784 -0
  111. package/dist/server/market/catalog.js.map +1 -0
  112. package/dist/server/market/installers.d.ts +64 -0
  113. package/dist/server/market/installers.d.ts.map +1 -0
  114. package/dist/server/market/installers.js +181 -0
  115. package/dist/server/market/installers.js.map +1 -0
  116. package/dist/server/market/market-service.d.ts +146 -0
  117. package/dist/server/market/market-service.d.ts.map +1 -0
  118. package/dist/server/market/market-service.js +930 -0
  119. package/dist/server/market/market-service.js.map +1 -0
  120. package/dist/server/observability/call-recorder.d.ts +22 -0
  121. package/dist/server/observability/call-recorder.d.ts.map +1 -0
  122. package/dist/server/observability/call-recorder.js +99 -0
  123. package/dist/server/observability/call-recorder.js.map +1 -0
  124. package/dist/server/observability/logger.d.ts +16 -0
  125. package/dist/server/observability/logger.d.ts.map +1 -0
  126. package/dist/server/observability/logger.js +25 -0
  127. package/dist/server/observability/logger.js.map +1 -0
  128. package/dist/server/security/api-keys.d.ts +15 -0
  129. package/dist/server/security/api-keys.d.ts.map +1 -0
  130. package/dist/server/security/api-keys.js +35 -0
  131. package/dist/server/security/api-keys.js.map +1 -0
  132. package/dist/server/security/auth-service.d.ts +26 -0
  133. package/dist/server/security/auth-service.d.ts.map +1 -0
  134. package/dist/server/security/auth-service.js +76 -0
  135. package/dist/server/security/auth-service.js.map +1 -0
  136. package/dist/server/security/control-session.d.ts +9 -0
  137. package/dist/server/security/control-session.d.ts.map +1 -0
  138. package/dist/server/security/control-session.js +51 -0
  139. package/dist/server/security/control-session.js.map +1 -0
  140. package/dist/server/security/credential-materializer.d.ts +20 -0
  141. package/dist/server/security/credential-materializer.d.ts.map +1 -0
  142. package/dist/server/security/credential-materializer.js +82 -0
  143. package/dist/server/security/credential-materializer.js.map +1 -0
  144. package/dist/server/security/cursor-codec.d.ts +13 -0
  145. package/dist/server/security/cursor-codec.d.ts.map +1 -0
  146. package/dist/server/security/cursor-codec.js +43 -0
  147. package/dist/server/security/cursor-codec.js.map +1 -0
  148. package/dist/server/security/oauth-routes.d.ts +21 -0
  149. package/dist/server/security/oauth-routes.d.ts.map +1 -0
  150. package/dist/server/security/oauth-routes.js +100 -0
  151. package/dist/server/security/oauth-routes.js.map +1 -0
  152. package/dist/server/security/oauth-server.d.ts +16 -0
  153. package/dist/server/security/oauth-server.d.ts.map +1 -0
  154. package/dist/server/security/oauth-server.js +544 -0
  155. package/dist/server/security/oauth-server.js.map +1 -0
  156. package/dist/server/security/secret-box.d.ts +7 -0
  157. package/dist/server/security/secret-box.d.ts.map +1 -0
  158. package/dist/server/security/secret-box.js +27 -0
  159. package/dist/server/security/secret-box.js.map +1 -0
  160. package/dist/server/security/secure-action.d.ts +21 -0
  161. package/dist/server/security/secure-action.d.ts.map +1 -0
  162. package/dist/server/security/secure-action.js +69 -0
  163. package/dist/server/security/secure-action.js.map +1 -0
  164. package/dist/server/storage/sqlite-store.d.ts +70 -0
  165. package/dist/server/storage/sqlite-store.d.ts.map +1 -0
  166. package/dist/server/storage/sqlite-store.js +1522 -0
  167. package/dist/server/storage/sqlite-store.js.map +1 -0
  168. package/dist/server/storage/store.d.ts +82 -0
  169. package/dist/server/storage/store.d.ts.map +1 -0
  170. package/dist/server/storage/store.js +2 -0
  171. package/dist/server/storage/store.js.map +1 -0
  172. package/dist/server/upstream/adapter.d.ts +52 -0
  173. package/dist/server/upstream/adapter.d.ts.map +1 -0
  174. package/dist/server/upstream/adapter.js +807 -0
  175. package/dist/server/upstream/adapter.js.map +1 -0
  176. package/dist/server/upstream/credential-resolver.d.ts +14 -0
  177. package/dist/server/upstream/credential-resolver.d.ts.map +1 -0
  178. package/dist/server/upstream/credential-resolver.js +56 -0
  179. package/dist/server/upstream/credential-resolver.js.map +1 -0
  180. package/dist/server/upstream/extension-transport.d.ts +17 -0
  181. package/dist/server/upstream/extension-transport.d.ts.map +1 -0
  182. package/dist/server/upstream/extension-transport.js +207 -0
  183. package/dist/server/upstream/extension-transport.js.map +1 -0
  184. package/dist/server/upstream/manager.d.ts +23 -0
  185. package/dist/server/upstream/manager.d.ts.map +1 -0
  186. package/dist/server/upstream/manager.js +324 -0
  187. package/dist/server/upstream/manager.js.map +1 -0
  188. package/dist/server/upstream/oauth-provider.d.ts +38 -0
  189. package/dist/server/upstream/oauth-provider.d.ts.map +1 -0
  190. package/dist/server/upstream/oauth-provider.js +225 -0
  191. package/dist/server/upstream/oauth-provider.js.map +1 -0
  192. package/dist/server/upstream/oauth-refresh.d.ts +65 -0
  193. package/dist/server/upstream/oauth-refresh.d.ts.map +1 -0
  194. package/dist/server/upstream/oauth-refresh.js +240 -0
  195. package/dist/server/upstream/oauth-refresh.js.map +1 -0
  196. package/dist/server/upstream/oauth-routes.d.ts +17 -0
  197. package/dist/server/upstream/oauth-routes.d.ts.map +1 -0
  198. package/dist/server/upstream/oauth-routes.js +87 -0
  199. package/dist/server/upstream/oauth-routes.js.map +1 -0
  200. package/dist/server/upstream/oauth-service.d.ts +143 -0
  201. package/dist/server/upstream/oauth-service.d.ts.map +1 -0
  202. package/dist/server/upstream/oauth-service.js +217 -0
  203. package/dist/server/upstream/oauth-service.js.map +1 -0
  204. package/dist/server/upstream/stable-json.d.ts +3 -0
  205. package/dist/server/upstream/stable-json.d.ts.map +1 -0
  206. package/dist/server/upstream/stable-json.js +17 -0
  207. package/dist/server/upstream/stable-json.js.map +1 -0
  208. package/package.json +69 -0
  209. package/skills/toolhome/SKILL.md +238 -0
  210. package/skills/toolhome/references/cli-reference.md +242 -0
  211. package/skills/toolhome/references/deployment.md +113 -0
  212. package/skills/toolhome/references/market-guide.md +128 -0
  213. package/skills/toolhome/references/oauth-guide.md +96 -0
  214. package/skills/toolhome/references/troubleshooting.md +133 -0
@@ -0,0 +1,930 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+ import { installArtifact, installerForEntry, } from './installers.js';
5
+ import { marketCatalog, entryPlane } from './catalog.js';
6
+ import { AppError } from '../domain/errors.js';
7
+ import { validateCliCredentialBindings } from '../security/credential-materializer.js';
8
+ import { fingerprint } from '../upstream/stable-json.js';
9
+ export class MarketService {
10
+ #service;
11
+ #store;
12
+ #actions;
13
+ #marketDir;
14
+ #uvEnv;
15
+ #jobs = new Map();
16
+ #activeTasks = new Set();
17
+ #installerProcesses = new Set();
18
+ #closing = false;
19
+ /** One in-flight mutating operation per catalog entry in this process. */
20
+ #entryOperations = new Map();
21
+ constructor(service, store, actions, marketDir, dataDir, uvIndexUrl) {
22
+ this.#service = service;
23
+ this.#store = store;
24
+ this.#actions = actions;
25
+ this.#marketDir = marketDir;
26
+ const uvRoot = dataDir === undefined ? marketDir : join(dataDir, '.uv');
27
+ this.#uvEnv = {
28
+ UV_CACHE_DIR: join(uvRoot, 'cache'),
29
+ UV_TOOL_DIR: join(uvRoot, 'tools'),
30
+ UV_TOOL_BIN_DIR: join(uvRoot, 'tools', 'bin'),
31
+ UV_COMPILE_BYTECODE: '0',
32
+ ...(uvIndexUrl === undefined ? {} : { UV_DEFAULT_INDEX: uvIndexUrl }),
33
+ };
34
+ // A fresh process never carries in-flight installs; surface them as
35
+ // interrupted so they are visible and retryable instead of silently lost.
36
+ this.#store.markInterruptedInstallJobs();
37
+ }
38
+ list() {
39
+ const servers = this.#service.listServers();
40
+ return marketCatalog.map((entry) => {
41
+ const installation = this.#store.getInstallation(entry.id);
42
+ const plane = entryPlane(entry);
43
+ const installed = plane === 'cli'
44
+ ? this.#store.getCliBySlug(entry.id) !== null
45
+ : servers.some((server) => server.slug === entry.id);
46
+ return {
47
+ ...entry,
48
+ plane,
49
+ installed,
50
+ installedVersion: installation?.entryVersion ?? null,
51
+ updateAvailable: installation !== null &&
52
+ ((entry.version ?? 'unpinned') !== installation.entryVersion ||
53
+ fingerprint(entry) !== installation.recipeRevision),
54
+ };
55
+ });
56
+ }
57
+ /**
58
+ * Per-entry version drift: installed version vs the catalog pin (deterministic,
59
+ * no network) plus a best-effort upstream lookup (registry JSON, short timeout).
60
+ */
61
+ async updates() {
62
+ const entries = marketCatalog
63
+ .map((entry) => ({ entry, installation: this.#store.getInstallation(entry.id) }))
64
+ .filter((item) => item.installation !== null);
65
+ return Promise.all(entries.map(async ({ entry, installation }) => {
66
+ const catalogVersion = entry.version ?? 'unpinned';
67
+ return {
68
+ entryId: entry.id,
69
+ installedVersion: installation.entryVersion,
70
+ catalogVersion,
71
+ updateAvailable: catalogVersion !== installation.entryVersion ||
72
+ fingerprint(entry) !== installation.recipeRevision,
73
+ latestUpstream: await this.#latestUpstream(entry),
74
+ };
75
+ }));
76
+ }
77
+ /**
78
+ * Explicit update to the catalog's current pin. Reinstalls the package, bumps
79
+ * the installation record, and restarts the server — the credential and
80
+ * visibility configuration survive. On failure the old process keeps running.
81
+ */
82
+ async update(id) {
83
+ const entry = this.#entry(id);
84
+ this.#assertEntryAvailable(entry.id);
85
+ const installation = this.#store.getInstallation(entry.id);
86
+ if (!installation) {
87
+ throw new AppError('market_not_installed', `Market entry "${id}" is not installed`, 404);
88
+ }
89
+ const catalogVersion = entry.version ?? 'unpinned';
90
+ const recipeRevision = fingerprint(entry);
91
+ if (installation.entryVersion === catalogVersion &&
92
+ installation.recipeRevision === recipeRevision) {
93
+ return { jobId: null, status: 'up_to_date' };
94
+ }
95
+ const job = this.#createJob(entry, entry.version ?? null, 'updating');
96
+ this.#claimEntry(entry.id, job.record.id);
97
+ try {
98
+ this.#update(job, { step: 'starting update' });
99
+ this.#trackTask(this.#runUpdate(entry, installation, job));
100
+ return { jobId: job.record.id, status: 'updating' };
101
+ }
102
+ catch (error) {
103
+ this.#releaseEntry(entry.id, job.record.id);
104
+ throw error;
105
+ }
106
+ }
107
+ installations() {
108
+ return this.#store.listInstallations();
109
+ }
110
+ async close() {
111
+ this.#closing = true;
112
+ for (const child of this.#installerProcesses) {
113
+ this.#killInstallerProcess(child, 'SIGTERM');
114
+ }
115
+ await Promise.allSettled([...this.#activeTasks]);
116
+ }
117
+ getJob(jobId) {
118
+ const live = this.#jobs.get(jobId);
119
+ if (live) {
120
+ if (live.record.status === 'awaiting_secret') {
121
+ return this.#expireIfActionStale(live.record);
122
+ }
123
+ return this.#view(live);
124
+ }
125
+ const record = this.#store.getInstallJob(jobId);
126
+ if (!record)
127
+ throw new AppError('market_job_not_found', 'Install job not found', 404);
128
+ if (record.status === 'awaiting_secret')
129
+ return this.#expireIfActionStale(record);
130
+ return {
131
+ id: record.id,
132
+ entryId: record.entryId,
133
+ status: record.status,
134
+ step: record.step,
135
+ output: record.boundedOutput,
136
+ error: record.errorCode ?? undefined,
137
+ };
138
+ }
139
+ async install(id, values = {}, principalId = 'cli') {
140
+ const entry = this.#entry(id);
141
+ this.#assertEntryAvailable(entry.id);
142
+ const existing = this.#store.getInstallation(entry.id);
143
+ if (existing) {
144
+ const target = existing.targetType === 'server'
145
+ ? this.#store.getServer(existing.targetId)
146
+ : this.#store.getCli(existing.targetId);
147
+ if (target !== null) {
148
+ return {
149
+ jobId: null,
150
+ status: 'already_installed',
151
+ installed: {
152
+ entryId: entry.id,
153
+ version: existing.entryVersion,
154
+ targetType: existing.targetType,
155
+ targetId: existing.targetId,
156
+ slug: target.slug,
157
+ },
158
+ };
159
+ }
160
+ // A prior uninstall may have removed the target after its marker write.
161
+ // Remove the stale marker so a retry can provision a clean target.
162
+ for (const installation of this.#store
163
+ .listInstallations()
164
+ .filter((item) => item.entryId === entry.id)) {
165
+ this.#store.deleteInstallation(installation.id);
166
+ }
167
+ }
168
+ for (const requirement of entry.requires) {
169
+ if (requirement.required && !requirement.secret && !values[requirement.name]) {
170
+ throw new AppError('market_missing_value', `Missing required value ${requirement.name}`, 400);
171
+ }
172
+ }
173
+ const missingSecrets = entry.requires.filter((requirement) => requirement.required && requirement.secret && !values[requirement.name]);
174
+ const requestedVersion = entry.version ?? null;
175
+ const job = this.#createJob(entry, requestedVersion, missingSecrets.length > 0 ? 'awaiting_secret' : 'installing');
176
+ this.#claimEntry(entry.id, job.record.id);
177
+ try {
178
+ if (missingSecrets.length > 0) {
179
+ // URL-mode elicitation: never accept the secret through tool arguments.
180
+ const { action, url } = this.#actions.create('market_install', job.record.id, principalId);
181
+ this.#updateRecord(job, { actionId: action.id });
182
+ return {
183
+ jobId: job.record.id,
184
+ status: 'awaiting_secret',
185
+ actionId: action.id,
186
+ actionUrl: url,
187
+ };
188
+ }
189
+ this.#startInstall(entry, values, job);
190
+ return { jobId: job.record.id, status: 'installing' };
191
+ }
192
+ catch (error) {
193
+ this.#releaseEntry(entry.id, job.record.id);
194
+ throw error;
195
+ }
196
+ }
197
+ #startInstall(entry, values, job) {
198
+ this.#update(job, { step: 'starting' });
199
+ const task = entryPlane(entry) === 'cli'
200
+ ? this.#runCliInstall(entry, values, job)
201
+ : this.#runInstall(entry, values, job);
202
+ this.#trackTask(task);
203
+ }
204
+ #trackTask(task) {
205
+ this.#activeTasks.add(task);
206
+ void task.then(() => this.#activeTasks.delete(task), () => this.#activeTasks.delete(task));
207
+ }
208
+ /** Completes a URL-mode secret action and resumes the linked install job. */
209
+ async completeAction(actionId, token, principalId, values) {
210
+ const actionRecord = this.#store.getSecureAction(actionId);
211
+ if (actionRecord === null) {
212
+ throw new AppError('secure_action_not_found', 'Secure action not found', 404);
213
+ }
214
+ let pending;
215
+ try {
216
+ pending = this.#actions.verify(actionId, token, principalId);
217
+ }
218
+ catch (error) {
219
+ if (actionRecord.status === 'pending' && Date.parse(actionRecord.expiresAt) <= Date.now()) {
220
+ const expiredJob = this.#store.getInstallJob(actionRecord.target);
221
+ if (expiredJob)
222
+ this.#releaseEntry(expiredJob.entryId, expiredJob.id);
223
+ }
224
+ throw error;
225
+ }
226
+ const pendingJob = this.#store.getInstallJob(pending.target);
227
+ if (!pendingJob || pendingJob.status !== 'awaiting_secret') {
228
+ if (pendingJob)
229
+ this.#releaseEntry(pendingJob.entryId, pendingJob.id);
230
+ throw new AppError('market_job_not_found', 'Install job not found or not awaiting a secret', 404);
231
+ }
232
+ const entry = this.#entry(pendingJob.entryId);
233
+ const heldBy = this.#entryOperations.get(entry.id);
234
+ if (heldBy !== undefined && heldBy.id !== pendingJob.id) {
235
+ throw new AppError('market_operation_in_progress', `Market entry "${entry.id}" already has an operation in progress`, 409);
236
+ }
237
+ const claimed = heldBy === undefined;
238
+ if (claimed)
239
+ this.#claimEntry(entry.id, pendingJob.id);
240
+ try {
241
+ const completed = this.#store.transaction(() => {
242
+ // Consume the action and transition its job together. If the job is no
243
+ // longer resumable, the transaction rolls back and the URL remains usable.
244
+ const action = this.#actions.complete(actionId, token, principalId, values);
245
+ const record = this.#store.getInstallJob(action.target);
246
+ if (!record || record.status !== 'awaiting_secret') {
247
+ throw new AppError('market_job_not_found', 'Install job not found or not awaiting a secret', 404);
248
+ }
249
+ const job = this.#store.updateInstallJob(record.id, {
250
+ status: 'installing',
251
+ step: 'starting',
252
+ });
253
+ return { action, job };
254
+ });
255
+ const live = this.#jobs.get(completed.action.target) ?? {
256
+ record: completed.job,
257
+ output: completed.job.boundedOutput,
258
+ };
259
+ live.record = completed.job;
260
+ this.#jobs.set(completed.action.target, live);
261
+ this.#startInstall(entry, values, live);
262
+ return this.#view(live);
263
+ }
264
+ catch (error) {
265
+ if (claimed)
266
+ this.#releaseEntry(entry.id, pendingJob.id);
267
+ throw error;
268
+ }
269
+ }
270
+ /** Reads a pending action's required secret fields after validating its URL token. */
271
+ secureActionInfo(actionId, token, principalId) {
272
+ if (token === undefined || token === '') {
273
+ throw new AppError('secure_action_invalid', 'Secure action token is required', 400);
274
+ }
275
+ const action = this.#actions.verify(actionId, token, principalId);
276
+ const job = this.#store.getInstallJob(action.target);
277
+ const entry = job ? marketCatalog.find((item) => item.id === job.entryId) : undefined;
278
+ return {
279
+ actionId: action.id,
280
+ status: action.status,
281
+ entryId: entry?.id ?? null,
282
+ entryName: entry?.name ?? null,
283
+ fields: (entry?.requires ?? [])
284
+ .filter((requirement) => requirement.secret)
285
+ .map((requirement) => ({
286
+ name: requirement.name,
287
+ description: requirement.description,
288
+ })),
289
+ };
290
+ }
291
+ async uninstall(id) {
292
+ const entry = this.#entry(id);
293
+ const operationId = `uninstall:${randomUUID()}`;
294
+ this.#assertEntryAvailable(entry.id);
295
+ this.#entryOperations.set(entry.id, { kind: 'uninstall', id: operationId });
296
+ try {
297
+ if (entryPlane(entry) === 'cli') {
298
+ const cli = this.#store.getCliBySlug(entry.id);
299
+ if (!cli) {
300
+ throw new AppError('market_not_installed', `Market entry "${id}" is not installed`, 404);
301
+ }
302
+ this.#store.transaction(() => {
303
+ if (cli.credentialId)
304
+ this.#store.deleteCredential(cli.credentialId);
305
+ this.#store.deleteCli(cli.id);
306
+ for (const installation of this.#store
307
+ .listInstallations()
308
+ .filter((item) => item.entryId === entry.id)) {
309
+ this.#store.deleteInstallation(installation.id);
310
+ }
311
+ });
312
+ }
313
+ else {
314
+ const servers = this.#service.listServers().filter((server) => server.slug === entry.id);
315
+ if (servers.length === 0) {
316
+ throw new AppError('market_not_installed', `Market entry "${id}" is not installed`, 404);
317
+ }
318
+ const installations = this.#store
319
+ .listInstallations()
320
+ .filter((item) => item.entryId === entry.id);
321
+ // Remove the marker before touching runtime-backed resources. If marker
322
+ // cleanup fails, the target and its credential remain untouched.
323
+ this.#store.transaction(() => {
324
+ for (const installation of installations) {
325
+ this.#store.deleteInstallation(installation.id);
326
+ }
327
+ });
328
+ try {
329
+ for (const server of servers) {
330
+ if (server.credentialId)
331
+ await this.#service.deleteCredential(server.credentialId);
332
+ await this.#service.deleteServer(server.id);
333
+ }
334
+ }
335
+ catch (error) {
336
+ // Restore the marker when an upstream/runtime deletion fails so the
337
+ // next uninstall can retry the same installation.
338
+ try {
339
+ this.#store.transaction(() => {
340
+ for (const installation of installations) {
341
+ this.#store.createInstallation({
342
+ source: installation.source,
343
+ entryId: installation.entryId,
344
+ entryVersion: installation.entryVersion,
345
+ recipeRevision: installation.recipeRevision,
346
+ targetType: installation.targetType,
347
+ targetId: installation.targetId,
348
+ credentialId: installation.credentialId,
349
+ });
350
+ }
351
+ });
352
+ }
353
+ catch {
354
+ // Preserve the original deletion error if compensation also fails.
355
+ }
356
+ throw error;
357
+ }
358
+ }
359
+ }
360
+ finally {
361
+ this.#releaseEntry(entry.id, operationId);
362
+ }
363
+ }
364
+ // ── internals ───────────────────────────────────────────────────────────
365
+ /**
366
+ * CLI-plane install: provision the credential and CLI record, verifying or
367
+ * fetching an installer-backed artifact before writing the installation marker.
368
+ */
369
+ async #runCliInstall(entry, values, job) {
370
+ let credential;
371
+ let cli;
372
+ let installation;
373
+ try {
374
+ this.#update(job, { step: 'creating credential' });
375
+ credential = this.#service.createCredential({
376
+ name: entry.name,
377
+ payload: this.#credentialPayload(entry, values),
378
+ });
379
+ this.#update(job, { step: 'installing CLI artifact' });
380
+ const cliArtifact = installerForEntry(entry);
381
+ const installedRuntime = cliArtifact === null
382
+ ? null
383
+ : await installArtifact(cliArtifact, this.#installerContext(job));
384
+ this.#update(job, { step: 'creating CLI record' });
385
+ const projection = this.#cliProjection(entry, installedRuntime);
386
+ const provisioned = this.#store.transaction(() => {
387
+ const createdCli = this.#store.createCli({
388
+ slug: entry.id,
389
+ name: entry.name,
390
+ command: projection.command,
391
+ executionMode: projection.executionMode,
392
+ entrypoint: projection.entrypoint,
393
+ authStrategy: projection.authStrategy,
394
+ containerVolumes: projection.containerVolumes,
395
+ allowList: projection.allowList,
396
+ interactive: false,
397
+ credentialId: credential.id,
398
+ credentialBindings: entry.credentialBindings ?? {},
399
+ platform: entry.platform ?? null,
400
+ probe: entry.probe ?? { command: projection.command, args: ['--version'] },
401
+ enabled: true,
402
+ timeoutMs: entry.execTimeoutMs ?? 120_000,
403
+ maxOutputBytes: 64 * 1024,
404
+ });
405
+ const createdInstallation = this.#store.createInstallation({
406
+ source: 'curated',
407
+ entryId: entry.id,
408
+ entryVersion: entry.version ?? 'unpinned',
409
+ recipeRevision: fingerprint(entry),
410
+ targetType: 'cli',
411
+ targetId: createdCli.id,
412
+ credentialId: credential.id,
413
+ });
414
+ return { cli: createdCli, installation: createdInstallation };
415
+ });
416
+ cli = provisioned.cli;
417
+ installation = provisioned.installation;
418
+ this.#update(job, {
419
+ step: 'done',
420
+ result: {
421
+ entryId: entry.id,
422
+ cliId: cli.id,
423
+ slug: cli.slug,
424
+ credential,
425
+ installation,
426
+ },
427
+ });
428
+ this.#updateRecord(job, { status: 'completed', resultReference: installation.id });
429
+ }
430
+ catch (error) {
431
+ try {
432
+ if (installation !== undefined)
433
+ this.#store.deleteInstallation(installation.id);
434
+ if (cli !== undefined)
435
+ this.#store.deleteCli(cli.id);
436
+ if (credential !== undefined)
437
+ await this.#service.deleteCredential(credential.id);
438
+ }
439
+ catch {
440
+ // Preserve the original install error if cleanup also fails.
441
+ }
442
+ try {
443
+ this.#update(job, {
444
+ step: 'failed',
445
+ error: error instanceof Error ? error.message : String(error),
446
+ });
447
+ this.#updateRecord(job, {
448
+ status: 'failed',
449
+ errorCode: error instanceof AppError ? error.code : 'market_install_failed',
450
+ });
451
+ }
452
+ catch {
453
+ // The runtime may have been torn down mid-install; nothing to persist.
454
+ }
455
+ }
456
+ finally {
457
+ this.#releaseEntry(entry.id, job.record.id);
458
+ }
459
+ }
460
+ #createJob(entry, requestedVersion, status) {
461
+ const record = this.#store.createInstallJob({
462
+ entryId: entry.id,
463
+ requestedVersion,
464
+ // Unique per attempt: a failed/interrupted job keeps its row (audit),
465
+ // and retrying must not collide on the UNIQUE constraint. Post-success
466
+ // duplicates are already rejected by the already-installed check.
467
+ idempotencyKey: `${entry.id}:${requestedVersion ?? 'latest'}:${randomUUID()}`,
468
+ status,
469
+ step: status,
470
+ boundedOutput: '',
471
+ resultReference: null,
472
+ actionId: null,
473
+ errorCode: null,
474
+ });
475
+ const live = { record, output: '' };
476
+ this.#jobs.set(record.id, live);
477
+ return live;
478
+ }
479
+ #view(live) {
480
+ return {
481
+ id: live.record.id,
482
+ entryId: live.record.entryId,
483
+ status: live.record.status,
484
+ step: live.record.step,
485
+ output: live.output || live.record.boundedOutput,
486
+ ...(live.result === undefined ? {} : { result: live.result }),
487
+ ...(live.error === undefined ? {} : { error: live.error }),
488
+ };
489
+ }
490
+ /**
491
+ * A waiting job whose secret link has passed its TTL can never resume. Fail it
492
+ * on read so pollers observe a terminal state, and release the entry claim so
493
+ * a retry can start; the action itself stays pending until someone consumes
494
+ * or the store prunes it.
495
+ */
496
+ #expireIfActionStale(record) {
497
+ const action = record.actionId === null ? null : this.#store.getSecureAction(record.actionId);
498
+ if (action !== null &&
499
+ action.status === 'pending' &&
500
+ Date.parse(action.expiresAt) <= Date.now()) {
501
+ this.#releaseEntry(record.entryId, record.id);
502
+ const failed = this.#store.updateInstallJob(record.id, {
503
+ status: 'failed',
504
+ step: 'secure action expired',
505
+ errorCode: 'secure_action_expired',
506
+ });
507
+ return {
508
+ id: failed.id,
509
+ entryId: failed.entryId,
510
+ status: failed.status,
511
+ step: failed.step,
512
+ output: failed.boundedOutput,
513
+ error: failed.errorCode ?? undefined,
514
+ };
515
+ }
516
+ return {
517
+ id: record.id,
518
+ entryId: record.entryId,
519
+ status: record.status,
520
+ step: record.step,
521
+ output: record.boundedOutput,
522
+ error: record.errorCode ?? undefined,
523
+ };
524
+ }
525
+ #update(job, patch) {
526
+ if (this.#closing)
527
+ return;
528
+ if (patch.step !== undefined) {
529
+ this.#updateRecord(job, { step: patch.step });
530
+ }
531
+ if (patch.output !== undefined) {
532
+ job.output = patch.output;
533
+ this.#updateRecord(job, { boundedOutput: patch.output });
534
+ }
535
+ if (patch.result !== undefined)
536
+ job.result = patch.result;
537
+ if (patch.error !== undefined)
538
+ job.error = patch.error;
539
+ }
540
+ #updateRecord(job, patch) {
541
+ if (this.#closing)
542
+ return;
543
+ job.record = this.#store.updateInstallJob(job.record.id, patch);
544
+ }
545
+ async #runInstall(entry, values, job) {
546
+ let credential;
547
+ let server;
548
+ let installation;
549
+ try {
550
+ const artifact = installerForEntry(entry);
551
+ const installedRuntime = artifact === null ? null : await installArtifact(artifact, this.#installerContext(job));
552
+ this.#update(job, { step: 'creating credential' });
553
+ credential = this.#service.createCredential({
554
+ name: entry.name,
555
+ payload: this.#credentialPayload(entry, values),
556
+ });
557
+ this.#update(job, { step: 'creating server' });
558
+ const transport = this.#serverTransport(entry, installedRuntime, values);
559
+ server = await this.#service.createServer({
560
+ slug: entry.id,
561
+ name: entry.name,
562
+ kind: installedRuntime === null ? 'remote' : 'home',
563
+ transport,
564
+ credentialId: credential.id,
565
+ enabled: true,
566
+ });
567
+ installation = this.#store.createInstallation({
568
+ source: 'curated',
569
+ entryId: entry.id,
570
+ entryVersion: entry.version ?? 'unpinned',
571
+ recipeRevision: fingerprint(entry),
572
+ targetType: 'server',
573
+ targetId: server.id,
574
+ credentialId: credential.id,
575
+ });
576
+ const result = { server, credential, installation };
577
+ this.#update(job, { step: 'done', result });
578
+ this.#updateRecord(job, { status: 'completed', resultReference: installation.id });
579
+ }
580
+ catch (error) {
581
+ try {
582
+ if (installation !== undefined)
583
+ this.#store.deleteInstallation(installation.id);
584
+ }
585
+ catch {
586
+ // Preserve the original install error if compensation also fails.
587
+ }
588
+ try {
589
+ if (server !== undefined)
590
+ await this.#service.deleteServer(server.id);
591
+ }
592
+ catch {
593
+ // Preserve the original install error if compensation also fails.
594
+ }
595
+ try {
596
+ if (credential !== undefined)
597
+ await this.#service.deleteCredential(credential.id);
598
+ }
599
+ catch {
600
+ // Preserve the original install error if compensation also fails.
601
+ }
602
+ try {
603
+ this.#update(job, {
604
+ step: 'failed',
605
+ error: error instanceof Error ? error.message : String(error),
606
+ });
607
+ this.#updateRecord(job, {
608
+ status: 'failed',
609
+ errorCode: error instanceof AppError ? error.code : 'market_install_failed',
610
+ });
611
+ }
612
+ catch {
613
+ // The runtime may have been torn down mid-install; nothing to persist.
614
+ }
615
+ }
616
+ finally {
617
+ this.#releaseEntry(entry.id, job.record.id);
618
+ }
619
+ }
620
+ #entry(id) {
621
+ const entry = marketCatalog.find((item) => item.id === id);
622
+ if (!entry)
623
+ throw new AppError('market_not_found', `Market entry "${id}" not found`, 404);
624
+ return entry;
625
+ }
626
+ #assertEntryAvailable(entryId) {
627
+ const operation = this.#entryOperations.get(entryId);
628
+ if (operation === undefined)
629
+ return;
630
+ if (operation.kind === 'job') {
631
+ const job = this.#store.getInstallJob(operation.id);
632
+ const active = job !== null && ['awaiting_secret', 'installing', 'updating'].includes(job.status);
633
+ if (!active) {
634
+ this.#entryOperations.delete(entryId);
635
+ return;
636
+ }
637
+ if (job.status === 'awaiting_secret') {
638
+ const action = job.actionId === null ? null : this.#store.getSecureAction(job.actionId);
639
+ if (action === null ||
640
+ action.status !== 'pending' ||
641
+ Date.parse(action.expiresAt) <= Date.now()) {
642
+ this.#entryOperations.delete(entryId);
643
+ return;
644
+ }
645
+ }
646
+ }
647
+ throw new AppError('market_operation_in_progress', `Market entry "${entryId}" already has an operation in progress`, 409);
648
+ }
649
+ #claimEntry(entryId, operationId) {
650
+ this.#assertEntryAvailable(entryId);
651
+ this.#entryOperations.set(entryId, { kind: 'job', id: operationId });
652
+ }
653
+ #releaseEntry(entryId, operationId) {
654
+ if (this.#entryOperations.get(entryId)?.id === operationId) {
655
+ this.#entryOperations.delete(entryId);
656
+ }
657
+ }
658
+ async #runUpdate(entry, installation, job) {
659
+ try {
660
+ const artifact = installerForEntry(entry);
661
+ const installedRuntime = artifact === null
662
+ ? null
663
+ : await installArtifact(artifact, this.#installerContext(job), { force: true });
664
+ if (installation.targetType === 'server') {
665
+ const server = this.#store.getServer(installation.targetId);
666
+ if (server === null) {
667
+ throw new AppError('server_not_found', 'Installed Market server not found', 404);
668
+ }
669
+ const values = this.#credentialEnvValues(installation.credentialId);
670
+ const transport = this.#serverTransport(entry, installedRuntime, values);
671
+ this.#update(job, { step: 'updating server runtime' });
672
+ await this.#service.updateServer(server.id, { transport });
673
+ this.#update(job, { step: 'restarting server' });
674
+ // A failed restart must not roll the job back — the updated target is
675
+ // persisted and can be restarted manually later.
676
+ let restartError;
677
+ try {
678
+ await this.#service.restartServer(server.id);
679
+ }
680
+ catch (error) {
681
+ restartError = error instanceof Error ? error.message : String(error);
682
+ }
683
+ this.#update(job, { step: 'updating installation record' });
684
+ const updated = this.#store.updateInstallation(installation.id, {
685
+ entryVersion: entry.version ?? 'unpinned',
686
+ recipeRevision: fingerprint(entry),
687
+ });
688
+ this.#update(job, {
689
+ step: 'done',
690
+ result: {
691
+ entryId: entry.id,
692
+ version: updated.entryVersion,
693
+ targetType: installation.targetType,
694
+ targetId: installation.targetId,
695
+ ...(restartError === undefined ? {} : { restartError }),
696
+ },
697
+ });
698
+ }
699
+ else {
700
+ const cli = this.#store.getCli(installation.targetId);
701
+ if (cli === null) {
702
+ throw new AppError('cli_not_found', 'Installed Market CLI not found', 404);
703
+ }
704
+ const credentialPayload = installation.credentialId === null
705
+ ? null
706
+ : this.#store.getCredentialPayload(installation.credentialId);
707
+ if (credentialPayload !== null) {
708
+ validateCliCredentialBindings(credentialPayload, entry.credentialBindings ?? {});
709
+ }
710
+ const projection = this.#cliProjection(entry, installedRuntime);
711
+ this.#update(job, { step: 'updating CLI runtime' });
712
+ this.#store.updateCli(cli.id, {
713
+ command: projection.command,
714
+ executionMode: projection.executionMode,
715
+ entrypoint: projection.entrypoint,
716
+ authStrategy: projection.authStrategy,
717
+ containerVolumes: projection.containerVolumes,
718
+ allowList: projection.allowList,
719
+ credentialBindings: entry.credentialBindings ?? {},
720
+ platform: entry.platform ?? null,
721
+ probe: entry.probe ?? { command: projection.command, args: ['--version'] },
722
+ });
723
+ this.#update(job, { step: 'updating installation record' });
724
+ const updated = this.#store.updateInstallation(installation.id, {
725
+ entryVersion: entry.version ?? 'unpinned',
726
+ recipeRevision: fingerprint(entry),
727
+ });
728
+ this.#update(job, {
729
+ step: 'done',
730
+ result: {
731
+ entryId: entry.id,
732
+ version: updated.entryVersion,
733
+ targetType: installation.targetType,
734
+ targetId: installation.targetId,
735
+ },
736
+ });
737
+ }
738
+ this.#updateRecord(job, { status: 'completed', resultReference: installation.id });
739
+ }
740
+ catch (error) {
741
+ try {
742
+ this.#update(job, {
743
+ step: 'failed',
744
+ error: error instanceof Error ? error.message : String(error),
745
+ });
746
+ this.#updateRecord(job, {
747
+ status: 'failed',
748
+ errorCode: error instanceof AppError ? error.code : 'market_update_failed',
749
+ });
750
+ }
751
+ catch {
752
+ // Runtime torn down mid-update; nothing to persist.
753
+ }
754
+ }
755
+ finally {
756
+ this.#releaseEntry(entry.id, job.record.id);
757
+ }
758
+ }
759
+ /** Best-effort latest upstream version; null when unreachable or unsupported. */
760
+ async #latestUpstream(entry) {
761
+ const pkg = entry.package;
762
+ if (pkg === undefined)
763
+ return null;
764
+ const url = entry.kind === 'uvx'
765
+ ? `https://pypi.org/pypi/${pkg}/json`
766
+ : entry.kind === 'home-stdio'
767
+ ? `https://registry.npmmirror.com/${pkg}/latest`
768
+ : null;
769
+ if (url === null)
770
+ return null;
771
+ try {
772
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
773
+ if (!response.ok)
774
+ return null;
775
+ const body = (await response.json());
776
+ const version = typeof body.version === 'string'
777
+ ? body.version
778
+ : typeof body.info?.version === 'string'
779
+ ? body.info.version
780
+ : null;
781
+ return version;
782
+ }
783
+ catch {
784
+ return null;
785
+ }
786
+ }
787
+ #serverTransport(entry, runtime, values) {
788
+ if (runtime === null) {
789
+ return {
790
+ type: 'streamable-http',
791
+ url: entry.url ?? '',
792
+ protocolMode: 'auto',
793
+ allowSseFallback: false,
794
+ headers: {},
795
+ };
796
+ }
797
+ const args = (entry.argsTemplate ?? []).map((argument) => argument.replace(/\$\{([^}]+)\}/g, (_, key) => values[key] ?? ''));
798
+ if (runtime.executionMode === 'host') {
799
+ return {
800
+ type: 'stdio',
801
+ command: runtime.command,
802
+ args,
803
+ env: entry.kind === 'uvx' ? { ...this.#uvEnv } : {},
804
+ protocolMode: 'auto',
805
+ };
806
+ }
807
+ return {
808
+ type: 'stdio',
809
+ command: 'docker',
810
+ args: [
811
+ 'run',
812
+ '--rm',
813
+ '-i',
814
+ runtime.command,
815
+ ...(runtime.entrypoint === null ? [] : [runtime.entrypoint]),
816
+ ],
817
+ env: {},
818
+ protocolMode: 'auto',
819
+ };
820
+ }
821
+ #credentialEnvValues(credentialId) {
822
+ if (credentialId === null)
823
+ return {};
824
+ const payload = this.#store.getCredentialPayload(credentialId);
825
+ return payload?.type === 'env' ? payload.variables : {};
826
+ }
827
+ #cliProjection(entry, runtime = null) {
828
+ const artifact = installerForEntry(entry);
829
+ const allowList = entry.allowList;
830
+ if (allowList === undefined || allowList.allow.length === 0) {
831
+ throw new AppError('market_cli_allow_list_required', `Market CLI entry "${entry.id}" must declare an argv allow-list`, 400);
832
+ }
833
+ return {
834
+ command: runtime?.command ??
835
+ (entry.kind === 'cli-image'
836
+ ? artifact?.type === 'docker'
837
+ ? artifact.image
838
+ : (entry.image ?? '')
839
+ : (entry.bin ?? entry.id)),
840
+ executionMode: runtime?.executionMode ?? (entry.kind === 'cli-image' ? 'docker' : 'host'),
841
+ entrypoint: runtime?.entrypoint ?? (entry.kind === 'cli-image' ? (entry.entrypoint ?? null) : null),
842
+ authStrategy: entry.cliRuntime?.authStrategy ?? 'none',
843
+ containerVolumes: (entry.cliRuntime?.containerVolumes ?? []).map((volume) => ({
844
+ source: volume.source,
845
+ target: volume.target,
846
+ readOnly: volume.readOnly ?? false,
847
+ })),
848
+ allowList,
849
+ };
850
+ }
851
+ #credentialPayload(entry, values) {
852
+ switch (entry.credential.type) {
853
+ case 'oauth':
854
+ return { type: 'oauth', tokenType: 'Bearer' };
855
+ case 'env':
856
+ return { type: 'env', variables: values };
857
+ case 'bearer':
858
+ return { type: 'bearer', token: values[entry.credential.tokenKey] ?? '' };
859
+ case 'api-key':
860
+ return {
861
+ type: 'api-key',
862
+ headerName: entry.credential.headerName,
863
+ value: values[entry.credential.valueKey] ?? '',
864
+ };
865
+ case 'headers':
866
+ return {
867
+ type: 'headers',
868
+ headers: Object.fromEntries(entry.credential.headers.map((header) => [
869
+ header.name,
870
+ header.valueKey ? (values[header.valueKey] ?? '') : (header.value ?? ''),
871
+ ])),
872
+ };
873
+ }
874
+ }
875
+ #installerContext(job) {
876
+ return {
877
+ marketDir: this.#marketDir,
878
+ uvEnv: this.#uvEnv,
879
+ update: (patch) => this.#update(job, patch),
880
+ runCommand: (command, args, options) => this.#runInstallerCommand(job, command, args, options),
881
+ };
882
+ }
883
+ #killInstallerProcess(child, signal) {
884
+ if (child.pid === undefined)
885
+ return;
886
+ try {
887
+ process.kill(-child.pid, signal);
888
+ }
889
+ catch {
890
+ try {
891
+ child.kill(signal);
892
+ }
893
+ catch {
894
+ // process already exited
895
+ }
896
+ }
897
+ }
898
+ #runInstallerCommand(job, command, args, options = {}) {
899
+ return new Promise((resolve) => {
900
+ const child = spawn(command, args, {
901
+ detached: true,
902
+ env: { ...process.env, ...(options.env ?? {}) },
903
+ stdio: ['ignore', 'pipe', 'pipe'],
904
+ });
905
+ this.#installerProcesses.add(child);
906
+ let output = '';
907
+ const append = (chunk) => {
908
+ output = `${output}${chunk}`.slice(-4000);
909
+ this.#update(job, { output });
910
+ };
911
+ child.stdout.on('data', (chunk) => append(String(chunk)));
912
+ child.stderr.on('data', (chunk) => append(String(chunk)));
913
+ const timer = setTimeout(() => this.#killInstallerProcess(child, 'SIGKILL'), options.timeoutMs ?? 300_000);
914
+ const cleanup = () => {
915
+ this.#installerProcesses.delete(child);
916
+ };
917
+ child.on('close', (code) => {
918
+ clearTimeout(timer);
919
+ cleanup();
920
+ resolve({ code: code ?? 1, output });
921
+ });
922
+ child.on('error', (error) => {
923
+ clearTimeout(timer);
924
+ cleanup();
925
+ resolve({ code: 1, output: `${command}: ${error.message}` });
926
+ });
927
+ });
928
+ }
929
+ }
930
+ //# sourceMappingURL=market-service.js.map