start-command 0.43.0 → 0.44.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.
@@ -5,4 +5,4 @@ with multi-package repos, or single-package repos to help you version and publis
5
5
  find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6
6
 
7
7
  We have a quick list of common questions to get you started engaging with this project in
8
- [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
8
+ [our documentation](https://github.com/changesets/changesets/blob/main/docs/faq.md)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # start-command
2
2
 
3
+ ## 0.44.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 76efaf3: `--pause <id>` / `--unpause <id>` freeze and thaw a detached Docker execution in place (#263).
8
+
9
+ - **Same execution:** `docker pause` (cgroup freezer) keeps the container, its process (same PID), the execution UUID, record and log; `docker unpause` lets the same process continue. No new execution, attempt or recovery is created. Docker is called with argument arrays and a 30 second timeout.
10
+ - **Resources:** CPU is released while memory and disk stay allocated; `--stop` frees the RAM. Reports include the resource state and container PID.
11
+ - **Record:** `paused`, `pausedAt`, `pauseCount`, `pausedBy` and `pauseReason` (`--pause-reason`, `--paused-by`); unpause clears the pause and records `pauseEndedBy`. Pausing an already paused container or unpausing a running one is a no-op; stopped or removed containers fail with a pointer to `--status` and `--resume`. screen and tmux sessions are refused.
12
+ - **Status:** `--status` reports `dockerState`, `paused` and `pausedAt`; a paused execution stays `executing` and `--list --running` includes it. The recorded pause is reconciled with `docker inspect`, so a daemon restart, reboot or external `docker unpause`/`docker rm` never leaves a stale `paused: true`, and a removed container shows `removed`.
13
+ - **Guards:** a paused execution is never attached to, resumed (no snapshot), resumed by `--resume-all` (skipped with a reason), cleaned up or completed by the watcher. `--stop`/`--terminate` clear the pause and count as an intentional stop, so `--on-kill-resume` does not restart it.
14
+ - **Parity:** the JavaScript and Rust pause/unpause paths are held to shared golden contracts (`pause-control` in `parity/fixtures/contracts.json`) covering reports in every output format, errors, docker calls and the saved record.
15
+
3
16
  ## 0.43.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -27,6 +27,8 @@ $ --list
27
27
  $ --help
28
28
  $ -i docker -d --label task.tool=codex -- my-task
29
29
  $ --attach <id>
30
+ $ --pause <id>
31
+ $ --unpause <id>
30
32
  $ --resume <id> -- <command>
31
33
  $ --resume-all
32
34
  $ --session-id <uuid> --append-attempt -- <command>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
5
5
  "main": "src/bin/cli.js",
6
6
  "exports": {
@@ -49,12 +49,12 @@
49
49
  "devDependencies": {
50
50
  "@changesets/cli": "^3.0.3",
51
51
  "@eslint/js": "^10.0.1",
52
- "eslint": "^10.14.0",
52
+ "eslint": "^10.15.0",
53
53
  "eslint-config-prettier": "^10.1.8",
54
54
  "eslint-plugin-prettier": "^5.5.7",
55
55
  "husky": "^9.1.7",
56
56
  "lint-staged": "^17.6.0",
57
- "prettier": "^3.9.10"
57
+ "prettier": "^3.10.1"
58
58
  },
59
59
  "lint-staged": {
60
60
  "*.{js,mjs,cjs}": [
@@ -70,10 +70,10 @@
70
70
  "dependencies": {
71
71
  "@link-foundation/sensitive-data-sanitizer": "^0.14.0",
72
72
  "command-stream": "^2.6.0",
73
- "lino-objects-codec": "^0.9.5"
73
+ "lino-objects-codec": "^0.9.6"
74
74
  },
75
75
  "overrides": {
76
- "brace-expansion": "^5.0.12",
76
+ "brace-expansion": "^5.0.13",
77
77
  "flatted": "^3.4.4"
78
78
  }
79
79
  }
@@ -7,6 +7,8 @@
7
7
  *
8
8
  * Query modes: --status, --list [--running]
9
9
  * Control modes: --upload-log, --stop, --terminate, --attach [--read-only],
10
+ * --pause [--pause-reason <text>] [--paused-by <name>],
11
+ * --unpause (issue #263),
10
12
  * --resume [-- <command>], --resume-all,
11
13
  * --cleanup [<uuid-or-session-name>]
12
14
  * Naming modes: --alias <id> <name>, --rename <id> <name> (issue #231)
@@ -33,6 +35,10 @@ function createQueryOptionDefaults() {
33
35
  outputFormat: null, // Output format (links-notation, json, text)
34
36
  stop: null, // UUID/session name to stop gracefully
35
37
  terminate: null, // UUID/session name to terminate immediately
38
+ pause: null, // UUID/session name of a docker execution to freeze
39
+ unpause: null, // UUID/session name of a paused docker execution to thaw
40
+ pauseReason: null, // Why --pause froze the execution (recorded)
41
+ pausedBy: null, // Who --pause froze the execution for (recorded)
36
42
  attach: null, // UUID/session name whose terminal should be attached
37
43
  readOnly: false, // Follow --attach output without forwarding stdin
38
44
  resume: null, // UUID/session name to resume (optionally with a new command)
@@ -55,6 +61,8 @@ const IDENTIFIER_OPTIONS = {
55
61
  '--upload-log': 'uploadLog',
56
62
  '--stop': 'stop',
57
63
  '--terminate': 'terminate',
64
+ '--pause': 'pause',
65
+ '--unpause': 'unpause',
58
66
  '--attach': 'attach',
59
67
  '--resume': 'resume',
60
68
  };
@@ -153,6 +161,32 @@ function parseNamingOption(args, index, options) {
153
161
  return 3;
154
162
  }
155
163
 
164
+ /**
165
+ * `--pause-reason <text>` and `--paused-by <name>` (issue #263) annotate a
166
+ * `--pause`; their values may contain spaces but not start with `-`.
167
+ */
168
+ const PAUSE_DETAIL_OPTIONS = {
169
+ '--pause-reason': 'pauseReason',
170
+ '--paused-by': 'pausedBy',
171
+ };
172
+
173
+ function parsePauseDetailOption(args, index, options) {
174
+ const arg = args[index];
175
+ const equals = arg.indexOf('=');
176
+ const name = equals === -1 ? arg : arg.slice(0, equals);
177
+ const key = PAUSE_DETAIL_OPTIONS[name];
178
+ if (!key) {
179
+ return 0;
180
+ }
181
+ const inline = equals !== -1;
182
+ const value = inline ? arg.slice(equals + 1) : args[index + 1];
183
+ if (!value || (!inline && value.startsWith('-'))) {
184
+ throw new Error(`Option ${name} requires a value`);
185
+ }
186
+ options[key] = value;
187
+ return inline ? 1 : 2;
188
+ }
189
+
156
190
  /**
157
191
  * Parse a single query/control option.
158
192
  * @param {string[]} args - Arguments array
@@ -202,6 +236,11 @@ function parseQueryOption(args, index, options) {
202
236
  }
203
237
  }
204
238
 
239
+ const pauseDetailConsumed = parsePauseDetailOption(args, index, options);
240
+ if (pauseDetailConsumed > 0) {
241
+ return pauseDetailConsumed;
242
+ }
243
+
205
244
  // --output-format <format>
206
245
  if (arg === '--output-format') {
207
246
  if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
@@ -242,6 +281,12 @@ function activeQueryModes(options) {
242
281
  if (hasValue(options.terminate)) {
243
282
  modes.push('--terminate');
244
283
  }
284
+ if (hasValue(options.pause)) {
285
+ modes.push('--pause');
286
+ }
287
+ if (hasValue(options.unpause)) {
288
+ modes.push('--unpause');
289
+ }
245
290
  if (hasValue(options.attach)) {
246
291
  modes.push('--attach');
247
292
  }
@@ -266,7 +311,14 @@ function activeQueryModes(options) {
266
311
  /**
267
312
  * Formats are only meaningful for modes that print a machine-readable report.
268
313
  */
269
- const OUTPUT_FORMAT_MODES = ['--status', '--list', '--resume', '--resume-all'];
314
+ const OUTPUT_FORMAT_MODES = [
315
+ '--status',
316
+ '--list',
317
+ '--resume',
318
+ '--resume-all',
319
+ '--pause',
320
+ '--unpause',
321
+ ];
270
322
 
271
323
  /**
272
324
  * Validate query/control options.
@@ -292,7 +344,7 @@ function validateQueryOptions(options) {
292
344
 
293
345
  if (modes.length > 1) {
294
346
  throw new Error(
295
- 'Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, --cleanup, --alias, or --rename in the same invocation'
347
+ 'Cannot combine --status, --list, --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume, --resume-all, --cleanup, --alias, or --rename in the same invocation'
296
348
  );
297
349
  }
298
350
 
@@ -301,7 +353,7 @@ function validateQueryOptions(options) {
301
353
  !modes.some((mode) => OUTPUT_FORMAT_MODES.includes(mode))
302
354
  ) {
303
355
  throw new Error(
304
- '--output-format option is only valid with --status, --list, --resume, or --resume-all'
356
+ '--output-format option is only valid with --status, --list, --resume, --resume-all, --pause, or --unpause'
305
357
  );
306
358
  }
307
359
 
@@ -312,6 +364,15 @@ function validateQueryOptions(options) {
312
364
  if (options.readOnly && !hasValue(options.attach)) {
313
365
  throw new Error('--read-only option is only valid with --attach');
314
366
  }
367
+
368
+ if (
369
+ (hasValue(options.pauseReason) || hasValue(options.pausedBy)) &&
370
+ !hasValue(options.pause)
371
+ ) {
372
+ throw new Error(
373
+ '--pause-reason and --paused-by options are only valid with --pause'
374
+ );
375
+ }
315
376
  }
316
377
 
317
378
  module.exports = {
@@ -27,6 +27,9 @@ const BATCH_FIELDS = [
27
27
  '.State.OOMKilled',
28
28
  '.State.StartedAt',
29
29
  '.State.FinishedAt',
30
+ // Paused state for --status/--list (issue #263).
31
+ '.State.Status',
32
+ '.State.Paused',
30
33
  ];
31
34
  const BATCH_FORMAT = BATCH_FIELDS.map((field) => `{{${field}}}`).join(' ');
32
35
  const NOT_FOUND_LINE = /no such (?:object|container):\s*(\S+)/gi;
@@ -12,7 +12,11 @@ const { spawnSync } = require('child_process');
12
12
  const { escapeForLinksNotation } = require('./output-blocks');
13
13
  const { getDockerCommand, getDockerSpawnOptions } = require('./docker-cleanup');
14
14
  const { runCommand } = require('./execution-control');
15
- const { SessionState, probeSession } = require('./session-probe');
15
+ const {
16
+ SessionState,
17
+ pausedSessionError,
18
+ probeSession,
19
+ } = require('./session-probe');
16
20
 
17
21
  /**
18
22
  * Run a command with the parent terminal attached (stdin included unless the
@@ -104,6 +108,11 @@ function buildAttachPlan(record, options = {}) {
104
108
  return logFollowPlan(record, backend, sessionName);
105
109
  }
106
110
 
111
+ if (probe.state === SessionState.PAUSED) {
112
+ // `docker attach` to a frozen container would hang (issue #263).
113
+ return pausedSessionError('attach to', record.uuid, sessionName);
114
+ }
115
+
107
116
  if (!probe.alive) {
108
117
  return notRunningError(record, { ...probe, sessionName });
109
118
  }
@@ -153,6 +153,14 @@ function cleanupExecution(store, identifier, deps = {}) {
153
153
  }
154
154
  const runner = deps.runner || runCommand;
155
155
  const dryRun = deps.dryRun === true;
156
+ if (require('./execution-pause').isConfirmedPaused(record, runner)) {
157
+ // A paused container keeps its process; never remove it (issue #263).
158
+ return {
159
+ success: false,
160
+ output: `Retained container ${record.options.sessionName} (paused)`,
161
+ error: `${identifier} is paused; nothing was removed. Use \`$ --unpause ${record.uuid}\` or \`$ --stop ${record.uuid}\` first.`,
162
+ };
163
+ }
156
164
  const plan = planExecutionCleanup(record, runner);
157
165
  const outcomes = plan.containers.map((name) =>
158
166
  removeObject('container', name, runner, dryRun)
@@ -271,16 +271,25 @@ function formatControlResultAsLinksNotation(result) {
271
271
  return lines.join('\n');
272
272
  }
273
273
 
274
+ /**
275
+ * Whether a deliberate stop must be recorded: with launch-time kill recovery
276
+ * (issue #176), and for a paused container, whose stop is never a crash
277
+ * (issue #263).
278
+ */
279
+ function tracksStopIntent(options) {
280
+ return Boolean(options.onKillResume) || options.paused === true;
281
+ }
282
+
274
283
  /**
275
284
  * Record that a stop was requested, for sessions with launch-time kill
276
- * recovery (issue #176).
285
+ * recovery (issue #176) or a recorded pause (issue #263).
277
286
  * @param {object} store - Execution store
278
287
  * @param {object} record - Execution record
279
288
  * @returns {string|undefined} The previous marker, to restore on failure
280
289
  */
281
290
  function markStopRequested(store, record) {
282
291
  const options = record.options || {};
283
- if (!options.onKillResume) {
292
+ if (!tracksStopIntent(options)) {
284
293
  return undefined;
285
294
  }
286
295
  const previous = options.stopRequestedAt;
@@ -294,7 +303,7 @@ function markStopRequested(store, record) {
294
303
  }
295
304
 
296
305
  function restoreStopRequested(store, record, previous) {
297
- if (!record.options || !record.options.onKillResume) {
306
+ if (!tracksStopIntent(record.options || {})) {
298
307
  return;
299
308
  }
300
309
  record.options = { ...record.options, stopRequestedAt: previous };
@@ -374,6 +383,8 @@ function controlExecution(store, identifier, action, runner = runCommand) {
374
383
  };
375
384
  }
376
385
 
386
+ // Stopping a paused container ends its pause (issue #263).
387
+ require('./execution-pause').clearPauseAfterControl(store, record, action);
377
388
  const output = formatControlResultAsLinksNotation({
378
389
  action,
379
390
  identifier,