start-command 0.37.8 → 0.38.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 (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +8 -0
  3. package/package.json +1 -1
  4. package/src/bin/cli.js +66 -26
  5. package/src/lib/args-parser-queries.js +90 -9
  6. package/src/lib/args-parser.js +20 -1
  7. package/src/lib/container-removal.js +163 -0
  8. package/src/lib/detached-finalize.js +29 -2
  9. package/src/lib/docker-cleanup.js +7 -4
  10. package/src/lib/docker-command-handoff.js +34 -6
  11. package/src/lib/docker-post-mortem.js +2 -0
  12. package/src/lib/docker-runtime-args.js +7 -0
  13. package/src/lib/docker-snapshot-resume.js +222 -0
  14. package/src/lib/docker-snapshot-safety.js +124 -12
  15. package/src/lib/execution-attempt.js +21 -9
  16. package/src/lib/execution-chain.js +182 -0
  17. package/src/lib/execution-cleanup.js +193 -0
  18. package/src/lib/execution-control.js +2 -1
  19. package/src/lib/execution-env.js +120 -0
  20. package/src/lib/execution-identity.js +180 -0
  21. package/src/lib/execution-recovery.js +19 -0
  22. package/src/lib/execution-resume.js +77 -6
  23. package/src/lib/execution-store.js +63 -22
  24. package/src/lib/isolation.js +25 -3
  25. package/src/lib/launch-identity-options.js +81 -0
  26. package/src/lib/launch-identity.js +411 -0
  27. package/src/lib/log-uploader.js +15 -1
  28. package/src/lib/query-commands.js +34 -6
  29. package/src/lib/screen-isolation.js +11 -2
  30. package/src/lib/snapshot-resume-options.js +174 -0
  31. package/src/lib/spawn-helpers.js +33 -16
  32. package/src/lib/status-formatter.js +64 -35
  33. package/src/lib/status-links-notation.js +97 -0
  34. package/src/lib/store-lock.js +3 -0
  35. package/src/lib/usage.js +15 -1
  36. package/test/args-parser-attach-resume.js +1 -1
  37. package/test/args-parser-control.js +2 -2
  38. package/test/regression-224.js +807 -0
  39. package/test/regression-225.js +415 -0
  40. package/test/regression-226.js +81 -0
  41. package/test/regression-229.js +267 -0
  42. package/test/regression-230.js +234 -0
  43. package/test/regression-231.js +298 -0
  44. package/test/shared-golden.js +35 -0
@@ -0,0 +1,411 @@
1
+ /**
2
+ * What a new launch may do with its execution id (issue #229).
3
+ *
4
+ * A launch with an explicit `--session-id` that is already tracked used to
5
+ * replace the record and truncate the log. It is now refused unless
6
+ * `--append-attempt` is given, which keeps the record, archives the previous
7
+ * attempt into `attemptHistory`, and appends the new attempt to the same log
8
+ * after a lifecycle header (the layout `--resume` already writes).
9
+ *
10
+ * Two cases keep the earlier behaviour:
11
+ * - the next level of a stacked isolation, which receives the same
12
+ * `--session-id` from its parent and runs with START_EXECUTION_ID already
13
+ * set to it (`isNestedLaunchOf`);
14
+ * - a generated UUID, which cannot collide.
15
+ */
16
+ const fs = require('fs');
17
+ const { isNestedLaunchOf, ATTEMPT_ENV } = require('./execution-env');
18
+ const { findRecord, withStoreLock } = require('./execution-identity');
19
+
20
+ const LOG_HEADER = '=== Start Command Log ===';
21
+
22
+ class LaunchRefusedError extends Error {}
23
+
24
+ function alreadyExistsMessage(uuid) {
25
+ return `execution ${uuid} already exists; use $ --resume ${uuid} -- <command> to continue it, or pass --append-attempt`;
26
+ }
27
+
28
+ function isPidAlive(pid) {
29
+ if (!pid) {
30
+ return false;
31
+ }
32
+ try {
33
+ process.kill(pid, 0);
34
+ return true;
35
+ } catch (error) {
36
+ return error.code === 'EPERM';
37
+ }
38
+ }
39
+
40
+ /** True while the stored execution may still be running. */
41
+ function isExecutionLive(record) {
42
+ if (require('./launch-owner').hasActiveLaunch(record)) {
43
+ return true;
44
+ }
45
+ if (record.status !== 'executing') {
46
+ return false;
47
+ }
48
+ if (record.options && record.options.isolated) {
49
+ try {
50
+ return require('./session-probe').probeSession(record).alive === true;
51
+ } catch {
52
+ return true;
53
+ }
54
+ }
55
+ return isPidAlive(record.pid);
56
+ }
57
+
58
+ /** Attempts already in a log written without tracking, from its headers. */
59
+ function countLoggedAttempts(logPath) {
60
+ try {
61
+ return fs.readFileSync(logPath, 'utf8').split(LOG_HEADER).length - 1;
62
+ } catch {
63
+ return 0;
64
+ }
65
+ }
66
+
67
+ function logSize(logPath) {
68
+ try {
69
+ return fs.statSync(logPath).size;
70
+ } catch {
71
+ return 0;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Decide how a launch uses its execution id, before anything is printed or
77
+ * created.
78
+ * @param {object} params - {store, sessionId, explicitSessionId,
79
+ * appendAttempt, logPath, env}
80
+ * @returns {{append: boolean, previous: object|null, attemptNumber: number,
81
+ * logPath: string, nested: boolean, checked: boolean}}
82
+ * @throws {LaunchRefusedError} When the launch must not run
83
+ */
84
+ function preflightLaunch(params) {
85
+ const { store, sessionId, logPath } = params;
86
+ const env = params.env || process.env;
87
+ const fresh = {
88
+ append: false,
89
+ previous: null,
90
+ attemptNumber: 1,
91
+ logPath,
92
+ nested: false,
93
+ checked: false,
94
+ };
95
+ if (!params.explicitSessionId) {
96
+ return fresh;
97
+ }
98
+ if (isNestedLaunchOf(sessionId, env)) {
99
+ const inherited = Number(env[ATTEMPT_ENV]);
100
+ return {
101
+ ...fresh,
102
+ nested: true,
103
+ attemptNumber:
104
+ Number.isSafeInteger(inherited) && inherited > 0 ? inherited : 1,
105
+ };
106
+ }
107
+ const existing = store ? store.get(sessionId) : null;
108
+ if (existing && existing.uuid !== sessionId) {
109
+ throw new LaunchRefusedError(
110
+ `execution id ${sessionId} is already used as a name of execution ${existing.uuid}; pick another --session-id`
111
+ );
112
+ }
113
+ if (existing) {
114
+ if (!params.appendAttempt) {
115
+ throw new LaunchRefusedError(alreadyExistsMessage(sessionId));
116
+ }
117
+ if (isExecutionLive(existing)) {
118
+ throw new LaunchRefusedError(
119
+ `execution ${sessionId} is still running; wait for it to finish or use $ --stop ${sessionId} before --append-attempt`
120
+ );
121
+ }
122
+ const { nextAttemptNumber } = require('./execution-attempt');
123
+ return {
124
+ ...fresh,
125
+ append: true,
126
+ previous: existing,
127
+ attemptNumber: nextAttemptNumber(existing),
128
+ logPath: existing.logPath || logPath,
129
+ checked: true,
130
+ };
131
+ }
132
+ // Untracked (or deleted) executions are recognised by their log file.
133
+ if (logPath && fs.existsSync(logPath)) {
134
+ if (!params.appendAttempt) {
135
+ throw new LaunchRefusedError(alreadyExistsMessage(sessionId));
136
+ }
137
+ return {
138
+ ...fresh,
139
+ append: true,
140
+ attemptNumber: countLoggedAttempts(logPath) + 1,
141
+ checked: true,
142
+ };
143
+ }
144
+ return { ...fresh, checked: true };
145
+ }
146
+
147
+ /**
148
+ * Turn the record a launch built into the next attempt of `previous`,
149
+ * keeping the identity, history and log of the execution.
150
+ * @param {object} record - Freshly built ExecutionRecord (mutated)
151
+ * @param {object} previous - Stored record of the same UUID
152
+ * @returns {object} The record
153
+ */
154
+ function mergeAppendedAttempt(record, previous) {
155
+ const {
156
+ archiveAttempt,
157
+ createAttempt,
158
+ currentAttemptNumber,
159
+ } = require('./execution-attempt');
160
+ const prior = require('./execution-store').ExecutionRecord.fromObject(
161
+ JSON.parse(JSON.stringify(previous.toObject()))
162
+ );
163
+ if (!prior.attempt && currentAttemptNumber(prior) === 1) {
164
+ // A launch writes a fresh log, so its first attempt starts at offset 0.
165
+ prior.attempt = { number: 1, startedAt: prior.startTime, logOffset: 0 };
166
+ }
167
+ const attempt = createAttempt(prior, {
168
+ mode: 'append-attempt',
169
+ sessionName: record.options.sessionName || null,
170
+ });
171
+ archiveAttempt(prior);
172
+ const keep = prior.options || {};
173
+ const history = [...(keep.sessionNameHistory || [])];
174
+ if (keep.sessionName && keep.sessionName !== record.options.sessionName) {
175
+ history.push(keep.sessionName);
176
+ }
177
+ record.attempt = attempt;
178
+ record.attemptHistory = prior.attemptHistory;
179
+ record.startTime = prior.startTime;
180
+ record.logPath = prior.logPath || record.logPath;
181
+ record.continues = record.continues || prior.continues;
182
+ record.options = {
183
+ ...record.options,
184
+ ...(history.length ? { sessionNameHistory: [...new Set(history)] } : {}),
185
+ ...(keep.aliases ? { aliases: keep.aliases } : {}),
186
+ ...(keep.name ? { name: keep.name } : {}),
187
+ ...(keep.resumeCount ? { resumeCount: keep.resumeCount } : {}),
188
+ };
189
+ return record;
190
+ }
191
+
192
+ /**
193
+ * Persist the launch's record, refusing if another launch claimed the id in
194
+ * the meantime: a new id must still be free, an appended attempt must still
195
+ * follow the attempt the preflight saw.
196
+ * @param {object} store - ExecutionStore
197
+ * @param {object} record - Record to save
198
+ * @param {object} launch - preflightLaunch() result
199
+ */
200
+ function claimLaunch(store, record, launch) {
201
+ if (!launch.checked) {
202
+ store.save(record);
203
+ return;
204
+ }
205
+ withStoreLock(store, (records) => {
206
+ const index = records.findIndex((r) => r.uuid === record.uuid);
207
+ if (launch.previous) {
208
+ const current = records[index];
209
+ if (
210
+ !current ||
211
+ require('./launch-owner').hasActiveLaunch(current) ||
212
+ (current.attempt?.number || 0) !==
213
+ (launch.previous.attempt?.number || 0)
214
+ ) {
215
+ throw new LaunchRefusedError(
216
+ `execution ${record.uuid} was claimed by another launch`
217
+ );
218
+ }
219
+ records[index] = record;
220
+ } else {
221
+ if (findRecord(records, record.uuid)) {
222
+ throw new LaunchRefusedError(alreadyExistsMessage(record.uuid));
223
+ }
224
+ records.push(record);
225
+ }
226
+ return record;
227
+ });
228
+ }
229
+
230
+ /**
231
+ * Header a launch writes into its log: unchanged for a fresh log; for an
232
+ * appended attempt, preceded by a lifecycle line in the format `--resume`
233
+ * writes, so the attempts stay apart.
234
+ * @param {string} content - Header content
235
+ * @param {object} launch - preflightLaunch() result
236
+ * @param {object|null} record - Record of the launch, when tracked
237
+ * @returns {string} Content to write
238
+ */
239
+ function launchLogContent(content, launch, record = null) {
240
+ if (!launch.append) {
241
+ return content;
242
+ }
243
+ const lifecycle = {
244
+ event: 'attempt-appended',
245
+ at: new Date().toISOString(),
246
+ uuid: record ? record.uuid : null,
247
+ attemptNumber: launch.attemptNumber,
248
+ ...(record && record.attempt ? record.attempt : {}),
249
+ };
250
+ return `\n[Start Command Lifecycle] ${JSON.stringify(lifecycle)}\n${content}`;
251
+ }
252
+
253
+ /**
254
+ * Save the initial record of a direct run, before it starts. A refused
255
+ * claim ends the launch with exit code 1; other failures only lose tracking.
256
+ * @param {object|null} store - ExecutionStore
257
+ * @param {object|null} record - Execution record
258
+ * @param {object} launch - prepareLaunch() result
259
+ * @param {object} [config] - CLI configuration ({verbose})
260
+ */
261
+ function claimDirectLaunch(store, record, launch, config = {}) {
262
+ if (!record || !store) {
263
+ return;
264
+ }
265
+ try {
266
+ claimLaunch(store, record, launch);
267
+ } catch (err) {
268
+ if (err instanceof LaunchRefusedError) {
269
+ console.error(`Error: ${err.message}`);
270
+ process.exit(1);
271
+ }
272
+ if (config.verbose) {
273
+ console.error(
274
+ `[Tracking] Warning: Could not save execution record: ${err.message}`
275
+ );
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Write the launch header: a fresh log, or, for an appended attempt, a
282
+ * lifecycle line and the header after the earlier attempts.
283
+ * @param {string} logPath - Log path
284
+ * @param {string} content - Header content
285
+ * @param {object} launch - preflightLaunch() result
286
+ * @param {object|null} record - Record of the launch, when tracked
287
+ */
288
+ function writeLaunchLog(logPath, content, launch, record = null) {
289
+ const { appendLogFile, writeLogFile } = require('./isolation-log-utils');
290
+ const text = launchLogContent(content, launch, record);
291
+ if (launch.append) {
292
+ appendLogFile(logPath, text);
293
+ } else {
294
+ writeLogFile(logPath, text);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Preflight of the CLI: how this launch uses its id (#229) and which
300
+ * execution it continues (#231).
301
+ * @param {object} store - ExecutionStore, or null when tracking is off
302
+ * @param {object} options - Parsed wrapper options
303
+ * @param {string} sessionId - Execution UUID of the launch
304
+ * @param {string} logPath - Default log path of the launch
305
+ * @returns {object} preflightLaunch() result plus `continues`
306
+ * @throws {Error} When the launch must not run
307
+ */
308
+ function prepareLaunch(store, options, sessionId, logPath) {
309
+ const launch = preflightLaunch({
310
+ store,
311
+ sessionId,
312
+ explicitSessionId: Boolean(options.sessionId),
313
+ appendAttempt: Boolean(options.appendAttempt),
314
+ logPath,
315
+ });
316
+ launch.continues = options.continues
317
+ ? require('./execution-chain').resolveContinues(
318
+ store,
319
+ options.continues,
320
+ sessionId
321
+ )
322
+ : null;
323
+ return launch;
324
+ }
325
+
326
+ /**
327
+ * Apply the preflight to the record a launch built: the continued UUID and,
328
+ * for `--append-attempt`, the identity and history of the stored execution.
329
+ * @param {object|null} record - Freshly built ExecutionRecord (mutated)
330
+ * @param {object} launch - prepareLaunch() result
331
+ * @returns {object|null} The record
332
+ */
333
+ function applyLaunch(record, launch) {
334
+ if (!record) {
335
+ return record;
336
+ }
337
+ if (launch.continues) {
338
+ record.continues = launch.continues;
339
+ }
340
+ if (launch.previous) {
341
+ mergeAppendedAttempt(record, launch.previous);
342
+ } else if (launch.attemptNumber > 1) {
343
+ // A nested level or an untracked log: number the attempt all the same.
344
+ record.attempt = {
345
+ number: launch.attemptNumber,
346
+ startedAt: new Date().toISOString(),
347
+ logOffset: launch.append ? logSize(launch.logPath) : 0,
348
+ mode: launch.nested ? 'nested' : 'append-attempt',
349
+ };
350
+ }
351
+ return record;
352
+ }
353
+
354
+ /**
355
+ * START_* variables of a run without a backend session (direct, or
356
+ * `--isolated-user` alone): the session name is empty.
357
+ * @param {string} sessionId - Execution UUID
358
+ * @param {object} launch - prepareLaunch() result
359
+ * @returns {object} Variables
360
+ */
361
+ function directExecutionEnv(sessionId, launch) {
362
+ return require('./execution-env').buildExecutionEnv({
363
+ executionId: sessionId,
364
+ sessionName: '',
365
+ attempt: launch.attemptNumber,
366
+ });
367
+ }
368
+
369
+ /**
370
+ * Child environment of a direct run.
371
+ * @param {string} sessionId - Execution UUID
372
+ * @param {object} launch - prepareLaunch() result
373
+ * @returns {object} Child environment
374
+ */
375
+ function directLaunchEnv(sessionId, launch) {
376
+ return require('./execution-env').withExecutionEnv(
377
+ directExecutionEnv(sessionId, launch)
378
+ );
379
+ }
380
+
381
+ /**
382
+ * Command of an `--isolated-user` run without a backend: `sudo` resets the
383
+ * environment, so the variables are exported by the command itself.
384
+ * @param {string} cmd - Shell command
385
+ * @param {string} sessionId - Execution UUID
386
+ * @param {object} launch - prepareLaunch() result
387
+ * @returns {string} Prefixed command
388
+ */
389
+ function isolatedUserCommand(cmd, sessionId, launch) {
390
+ return require('./execution-env').prefixWithExecutionEnv(
391
+ cmd,
392
+ directExecutionEnv(sessionId, launch)
393
+ );
394
+ }
395
+
396
+ module.exports = {
397
+ LaunchRefusedError,
398
+ alreadyExistsMessage,
399
+ applyLaunch,
400
+ claimDirectLaunch,
401
+ claimLaunch,
402
+ directExecutionEnv,
403
+ directLaunchEnv,
404
+ isolatedUserCommand,
405
+ isExecutionLive,
406
+ launchLogContent,
407
+ mergeAppendedAttempt,
408
+ prepareLaunch,
409
+ preflightLaunch,
410
+ writeLaunchLog,
411
+ };
@@ -200,7 +200,21 @@ function uploadExecutionLog(store, identifier, options = {}) {
200
200
  };
201
201
  }
202
202
 
203
- return uploadLogPath(record.logPath, options);
203
+ // A continuation chain uploads every execution, oldest first (#231).
204
+ let prepared;
205
+ try {
206
+ prepared = require('./execution-chain').prepareUploadLog(store, record);
207
+ } catch (error) {
208
+ return {
209
+ success: false,
210
+ error: `Could not join chain logs: ${error.message}`,
211
+ };
212
+ }
213
+ try {
214
+ return uploadLogPath(prepared.path, options);
215
+ } finally {
216
+ prepared.cleanup();
217
+ }
204
218
  }
205
219
 
206
220
  module.exports = {
@@ -4,8 +4,9 @@
4
4
  * Every option in this module addresses an execution that already exists in
5
5
  * the tracking store instead of launching a new one: --status, --list,
6
6
  * --upload-log, --stop, --terminate, --attach, --resume, --resume-all and
7
- * --cleanup. `dispatchQueryCommand` is the single entry point the CLI calls
8
- * before it starts interpreting the command line as a command to run.
7
+ * --cleanup, plus --alias and --rename. `dispatchQueryCommand` is the single
8
+ * entry point the CLI calls before it starts interpreting the command line as
9
+ * a command to run.
9
10
  */
10
11
 
11
12
  const { queryStatus, listExecutions } = require('./status-formatter');
@@ -13,6 +14,8 @@ const { ControlAction, controlExecution } = require('./execution-control');
13
14
  const { attachExecution } = require('./execution-attach');
14
15
  const { resumeAllExecutions, resumeExecution } = require('./execution-resume');
15
16
  const { uploadExecutionLog } = require('./log-uploader');
17
+ const { snapshotResumeSettings } = require('./snapshot-resume-options');
18
+ const { cleanupExecution } = require('./execution-cleanup');
16
19
 
17
20
  /**
18
21
  * Print a handler result, returning the process exit code.
@@ -68,6 +71,8 @@ async function handleResumeQuery(
68
71
  outputFormat,
69
72
  resourceOptions,
70
73
  removeOriginal: resourceOptions.removeOriginal === true,
74
+ removeOriginalAfter: resourceOptions.removeOriginalAfter ?? 0,
75
+ snapshotOptions: snapshotResumeSettings(resourceOptions),
71
76
  })
72
77
  );
73
78
  }
@@ -78,12 +83,21 @@ function handleResumeAllQuery(store, outputFormat) {
78
83
 
79
84
  /**
80
85
  * Handle --cleanup / --cleanup-dry-run.
81
- * Cleans up stale "executing" records (processes that crashed or were killed).
86
+ * Cleans up stale "executing" records (processes that crashed or were killed),
87
+ * or with a target the Docker containers and snapshot images of one execution.
82
88
  * @param {?object} store - Execution store
83
89
  * @param {boolean} dryRun - If true, just report what would be cleaned
90
+ * @param {?string} [target] - UUID or session name of one execution
84
91
  * @returns {number} Exit code
85
92
  */
86
- function handleCleanup(store, dryRun) {
93
+ function handleCleanup(store, dryRun, target = null) {
94
+ if (hasValue(target)) {
95
+ const result = cleanupExecution(store, target, { dryRun });
96
+ if (!result.success && result.output) {
97
+ console.log(result.output);
98
+ }
99
+ return reportResult(result);
100
+ }
87
101
  if (!store) {
88
102
  console.error('Error: Execution tracking is disabled.');
89
103
  return 1;
@@ -144,7 +158,9 @@ function hasQueryMode(options) {
144
158
  hasValue(options.attach) ||
145
159
  hasValue(options.resume) ||
146
160
  options.resumeAll === true ||
147
- options.cleanup === true
161
+ options.cleanup === true ||
162
+ hasValue(options.alias) ||
163
+ hasValue(options.rename)
148
164
  );
149
165
  }
150
166
 
@@ -193,7 +209,19 @@ async function dispatchQueryCommand(options, context = {}) {
193
209
  return handleResumeAllQuery(store, outputFormat);
194
210
  }
195
211
  if (options.cleanup) {
196
- return handleCleanup(store, options.cleanupDryRun);
212
+ return handleCleanup(store, options.cleanupDryRun, options.cleanupTarget);
213
+ }
214
+ if (hasValue(options.alias) || hasValue(options.rename)) {
215
+ const { nameExecution } = require('./execution-identity');
216
+ const rename = hasValue(options.rename);
217
+ return reportResult(
218
+ nameExecution(
219
+ store,
220
+ rename ? options.rename : options.alias,
221
+ options.newName,
222
+ { rename }
223
+ )
224
+ );
197
225
  }
198
226
 
199
227
  return null;
@@ -128,6 +128,8 @@ function supportsLogfileOption() {
128
128
  * @param {string|null} user - Username to run command as (optional)
129
129
  * @param {Function} wrapCommandWithUser - Function to wrap command with user
130
130
  * @param {Function} isInteractiveShellCommand - Function to check if command is interactive shell
131
+ * @param {string|null} logPath - Log file to append to (optional)
132
+ * @param {object|null} env - Extra environment, e.g. START_* (optional)
131
133
  * @returns {Promise<{success: boolean, sessionName: string, message: string, output: string, exitCode: number}>}
132
134
  */
133
135
  function runScreenWithLogCapture(
@@ -137,7 +139,8 @@ function runScreenWithLogCapture(
137
139
  user = null,
138
140
  wrapCommandWithUser,
139
141
  isInteractiveShellCommand,
140
- logPath = null
142
+ logPath = null,
143
+ env = null
141
144
  ) {
142
145
  const { shell, shellArg } = shellInfo;
143
146
  const screenTempDir = getTempDir('isolation', 'screen');
@@ -237,8 +240,10 @@ function runScreenWithLogCapture(
237
240
  }
238
241
 
239
242
  // Use spawnSync with array args (not execSync string) to avoid quoting issues (issue #25)
243
+ // Each `screen -dmS` server inherits this env (START_*, issue #230).
240
244
  const result = spawnSync('screen', screenArgs, {
241
245
  stdio: 'inherit',
246
+ env: { ...process.env, ...env },
242
247
  });
243
248
 
244
249
  if (result.error) {
@@ -424,6 +429,7 @@ function startDetachedScreenWithLogCapture(
424
429
  keepAlive = false,
425
430
  logPath = null,
426
431
  wrapCommandWithUser,
432
+ env = null,
427
433
  } = options;
428
434
 
429
435
  const screenTempDir = getTempDir('isolation', 'screen');
@@ -471,7 +477,10 @@ function startDetachedScreenWithLogCapture(
471
477
  }
472
478
 
473
479
  try {
474
- const result = spawnSync('screen', screenArgs, { stdio: 'inherit' });
480
+ const result = spawnSync('screen', screenArgs, {
481
+ stdio: 'inherit',
482
+ env: { ...process.env, ...env },
483
+ });
475
484
  if (result.error) {
476
485
  throw result.error;
477
486
  }