@sanity/runtime-cli 15.1.4 → 15.1.5

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.
package/README.md CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @sanity/runtime-cli
20
20
  $ sanity-run COMMAND
21
21
  running command...
22
22
  $ sanity-run (--version)
23
- @sanity/runtime-cli/15.1.4 linux-x64 node-v24.16.0
23
+ @sanity/runtime-cli/15.1.5 linux-x64 node-v24.16.0
24
24
  $ sanity-run --help [COMMAND]
25
25
  USAGE
26
26
  $ sanity-run COMMAND
@@ -103,7 +103,7 @@ EXAMPLES
103
103
  $ sanity-run blueprints add function --name my-function --fn-type document-create --fn-type document-update --lang js
104
104
  ```
105
105
 
106
- _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/add.ts)_
106
+ _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/add.ts)_
107
107
 
108
108
  ## `sanity-run blueprints config`
109
109
 
@@ -140,7 +140,7 @@ EXAMPLES
140
140
  $ sanity-run blueprints config --edit --project-id <projectId> --stack <name-or-id>
141
141
  ```
142
142
 
143
- _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/config.ts)_
143
+ _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/config.ts)_
144
144
 
145
145
  ## `sanity-run blueprints deploy`
146
146
 
@@ -175,6 +175,9 @@ DESCRIPTION
175
175
 
176
176
  Set SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.
177
177
 
178
+ Exit codes: 0 deployed, 2 deployment failed, 75 deployment accepted but completion could not be confirmed (rerun
179
+ 'blueprints info' to check).
180
+
178
181
  EXAMPLES
179
182
  $ sanity-run blueprints deploy
180
183
 
@@ -191,7 +194,7 @@ EXAMPLES
191
194
  $ sanity-run blueprints deploy --new-stack-name <new-name>
192
195
  ```
193
196
 
194
- _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/deploy.ts)_
197
+ _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/deploy.ts)_
195
198
 
196
199
  ## `sanity-run blueprints destroy`
197
200
 
@@ -220,13 +223,16 @@ DESCRIPTION
220
223
 
221
224
  Use this to clean up test environments or decommission a Stack you no longer need.
222
225
 
226
+ Exit codes: 0 destroyed, 2 destruction failed, 75 destruction accepted but completion could not be confirmed (rerun
227
+ 'blueprints info' to check).
228
+
223
229
  EXAMPLES
224
230
  $ sanity-run blueprints destroy
225
231
 
226
232
  $ sanity-run blueprints destroy --stack <name-or-id> --project-id <projectId> --force --no-wait
227
233
  ```
228
234
 
229
- _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/destroy.ts)_
235
+ _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/destroy.ts)_
230
236
 
231
237
  ## `sanity-run blueprints doctor`
232
238
 
@@ -257,7 +263,7 @@ EXAMPLES
257
263
  $ sanity-run blueprints doctor --fix
258
264
  ```
259
265
 
260
- _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/doctor.ts)_
266
+ _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/doctor.ts)_
261
267
 
262
268
  ## `sanity-run blueprints info`
263
269
 
@@ -294,7 +300,7 @@ EXAMPLES
294
300
  $ sanity-run blueprints info --organization-id <orgId> --stack <name-or-id>
295
301
  ```
296
302
 
297
- _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/info.ts)_
303
+ _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/info.ts)_
298
304
 
299
305
  ## `sanity-run blueprints init [DIR]`
300
306
 
@@ -353,7 +359,7 @@ EXAMPLES
353
359
  $ sanity-run blueprints init old-stack --type <json|js|ts> --project-id <projectId> --stack-id <existingStackId>
354
360
  ```
355
361
 
356
- _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/init.ts)_
362
+ _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/init.ts)_
357
363
 
358
364
  ## `sanity-run blueprints logs`
359
365
 
@@ -399,7 +405,7 @@ EXAMPLES
399
405
  $ sanity-run blueprints logs --before 2026-05-01T00:00:00Z
400
406
  ```
401
407
 
402
- _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/logs.ts)_
408
+ _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/logs.ts)_
403
409
 
404
410
  ## `sanity-run blueprints mint-deploy-token`
405
411
 
@@ -444,7 +450,7 @@ EXAMPLES
444
450
  $ sanity-run blueprints mint-deploy-token --organization-id <orgId>
445
451
  ```
446
452
 
447
- _See code: [src/commands/blueprints/mint-deploy-token.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/mint-deploy-token.ts)_
453
+ _See code: [src/commands/blueprints/mint-deploy-token.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/mint-deploy-token.ts)_
448
454
 
449
455
  ## `sanity-run blueprints plan`
450
456
 
@@ -477,7 +483,7 @@ EXAMPLES
477
483
  $ sanity-run blueprints plan --organization-id <orgId> --stack <name-or-id>
478
484
  ```
479
485
 
480
- _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/plan.ts)_
486
+ _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/plan.ts)_
481
487
 
482
488
  ## `sanity-run blueprints promote`
483
489
 
@@ -513,7 +519,7 @@ EXAMPLES
513
519
  $ sanity-run blueprints promote --new-stack-name <new-name>
514
520
  ```
515
521
 
516
- _See code: [src/commands/blueprints/promote.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/promote.ts)_
522
+ _See code: [src/commands/blueprints/promote.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/promote.ts)_
517
523
 
518
524
  ## `sanity-run blueprints stacks`
519
525
 
@@ -549,7 +555,7 @@ EXAMPLES
549
555
  $ sanity-run blueprints stacks --organization-id <organizationId> --include-projects
550
556
  ```
551
557
 
552
- _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/blueprints/stacks.ts)_
558
+ _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/blueprints/stacks.ts)_
553
559
 
554
560
  ## `sanity-run functions add`
555
561
 
@@ -601,7 +607,7 @@ EXAMPLES
601
607
  $ sanity-run functions add --name my-function --type document-create --type document-update --lang js
602
608
  ```
603
609
 
604
- _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/functions/add.ts)_
610
+ _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/functions/add.ts)_
605
611
 
606
612
  ## `sanity-run functions dev`
607
613
 
@@ -636,7 +642,7 @@ EXAMPLES
636
642
  $ sanity-run functions dev --timeout 60
637
643
  ```
638
644
 
639
- _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/functions/dev.ts)_
645
+ _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/functions/dev.ts)_
640
646
 
641
647
  ## `sanity-run functions env add NAME KEY VALUE`
642
648
 
@@ -666,7 +672,7 @@ EXAMPLES
666
672
  $ sanity-run functions env add MyFunction API_URL https://api.example.com/
667
673
  ```
668
674
 
669
- _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/functions/env/add.ts)_
675
+ _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/functions/env/add.ts)_
670
676
 
671
677
  ## `sanity-run functions env list NAME`
672
678
 
@@ -693,7 +699,7 @@ EXAMPLES
693
699
  $ sanity-run functions env list MyFunction
694
700
  ```
695
701
 
696
- _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/functions/env/list.ts)_
702
+ _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/functions/env/list.ts)_
697
703
 
698
704
  ## `sanity-run functions env remove NAME KEY`
699
705
 
@@ -722,7 +728,7 @@ EXAMPLES
722
728
  $ sanity-run functions env remove MyFunction API_URL
723
729
  ```
724
730
 
725
- _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/functions/env/remove.ts)_
731
+ _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/functions/env/remove.ts)_
726
732
 
727
733
  ## `sanity-run functions logs [NAME]`
728
734
 
@@ -762,7 +768,7 @@ EXAMPLES
762
768
  $ sanity-run functions logs <name> --delete
763
769
  ```
764
770
 
765
- _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/functions/logs.ts)_
771
+ _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/functions/logs.ts)_
766
772
 
767
773
  ## `sanity-run functions test [NAME]`
768
774
 
@@ -819,7 +825,7 @@ EXAMPLES
819
825
  $ sanity-run functions test <name> --event update --data-before '{ "title": "before" }' --data-after '{ "title": "after" }'
820
826
  ```
821
827
 
822
- _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.4/src/commands/functions/test.ts)_
828
+ _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v15.1.5/src/commands/functions/test.ts)_
823
829
 
824
830
  ## `sanity-run help [COMMAND]`
825
831
 
@@ -0,0 +1,30 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { ActionResponse, AuthParams, StackOperation } from '../../utils/types.js';
3
+ /** Delay before the first operation poll. Override with SANITY_OPERATION_POLL_INITIAL_DELAY_MS. */
4
+ export declare const OPERATION_POLL_INITIAL_DELAY_MS: number;
5
+ /** Interval between operation status polls. Override with SANITY_OPERATION_POLL_INTERVAL_MS. */
6
+ export declare const OPERATION_POLL_INTERVAL_MS: number;
7
+ /** Consecutive failed polls tolerated before giving up on confirming status. */
8
+ export declare const OPERATION_POLL_MAX_CONSECUTIVE_ERRORS = 5;
9
+ /**
10
+ * Classify a single poll. Terminal status -> 'completed'/'failed'. 404 ->
11
+ * 'pending' (read-replica lag). Other failed responses -> 'error'. The caller
12
+ * decides whether an 'error' is retryable.
13
+ */
14
+ export declare function classifyOperationPoll(options: {
15
+ ok: boolean;
16
+ httpStatus: number | undefined;
17
+ operation: StackOperation | undefined;
18
+ }): 'pending' | 'completed' | 'failed' | 'error';
19
+ interface GetOperationResponse extends ActionResponse {
20
+ operation: StackOperation;
21
+ response?: Response;
22
+ }
23
+ export declare function getOperation({ stackId, operationId, auth, logger, includeDestroyed, }: {
24
+ stackId: string;
25
+ operationId: string;
26
+ auth: AuthParams;
27
+ logger: Logger;
28
+ includeDestroyed?: boolean;
29
+ }): Promise<GetOperationResponse>;
30
+ export {};
@@ -0,0 +1,50 @@
1
+ import getHeaders from '../../utils/get-headers.js';
2
+ import { createTracedFetch } from '../../utils/traced-fetch.js';
3
+ import { stacksUrl } from './stacks.js';
4
+ const envMs = (value, fallback) => {
5
+ if (!value)
6
+ return fallback;
7
+ const parsed = Number(value);
8
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
9
+ };
10
+ /** Delay before the first operation poll. Override with SANITY_OPERATION_POLL_INITIAL_DELAY_MS. */
11
+ export const OPERATION_POLL_INITIAL_DELAY_MS = envMs(process.env.SANITY_OPERATION_POLL_INITIAL_DELAY_MS, 1000);
12
+ /** Interval between operation status polls. Override with SANITY_OPERATION_POLL_INTERVAL_MS. */
13
+ export const OPERATION_POLL_INTERVAL_MS = envMs(process.env.SANITY_OPERATION_POLL_INTERVAL_MS, 1500);
14
+ /** Consecutive failed polls tolerated before giving up on confirming status. */
15
+ export const OPERATION_POLL_MAX_CONSECUTIVE_ERRORS = 5;
16
+ /**
17
+ * Classify a single poll. Terminal status -> 'completed'/'failed'. 404 ->
18
+ * 'pending' (read-replica lag). Other failed responses -> 'error'. The caller
19
+ * decides whether an 'error' is retryable.
20
+ */
21
+ export function classifyOperationPoll(options) {
22
+ const { ok, httpStatus, operation } = options;
23
+ if (ok) {
24
+ if (operation?.status === 'COMPLETED')
25
+ return 'completed';
26
+ if (operation?.status === 'FAILED')
27
+ return 'failed';
28
+ return 'pending';
29
+ }
30
+ if (httpStatus === 404)
31
+ return 'pending';
32
+ return 'error';
33
+ }
34
+ export async function getOperation({ stackId, operationId, auth, logger, includeDestroyed, }) {
35
+ const fetchFn = createTracedFetch(logger);
36
+ const url = new URL(`${stacksUrl}/${stackId}/operations/${operationId}`);
37
+ if (includeDestroyed)
38
+ url.searchParams.append('includeDestroyed', 'true');
39
+ const response = await fetchFn(url.toString(), {
40
+ method: 'GET',
41
+ headers: getHeaders(auth),
42
+ });
43
+ const data = await response.json();
44
+ return {
45
+ ok: response.ok,
46
+ error: response.ok ? null : data.message,
47
+ operation: data,
48
+ response,
49
+ };
50
+ }
@@ -38,7 +38,7 @@ export declare function createEmptyStack({ token, scopeType, scopeId, name, logg
38
38
  }): Promise<Stack>;
39
39
  interface UpdateStackResponse extends ActionResponse {
40
40
  stack: Stack & {
41
- operationId?: string;
41
+ operationId: string;
42
42
  };
43
43
  }
44
44
  export declare function updateStack({ stackId, stackMutation, auth, logger, }: {
@@ -99,7 +99,7 @@ export declare function planStack({ stackId, document, auth, logger, }: {
99
99
  }): Promise<PlanStackResponse>;
100
100
  interface DestroyStackResponse extends ActionResponse {
101
101
  stack: Stack & {
102
- operationId?: string;
102
+ operationId: string;
103
103
  };
104
104
  }
105
105
  export declare function resolveStackIdByNameOrId(value: string, auth: AuthParams, logger: Logger): Promise<string>;
@@ -0,0 +1,30 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { AuthParams } from '../../utils/types.js';
3
+ /** Result of waiting for a deploy/destroy operation to settle. */
4
+ export type OperationOutcome = {
5
+ type: 'completed';
6
+ } | {
7
+ type: 'failed';
8
+ logHints: string[];
9
+ } | {
10
+ type: 'unconfirmed';
11
+ error?: string;
12
+ };
13
+ export interface WaitForOperationOptions {
14
+ stackId: string;
15
+ operationId: string;
16
+ auth: AuthParams;
17
+ log: Logger;
18
+ bin: string;
19
+ verbose?: boolean;
20
+ /** Keep the operation queryable after the stack is gone (destroy). */
21
+ includeDestroyed?: boolean;
22
+ /** Noun for the idle message, e.g. "deployment" or "destruction". */
23
+ progressNoun: string;
24
+ }
25
+ /**
26
+ * Stream logs and poll an operation until it settles.
27
+ * handles log-stream lifecycle, retry budget, and idle messaging;
28
+ * the caller maps the returned outcome to its own result/messaging.
29
+ */
30
+ export declare function waitForOperation(options: WaitForOperationOptions): Promise<OperationOutcome>;
@@ -0,0 +1,88 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
+ import { createHintCollector } from '../../utils/blueprints/hints.js';
3
+ import { setupLogPolling } from './logs-polling.js';
4
+ import { classifyOperationPoll, getOperation, OPERATION_POLL_INITIAL_DELAY_MS, OPERATION_POLL_INTERVAL_MS, OPERATION_POLL_MAX_CONSECUTIVE_ERRORS, } from './operations.js';
5
+ /**
6
+ * Stream logs and poll an operation until it settles.
7
+ * handles log-stream lifecycle, retry budget, and idle messaging;
8
+ * the caller maps the returned outcome to its own result/messaging.
9
+ */
10
+ export async function waitForOperation(options) {
11
+ const { stackId, operationId, auth, log, bin, verbose = false, includeDestroyed, progressNoun, } = options;
12
+ const logHints = createHintCollector(bin);
13
+ let logStreamCleanup = null;
14
+ try {
15
+ let lastLogAt = Date.now();
16
+ let idleMessageShown = false;
17
+ let consecutiveErrors = 0;
18
+ let lastStatus;
19
+ logStreamCleanup = setupLogPolling({
20
+ stackId,
21
+ operationId,
22
+ auth,
23
+ log,
24
+ verbose,
25
+ onActivity: () => {
26
+ lastLogAt = Date.now();
27
+ },
28
+ onLogEntry: (logEntry) => logHints.inspectLog(logEntry),
29
+ });
30
+ // balk in case read replica lags
31
+ await sleep(OPERATION_POLL_INITIAL_DELAY_MS);
32
+ while (true) {
33
+ const { ok, error: pollError, operation, response, } = await getOperation({
34
+ stackId,
35
+ operationId,
36
+ auth,
37
+ logger: log,
38
+ includeDestroyed,
39
+ });
40
+ const httpStatus = response?.status;
41
+ const state = classifyOperationPoll({ ok, httpStatus, operation });
42
+ if (state === 'completed') {
43
+ if (logStreamCleanup)
44
+ logStreamCleanup();
45
+ log('');
46
+ return { type: 'completed' };
47
+ }
48
+ if (state === 'failed') {
49
+ if (logStreamCleanup)
50
+ logStreamCleanup();
51
+ log('');
52
+ return { type: 'failed', logHints: logHints.getSuggestions() };
53
+ }
54
+ // tolerate a few consecutive errors; the operation was accepted and may still be running
55
+ if (state === 'error') {
56
+ consecutiveErrors += 1;
57
+ log.verbose(`Could not read ${progressNoun} status (HTTP ${httpStatus ?? 'unknown'}${pollError ? `: ${pollError}` : ''}); retrying (${consecutiveErrors}/${OPERATION_POLL_MAX_CONSECUTIVE_ERRORS}).`);
58
+ if (consecutiveErrors >= OPERATION_POLL_MAX_CONSECUTIVE_ERRORS) {
59
+ if (logStreamCleanup)
60
+ logStreamCleanup();
61
+ log('');
62
+ return { type: 'unconfirmed', error: pollError ?? undefined };
63
+ }
64
+ }
65
+ else {
66
+ consecutiveErrors = 0;
67
+ if (httpStatus === 404) {
68
+ log.verbose(`Waiting for ${progressNoun} status (HTTP 404); retrying.`);
69
+ }
70
+ else if (operation && operation.status !== lastStatus) {
71
+ log.verbose(`${progressNoun} operation status: ${operation.status}.`);
72
+ lastStatus = operation.status;
73
+ }
74
+ }
75
+ if (!idleMessageShown && Date.now() - lastLogAt > 60_000) {
76
+ log(`No new activity for 60 seconds. The ${progressNoun} is still running on Sanity servers.`);
77
+ log(`You can safely exit and check status later with \`npx ${bin} blueprints info\`.`);
78
+ idleMessageShown = true;
79
+ }
80
+ await sleep(OPERATION_POLL_INTERVAL_MS);
81
+ }
82
+ }
83
+ catch (error) {
84
+ if (logStreamCleanup)
85
+ logStreamCleanup();
86
+ throw error;
87
+ }
88
+ }
@@ -136,6 +136,7 @@ export class RuntimeCommand extends Command {
136
136
  suggestions: result.suggestions,
137
137
  ref: result.ref,
138
138
  code: result.code,
139
+ exit: result.exitCode,
139
140
  });
140
141
  }
141
142
  /**
@@ -14,7 +14,9 @@ Use --no-wait to queue the deployment and return immediately without waiting for
14
14
 
15
15
  Use --fn-installer to force which package manager to use when deploying functions.
16
16
 
17
- Set SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.`;
17
+ Set SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.
18
+
19
+ Exit codes: 0 deployed, 2 deployment failed, 75 deployment accepted but completion could not be confirmed (rerun 'blueprints info' to check).`;
18
20
  static examples = [
19
21
  '<%= config.bin %> <%= command.id %>',
20
22
  '<%= config.bin %> <%= command.id %> --message "Enable staging dataset"',
@@ -9,7 +9,9 @@ export default class DestroyCommand extends ResolvedCommand {
9
9
 
10
10
  This is a destructive operation. You will be prompted to confirm unless --force is specified.
11
11
 
12
- Use this to clean up test environments or decommission a Stack you no longer need.`;
12
+ Use this to clean up test environments or decommission a Stack you no longer need.
13
+
14
+ Exit codes: 0 destroyed, 2 destruction failed, 75 destruction accepted but completion could not be confirmed (rerun 'blueprints info' to check).`;
13
15
  static examples = [
14
16
  '<%= config.bin %> <%= command.id %>',
15
17
  '<%= config.bin %> <%= command.id %> --stack <name-or-id> --project-id <projectId> --force --no-wait',
@@ -1,10 +1,10 @@
1
- import { setTimeout as sleep } from 'node:timers/promises';
2
1
  import { stashAsset } from '../../actions/blueprints/assets.js';
3
- import { setupLogPolling } from '../../actions/blueprints/logs-polling.js';
4
2
  import { getStack, updateStack } from '../../actions/blueprints/stacks.js';
3
+ import { waitForOperation } from '../../actions/blueprints/wait-for-operation.js';
5
4
  import { checkUserPermission } from '../../actions/sanity/access.js';
6
5
  import { createHintCollector } from '../../utils/blueprints/hints.js';
7
6
  import { niceId } from '../../utils/display/presenters.js';
7
+ import { CODE_OPERATION_UNCONFIRMED, EXIT_OPERATION_UNCONFIRMED } from '../../utils/exit-codes.js';
8
8
  import { styleText } from '../../utils/style-text.js';
9
9
  import { isAssetResource } from '../../utils/types.js';
10
10
  const DEFAULT_ASSET_TIMEOUT = 60;
@@ -91,90 +91,55 @@ export async function blueprintDeployCore(options) {
91
91
  const legacyWarning = await legacyPermissionsNoticePromise;
92
92
  return {
93
93
  success: true,
94
- json: { stackId: stack.id, resources },
94
+ json: { stackId, resources },
95
95
  data: { resources },
96
96
  warnings: legacyWarning ? [legacyWarning] : undefined,
97
97
  };
98
98
  }
99
99
  log(styleText('dim', 'Stack deployment progress:'));
100
100
  log('');
101
- const logHints = createHintCollector(bin);
102
- let logStreamCleanup = null;
103
- try {
104
- let lastLogAt = Date.now();
105
- let idleMessageShown = false;
106
- logStreamCleanup = setupLogPolling({
107
- stackId: stack.id,
108
- operationId: stack.operationId,
109
- auth,
110
- log,
111
- verbose,
112
- onActivity: () => {
113
- lastLogAt = Date.now();
114
- },
115
- onLogEntry: (logEntry) => logHints.inspectLog(logEntry),
116
- });
117
- while (true) {
118
- const { ok, stack: currentStack } = await getStack({ stackId: stack.id, auth, logger: log });
119
- if (!ok) {
120
- if (logStreamCleanup)
121
- logStreamCleanup();
122
- return {
123
- success: false,
124
- error: 'Failed to check Stack deployment status',
125
- suggestions: [
126
- 'The deployment may still be running on Sanity servers.',
127
- `Run \`npx ${bin} blueprints info\` to check status.`,
128
- ],
129
- };
130
- }
131
- const operation = currentStack.recentOperation;
132
- if (!operation) {
133
- if (logStreamCleanup)
134
- logStreamCleanup();
135
- return { success: false, error: 'No Stack deployment operation found' };
136
- }
137
- if (operation.status === 'FAILED') {
138
- if (logStreamCleanup)
139
- logStreamCleanup();
140
- log('');
141
- return {
142
- success: false,
143
- error: 'Stack deployment failed',
144
- suggestions: [
145
- ...logHints.getSuggestions(),
146
- 'Review the deployment output above for error details.',
147
- `Run \`npx ${bin} blueprints logs --verbose\` for more context.`,
148
- `Run \`npx ${bin} blueprints plan\` to identify issues with your Blueprint.`,
149
- ],
150
- };
151
- }
152
- if (operation.status === 'COMPLETED') {
153
- if (logStreamCleanup)
154
- logStreamCleanup();
155
- log('');
156
- log(styleText(['bold', 'green'], 'Stack deployment completed!'));
157
- const legacyWarning = await legacyPermissionsNoticePromise;
158
- return {
159
- success: true,
160
- json: { stackId: stack.id, resources },
161
- data: { resources },
162
- warnings: legacyWarning ? [legacyWarning] : undefined,
163
- };
164
- }
165
- if (!idleMessageShown && Date.now() - lastLogAt > 60_000) {
166
- log(`No new activity for 60 seconds. The deployment is still running on Sanity servers.`);
167
- log(`You can safely exit and check status later with \`npx ${bin} blueprints info\`.`);
168
- idleMessageShown = true;
169
- }
170
- await sleep(1500);
171
- }
101
+ const outcome = await waitForOperation({
102
+ stackId,
103
+ operationId: stack.operationId,
104
+ auth,
105
+ log,
106
+ bin,
107
+ verbose,
108
+ progressNoun: 'deployment',
109
+ });
110
+ if (outcome.type === 'completed') {
111
+ log(styleText(['bold', 'green'], 'Stack deployment completed!'));
112
+ const legacyWarning = await legacyPermissionsNoticePromise;
113
+ return {
114
+ success: true,
115
+ json: { stackId, resources },
116
+ data: { resources },
117
+ warnings: legacyWarning ? [legacyWarning] : undefined,
118
+ };
172
119
  }
173
- catch (error) {
174
- if (logStreamCleanup)
175
- logStreamCleanup();
176
- throw error;
120
+ if (outcome.type === 'unconfirmed') {
121
+ return {
122
+ success: false,
123
+ error: `Stack deployment was accepted but completion could not be confirmed${outcome.error ? ` (${outcome.error})` : ''}.`,
124
+ code: CODE_OPERATION_UNCONFIRMED,
125
+ exitCode: EXIT_OPERATION_UNCONFIRMED,
126
+ suggestions: [
127
+ 'The deployment may or may not have finished on Sanity servers.',
128
+ `Run \`npx ${bin} blueprints info\` to check status.`,
129
+ `Run \`npx ${bin} blueprints logs --watch\` to keep streaming logs.`,
130
+ ],
131
+ };
177
132
  }
133
+ return {
134
+ success: false,
135
+ error: 'Stack deployment failed',
136
+ suggestions: [
137
+ ...outcome.logHints,
138
+ 'Review the deployment output above for error details.',
139
+ `Run \`npx ${bin} blueprints logs --verbose\` for more context.`,
140
+ `Run \`npx ${bin} blueprints plan\` to identify issues with your Blueprint.`,
141
+ ],
142
+ };
178
143
  }
179
144
  catch (error) {
180
145
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -1,14 +1,14 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
2
  import { confirm } from '@inquirer/prompts';
3
3
  import { patchConfigFile } from '../../actions/blueprints/config.js';
4
- import { setupLogPolling } from '../../actions/blueprints/logs-polling.js';
5
4
  import { destroyStack, getStack, resolveStackIdByNameOrId } from '../../actions/blueprints/stacks.js';
6
- import { createHintCollector } from '../../utils/blueprints/hints.js';
5
+ import { waitForOperation } from '../../actions/blueprints/wait-for-operation.js';
7
6
  import { niceId } from '../../utils/display/presenters.js';
7
+ import { CODE_OPERATION_UNCONFIRMED, EXIT_OPERATION_UNCONFIRMED } from '../../utils/exit-codes.js';
8
8
  import { styleText } from '../../utils/style-text.js';
9
9
  export async function blueprintDestroyCore(options) {
10
10
  const { bin = 'sanity', log, token, blueprint, flags } = options;
11
- const { force = false, 'project-id': flagProjectId, 'organization-id': flagOrganizationId, stack: flagStack, 'no-wait': noWait = false, verbose: _verbose = false, } = flags;
11
+ const { force = false, 'project-id': flagProjectId, 'organization-id': flagOrganizationId, stack: flagStack, 'no-wait': noWait = false, verbose = false, } = flags;
12
12
  // 3-flag combo: destroy without needing a local blueprint config
13
13
  if ((flagProjectId || flagOrganizationId) && flagStack && force) {
14
14
  let scopeType;
@@ -45,6 +45,7 @@ export async function blueprintDestroyCore(options) {
45
45
  auth,
46
46
  log,
47
47
  bin,
48
+ verbose,
48
49
  });
49
50
  }
50
51
  const { scopeType, scopeId, stackId } = blueprint;
@@ -126,6 +127,7 @@ export async function blueprintDestroyCore(options) {
126
127
  auth,
127
128
  log,
128
129
  bin,
130
+ verbose,
129
131
  });
130
132
  }
131
133
  catch (error) {
@@ -147,62 +149,44 @@ function clearLocalStackIdFromConfig(blueprint, destroyedStackId, log) {
147
149
  }
148
150
  }
149
151
  async function waitForDestruction(options) {
150
- const { stackId, stackName, operationId, auth, log, bin } = options;
152
+ const { stackId, stackName, operationId, auth, log, bin, verbose = false } = options;
151
153
  log(styleText('dim', 'Stack destruction progress:'));
152
154
  log('');
153
- const logHints = createHintCollector(bin);
154
- let logStreamCleanup = null;
155
- try {
156
- let lastLogAt = Date.now();
157
- let idleMessageShown = false;
158
- logStreamCleanup = setupLogPolling({
159
- stackId,
160
- operationId,
161
- auth,
162
- log,
163
- onActivity: () => {
164
- lastLogAt = Date.now();
165
- },
166
- onLogEntry: (logEntry) => logHints.inspectLog(logEntry),
167
- });
168
- while (true) {
169
- const { ok, stack: currentStack } = await getStack({ stackId, auth, logger: log });
170
- const operation = currentStack?.recentOperation;
171
- if (!ok || !operation || operation?.status === 'COMPLETED') {
172
- // Operation is also marked destroyed when stack is deleted;
173
- // it's possible that the operation is "gone" or available and "COMPLETED"
174
- if (logStreamCleanup)
175
- logStreamCleanup();
176
- log('');
177
- log(styleText(['bold', 'magenta'], 'Stack destruction completed!'));
178
- return { success: true, json: { stackId, stackName } };
179
- }
180
- if (operation.status === 'FAILED') {
181
- if (logStreamCleanup)
182
- logStreamCleanup();
183
- log('');
184
- return {
185
- success: false,
186
- error: 'Stack destruction failed',
187
- suggestions: [
188
- ...logHints.getSuggestions(),
189
- 'Review the destruction output above for error details.',
190
- `Run \`npx ${bin} blueprints logs --verbose\` for more context.`,
191
- `Run \`npx ${bin} blueprints info\` to view current Stack status.`,
192
- ],
193
- };
194
- }
195
- if (!idleMessageShown && Date.now() - lastLogAt > 60_000) {
196
- log(`No new activity for 60 seconds. The destruction is still running on Sanity servers.`);
197
- log(`You can safely exit and check status later with \`npx ${bin} blueprints info\`.`);
198
- idleMessageShown = true;
199
- }
200
- await sleep(1500);
201
- }
155
+ const outcome = await waitForOperation({
156
+ stackId,
157
+ operationId,
158
+ auth,
159
+ log,
160
+ bin,
161
+ verbose,
162
+ includeDestroyed: true,
163
+ progressNoun: 'destruction',
164
+ });
165
+ if (outcome.type === 'completed') {
166
+ log(styleText(['bold', 'magenta'], 'Stack destruction completed!'));
167
+ return { success: true, json: { stackId, stackName } };
202
168
  }
203
- catch (error) {
204
- if (logStreamCleanup)
205
- logStreamCleanup();
206
- throw error;
169
+ if (outcome.type === 'unconfirmed') {
170
+ return {
171
+ success: false,
172
+ error: `Stack destruction was accepted but completion could not be confirmed${outcome.error ? ` (${outcome.error})` : ''}.`,
173
+ code: CODE_OPERATION_UNCONFIRMED,
174
+ exitCode: EXIT_OPERATION_UNCONFIRMED,
175
+ suggestions: [
176
+ 'The destruction may or may not have finished on Sanity servers.',
177
+ `Run \`npx ${bin} blueprints info\` to view current Stack status.`,
178
+ `Run \`npx ${bin} blueprints logs --watch\` to keep streaming logs.`,
179
+ ],
180
+ };
207
181
  }
182
+ return {
183
+ success: false,
184
+ error: 'Stack destruction failed',
185
+ suggestions: [
186
+ ...outcome.logHints,
187
+ 'Review the destruction output above for error details.',
188
+ `Run \`npx ${bin} blueprints logs --verbose\` for more context.`,
189
+ `Run \`npx ${bin} blueprints info\` to view current Stack status.`,
190
+ ],
191
+ };
208
192
  }
@@ -40,6 +40,8 @@ export type CoreResult = {
40
40
  ref?: string;
41
41
  /** A machine-readable error code. */
42
42
  code?: string;
43
+ /** Overrides the process exit code (defaults to oclif's 2). */
44
+ exitCode?: number;
43
45
  streaming?: never;
44
46
  json?: never;
45
47
  } | {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Process exit codes used by the CLI.
3
+ *
4
+ * oclif exits 0 on success and 2 on error by default. Codes defined here are
5
+ * deliberate, documented signals for scripts/CI to branch on.
6
+ */
7
+ /**
8
+ * A deploy/destroy was accepted but the CLI could not confirm completion
9
+ * (e.g. the operation status endpoint was unavailable). The operation may or
10
+ * may not have finished; rerun `blueprints info` to check. Mirrors EX_TEMPFAIL
11
+ * from sysexits.h ("temporary failure; the user is invited to retry").
12
+ */
13
+ export declare const EXIT_OPERATION_UNCONFIRMED = 75;
14
+ /** Machine-readable error code paired with {@link EXIT_OPERATION_UNCONFIRMED}. */
15
+ export declare const CODE_OPERATION_UNCONFIRMED = "OPERATION_UNCONFIRMED";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Process exit codes used by the CLI.
3
+ *
4
+ * oclif exits 0 on success and 2 on error by default. Codes defined here are
5
+ * deliberate, documented signals for scripts/CI to branch on.
6
+ */
7
+ /**
8
+ * A deploy/destroy was accepted but the CLI could not confirm completion
9
+ * (e.g. the operation status endpoint was unavailable). The operation may or
10
+ * may not have finished; rerun `blueprints info` to check. Mirrors EX_TEMPFAIL
11
+ * from sysexits.h ("temporary failure; the user is invited to retry").
12
+ */
13
+ export const EXIT_OPERATION_UNCONFIRMED = 75;
14
+ /** Machine-readable error code paired with {@link EXIT_OPERATION_UNCONFIRMED}. */
15
+ export const CODE_OPERATION_UNCONFIRMED = 'OPERATION_UNCONFIRMED';
@@ -373,7 +373,7 @@
373
373
  "blueprints:deploy": {
374
374
  "aliases": [],
375
375
  "args": {},
376
- "description": "Applies your local Blueprint to the remote Stack, creating, updating, or removing resources as needed. This is the primary command for applying infrastructure changes.\n\nBefore deploying, run 'blueprints plan' to preview changes. After deployment, use 'blueprints info' to verify Stack status or 'blueprints logs' to monitor activity.\n\nUse --no-wait to queue the deployment and return immediately without waiting for completion.\n\nUse --fn-installer to force which package manager to use when deploying functions.\n\nSet SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.",
376
+ "description": "Applies your local Blueprint to the remote Stack, creating, updating, or removing resources as needed. This is the primary command for applying infrastructure changes.\n\nBefore deploying, run 'blueprints plan' to preview changes. After deployment, use 'blueprints info' to verify Stack status or 'blueprints logs' to monitor activity.\n\nUse --no-wait to queue the deployment and return immediately without waiting for completion.\n\nUse --fn-installer to force which package manager to use when deploying functions.\n\nSet SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.\n\nExit codes: 0 deployed, 2 deployment failed, 75 deployment accepted but completion could not be confirmed (rerun 'blueprints info' to check).",
377
377
  "examples": [
378
378
  "<%= config.bin %> <%= command.id %>",
379
379
  "<%= config.bin %> <%= command.id %> --message \"Enable staging dataset\"",
@@ -523,7 +523,7 @@
523
523
  "blueprints:destroy": {
524
524
  "aliases": [],
525
525
  "args": {},
526
- "description": "Permanently removes the remote Stack and all its provisioned resources. Your Blueprint manifest and resource files remain intact; \"stackId\" is unset in your local config.\n\nThis is a destructive operation. You will be prompted to confirm unless --force is specified.\n\nUse this to clean up test environments or decommission a Stack you no longer need.",
526
+ "description": "Permanently removes the remote Stack and all its provisioned resources. Your Blueprint manifest and resource files remain intact; \"stackId\" is unset in your local config.\n\nThis is a destructive operation. You will be prompted to confirm unless --force is specified.\n\nUse this to clean up test environments or decommission a Stack you no longer need.\n\nExit codes: 0 destroyed, 2 destruction failed, 75 destruction accepted but completion could not be confirmed (rerun 'blueprints info' to check).",
527
527
  "examples": [
528
528
  "<%= config.bin %> <%= command.id %>",
529
529
  "<%= config.bin %> <%= command.id %> --stack <name-or-id> --project-id <projectId> --force --no-wait"
@@ -2998,5 +2998,5 @@
2998
2998
  ]
2999
2999
  }
3000
3000
  },
3001
- "version": "15.1.4"
3001
+ "version": "15.1.5"
3002
3002
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanity/runtime-cli",
3
3
  "description": "Sanity's Runtime CLI for Blueprints and Functions",
4
- "version": "15.1.4",
4
+ "version": "15.1.5",
5
5
  "author": "Sanity Runtime Team",
6
6
  "type": "module",
7
7
  "license": "MIT",