@sanity/runtime-cli 17.11.1 → 17.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +19 -19
  2. package/dist/actions/blueprints/assets.d.ts +21 -20
  3. package/dist/actions/blueprints/assets.js +17 -87
  4. package/dist/actions/blueprints/logs-polling.d.ts +3 -5
  5. package/dist/actions/blueprints/logs-polling.js +4 -9
  6. package/dist/actions/blueprints/logs.d.ts +1 -2
  7. package/dist/actions/blueprints/logs.js +1 -3
  8. package/dist/actions/blueprints/operations-state.d.ts +10 -0
  9. package/dist/actions/blueprints/operations-state.js +42 -0
  10. package/dist/actions/blueprints/operations.d.ts +2 -9
  11. package/dist/actions/blueprints/operations.js +1 -15
  12. package/dist/actions/blueprints/pre-deploy.d.ts +14 -9
  13. package/dist/actions/blueprints/pre-deploy.js +83 -26
  14. package/dist/actions/blueprints/process-assets.d.ts +12 -11
  15. package/dist/actions/blueprints/process-assets.js +8 -7
  16. package/dist/actions/blueprints/stacks.d.ts +4 -4
  17. package/dist/actions/blueprints/stacks.js +2 -4
  18. package/dist/actions/blueprints/wait-for-operation.d.ts +1 -10
  19. package/dist/actions/blueprints/wait-for-operation.js +56 -101
  20. package/dist/baseCommands.js +6 -1
  21. package/dist/cores/blueprints/deploy.js +28 -6
  22. package/dist/cores/blueprints/destroy.js +4 -2
  23. package/dist/cores/blueprints/doctor.js +1 -1
  24. package/dist/cores/blueprints/init.js +3 -1
  25. package/dist/cores/blueprints/logs.js +11 -5
  26. package/dist/cores/functions/build.js +4 -5
  27. package/dist/cores/functions/test.js +33 -21
  28. package/dist/cores/index.js +3 -1
  29. package/dist/server/handlers/invoke.js +1 -1
  30. package/dist/utils/blueprints/resolve-deployed-resources.d.ts +2 -3
  31. package/dist/utils/blueprints/resolve-deployed-resources.js +2 -2
  32. package/dist/utils/blueprints/wait-for.d.ts +60 -0
  33. package/dist/utils/blueprints/wait-for.js +115 -0
  34. package/dist/utils/functions/child-process-wrapper.js +1 -1
  35. package/dist/utils/functions/child-runtime/durable.js +8 -0
  36. package/dist/utils/functions/invoke/assert-console.d.ts +5 -0
  37. package/dist/utils/functions/invoke/assert-console.js +60 -0
  38. package/dist/utils/functions/invoke/local.js +9 -0
  39. package/dist/utils/functions/is-folder-larger-than.d.ts +5 -0
  40. package/dist/utils/functions/is-folder-larger-than.js +38 -0
  41. package/dist/utils/functions/prepare-asset.d.ts +2 -0
  42. package/dist/utils/functions/prepare-asset.js +43 -6
  43. package/dist/utils/studios/prepare-asset.js +5 -3
  44. package/dist/utils/types.d.ts +19 -5
  45. package/dist/utils/types.js +0 -3
  46. package/oclif.manifest.json +1 -1
  47. package/package.json +2 -2
  48. package/dist/utils/functions/get-folder-size.d.ts +0 -5
  49. package/dist/utils/functions/get-folder-size.js +0 -24
@@ -1,15 +1,51 @@
1
+ import { SANITY_FUNCTION_PREFIX } from '../../constants.js';
1
2
  import { resolveDeployedResources } from '../../utils/blueprints/resolve-deployed-resources.js';
2
3
  import { CODE_OPERATION_UNCONFIRMED, EXIT_OPERATION_UNCONFIRMED } from '../../utils/exit-codes.js';
4
+ import { prepareAsset as prepareFunctionAsset } from '../../utils/functions/prepare-asset.js';
3
5
  import { runCommand } from '../../utils/package-manager.js';
4
- import { styleText } from '../../utils/style-text.js';
5
- import { isAssetResource, } from '../../utils/types.js';
6
+ import { prepareStudioAsset } from '../../utils/studios/prepare-asset.js';
7
+ import { setupLogPolling } from './logs-polling.js';
8
+ import { getOperationStateInfo } from './operations-state.js';
6
9
  import { processAssets } from './process-assets.js';
7
10
  import { updateStack } from './stacks.js';
8
- import { waitForOperation } from './wait-for-operation.js';
11
+ /**
12
+ * A list of configurations for pre-deployment of resources. The first resourceType to match in the list will take precedence.
13
+ */
14
+ const preDeployConfigs = [
15
+ {
16
+ resourceType: 'sanity.studio',
17
+ async prepareAsset(options) {
18
+ return prepareStudioAsset({
19
+ resource: options.resource,
20
+ blueprintFileDir: options.blueprintFileDir,
21
+ });
22
+ },
23
+ },
24
+ {
25
+ resourceType: (type) => type.startsWith(SANITY_FUNCTION_PREFIX),
26
+ async prepareAsset(options) {
27
+ return prepareFunctionAsset({
28
+ resource: options.resource,
29
+ blueprintFileDir: options.blueprintFileDir,
30
+ installer: options.installer,
31
+ });
32
+ },
33
+ },
34
+ ];
35
+ function hasPreDeployResources(resources) {
36
+ return resources.some((r) => {
37
+ return !!getPredeployConfig(r);
38
+ });
39
+ }
40
+ function getPredeployConfig(r) {
41
+ return preDeployConfigs.find((c) => {
42
+ return ((typeof c.resourceType === 'function' && c.resourceType(r.type)) ||
43
+ (typeof c.resourceType !== 'function' && c.resourceType === r.type));
44
+ });
45
+ }
9
46
  export async function preDeploy(options) {
10
- const { resources, stackId, auth, blueprintFileDir, installer, logger } = options;
11
- const hasAssetResources = resources.some(isAssetResource);
12
- if (hasAssetResources) {
47
+ const { resources, stackId, auth, blueprintFileDir, installer, fetchFn } = options;
48
+ if (hasPreDeployResources(resources)) {
13
49
  const preDeployStackResult = await preDeployStack(options);
14
50
  if (!preDeployStackResult.success) {
15
51
  return preDeployStackResult;
@@ -19,17 +55,23 @@ export async function preDeploy(options) {
19
55
  resources,
20
56
  stackId,
21
57
  auth,
22
- logger,
58
+ fetchFn,
23
59
  });
24
- const assetResources = resolvedResources.filter(isAssetResource);
25
- let processedAssets;
60
+ const assetResources = [];
61
+ for (const r of resolvedResources) {
62
+ const config = getPredeployConfig(r);
63
+ if (typeof config !== 'undefined' && typeof config.prepareAsset === 'function') {
64
+ assetResources.push({ resource: r, prepareAsset: config.prepareAsset });
65
+ }
66
+ }
67
+ let processedAssets = [];
26
68
  try {
27
69
  processedAssets = await processAssets({
28
70
  assetResources,
29
71
  auth,
30
72
  blueprintFileDir,
31
73
  installer,
32
- logger,
74
+ fetchFn,
33
75
  onProgress: (progress) => options.onAssetEvent?.({ type: 'progress', progress }),
34
76
  onResult: (result) => options.onAssetEvent?.({ type: 'result', result }),
35
77
  });
@@ -52,8 +94,8 @@ export async function preDeploy(options) {
52
94
  return { success: true };
53
95
  }
54
96
  async function preDeployStack(options) {
55
- const { scopeType, scopeId, stackId, stackName, resources, auth, logger, bin, verbose } = options;
56
- const spinner = logger.ora('Starting pre-deploy...').start();
97
+ const { scopeType, scopeId, stackId, stackName, resources, auth, bin, fetchFn, waitForFn } = options;
98
+ options.onPreDeployStackStart?.();
57
99
  const stackMutation = {
58
100
  scopeType,
59
101
  scopeId,
@@ -61,9 +103,9 @@ async function preDeployStack(options) {
61
103
  preDeploy: true,
62
104
  document: { resources },
63
105
  };
64
- const { ok: deployOk, stack, error: deployError, } = await updateStack({ stackId, stackMutation, auth, logger });
106
+ const { ok: deployOk, stack, error: deployError, } = await updateStack({ stackId, stackMutation, auth, fetchFn });
65
107
  if (!deployOk) {
66
- spinner.fail(`${styleText('red', 'Failed')} to complete Stack pre-deployment`);
108
+ options.onPreDeployStackFail?.();
67
109
  return {
68
110
  success: false,
69
111
  error: deployError || 'Failed to complete Stack pre-deployment',
@@ -73,22 +115,37 @@ async function preDeployStack(options) {
73
115
  ],
74
116
  };
75
117
  }
76
- spinner.stop().clear();
118
+ options.onPreDeployStackDone?.();
77
119
  if (stack.changeCount === 0)
78
120
  return { success: true };
79
- logger(styleText('dim', 'Stack deployment progress:'));
80
- logger('');
81
- const outcome = await waitForOperation({
82
- stackId,
83
- operationId: stack.operationId,
84
- auth,
85
- log: logger,
86
- bin,
87
- verbose,
88
- progressNoun: 'pre-deployment',
121
+ options.onPreDeployOperationStart?.();
122
+ const outcome = await waitForFn({
123
+ getIdleMessage() {
124
+ return `The pre-deployment is still running on Sanity servers.`;
125
+ },
126
+ async onRequestLatestState() {
127
+ return await getOperationStateInfo({
128
+ stackId,
129
+ operationId: stack.operationId,
130
+ auth,
131
+ fetchFn,
132
+ includeDestroyed: false,
133
+ progressNoun: 'pre-deployment',
134
+ });
135
+ },
136
+ onStartLogPolling(options) {
137
+ return setupLogPolling({
138
+ auth,
139
+ fetchFn,
140
+ operationId: stack.operationId,
141
+ stackId,
142
+ onActivity: options.onActivity,
143
+ onLogEntry: options.onLogEntry,
144
+ });
145
+ },
89
146
  });
90
147
  if (outcome.type === 'completed') {
91
- logger(styleText(['bold', 'green'], 'Stack pre-deployment completed!'));
148
+ options.onPreDeployOperationDone?.();
92
149
  }
93
150
  else if (outcome.type === 'unconfirmed') {
94
151
  return {
@@ -1,29 +1,30 @@
1
- import type { Logger } from '../../utils/logger.js';
1
+ import type { BlueprintResource } from '@sanity/blueprints';
2
2
  import type { AssetResource, AuthParams, InstallerType } from '../../utils/types.js';
3
- type ProcessAssetsOptions = {
4
- assetResources: AssetResource[];
3
+ type ProcessAssetsOptions<R extends BlueprintResource> = {
4
+ assetResources: AssetResource<R>[];
5
5
  auth: AuthParams;
6
6
  blueprintFileDir: string;
7
7
  installer: InstallerType;
8
- logger: Logger;
8
+ fetchFn: typeof fetch;
9
9
  onProgress?: (progress: ProcessAssetsProgress) => void;
10
- onResult?: (result: ProcessedAsset) => void;
10
+ onResult?: (result: ProcessedAsset<R>) => void;
11
11
  };
12
12
  export type ProcessAssetsProgress = {
13
13
  completed: number;
14
14
  total: number;
15
15
  slow: boolean;
16
16
  };
17
- export type ProcessedAsset = {
17
+ export type ProcessedAsset<R extends BlueprintResource> = {
18
18
  success: false;
19
- resource: AssetResource;
19
+ resource: R;
20
20
  error: string;
21
21
  failureKind: 'processing' | 'upload';
22
22
  } | {
23
23
  success: true;
24
- resource: AssetResource;
25
- hash?: string;
26
- exists?: boolean;
24
+ resource: R;
25
+ assetParameter: string;
26
+ hash: string;
27
+ exists: boolean;
27
28
  };
28
- export declare function processAssets(options: ProcessAssetsOptions): Promise<ProcessedAsset[]>;
29
+ export declare function processAssets<R extends BlueprintResource>(options: ProcessAssetsOptions<R>): Promise<ProcessedAsset<R>[]>;
29
30
  export {};
@@ -21,8 +21,8 @@ export async function processAssets(options) {
21
21
  try {
22
22
  for (let start = 0; start < total; start += concurrency) {
23
23
  const batch = assetResources.slice(start, start + concurrency);
24
- const batchResults = await Promise.all(batch.map(async (resource) => {
25
- const result = await processAsset(resource, options);
24
+ const batchResults = await Promise.all(batch.map(async (assetResource) => {
25
+ const result = await processAsset(assetResource.resource, assetResource.prepareAsset, options);
26
26
  completed += 1;
27
27
  reportProgress();
28
28
  onResult?.(result);
@@ -42,13 +42,13 @@ function getAssetConcurrency() {
42
42
  const configured = Number(env.SANITY_ASSET_CONCURRENCY);
43
43
  return Number.isSafeInteger(configured) && configured > 0 ? configured : DEFAULT_ASSET_CONCURRENCY;
44
44
  }
45
- async function processAsset(resource, options) {
46
- const { auth, blueprintFileDir, installer, logger } = options;
45
+ async function processAsset(resource, prepAsset, options) {
46
+ const { auth, blueprintFileDir, installer, fetchFn } = options;
47
47
  let assetTimeoutTimer;
48
48
  let result;
49
49
  try {
50
50
  result = await Promise.race([
51
- stashAsset({ resource, blueprintFileDir, auth, logger, installer }),
51
+ stashAsset({ resource, prepAsset, blueprintFileDir, auth, fetchFn, installer }),
52
52
  new Promise((_, reject) => {
53
53
  assetTimeoutTimer = setTimeout(() => {
54
54
  reject(new Error(`Processing ${resource.name} timed out after ${assetTimeoutS}s`));
@@ -66,7 +66,8 @@ async function processAsset(resource, options) {
66
66
  if (result.success && result.assetId) {
67
67
  return {
68
68
  success: true,
69
- resource: { ...resource, src: result.assetId },
69
+ resource: { ...resource, [result.assetParameter]: result.assetId },
70
+ assetParameter: result.assetParameter,
70
71
  hash: result.hash,
71
72
  exists: result.exists,
72
73
  };
@@ -74,7 +75,7 @@ async function processAsset(resource, options) {
74
75
  return {
75
76
  success: false,
76
77
  resource,
77
- error: result.error || 'Failed to process asset',
78
+ error: ('error' in result && result.error) || 'Failed to process asset',
78
79
  failureKind: 'upload',
79
80
  };
80
81
  }
@@ -25,10 +25,10 @@ type GetStackResponse = {
25
25
  stack?: unknown;
26
26
  response?: Response;
27
27
  };
28
- export declare function getStack({ stackId, auth, logger, }: {
28
+ export declare function getStack({ stackId, auth, fetchFn, }: {
29
29
  stackId: string;
30
30
  auth: AuthParams;
31
- logger: Logger;
31
+ fetchFn: typeof fetch;
32
32
  }): Promise<GetStackResponse>;
33
33
  type CreateStackResponse = GetStackResponse;
34
34
  export declare function createStack({ stackMutation, auth, logger, }: {
@@ -55,11 +55,11 @@ type UpdateStackResponse = {
55
55
  error: string;
56
56
  stack?: unknown;
57
57
  };
58
- export declare function updateStack({ stackId, stackMutation, auth, logger, }: {
58
+ export declare function updateStack({ stackId, stackMutation, auth, fetchFn, }: {
59
59
  stackId: string;
60
60
  stackMutation: StackMutation;
61
61
  auth: AuthParams;
62
- logger: Logger;
62
+ fetchFn: typeof fetch;
63
63
  }): Promise<UpdateStackResponse>;
64
64
  interface PlanResourceSnapshot {
65
65
  name: string;
@@ -24,8 +24,7 @@ export async function listStacks(auth, logger) {
24
24
  };
25
25
  }
26
26
  }
27
- export async function getStack({ stackId, auth, logger, }) {
28
- const fetchFn = createTracedFetch(logger);
27
+ export async function getStack({ stackId, auth, fetchFn, }) {
29
28
  try {
30
29
  const response = await fetchFn(`${stacksUrl}/${stackId}`, {
31
30
  method: 'GET',
@@ -92,8 +91,7 @@ export async function createEmptyStack({ token, scopeType, scopeId, name, logger
92
91
  }
93
92
  return response.stack;
94
93
  }
95
- export async function updateStack({ stackId, stackMutation, auth, logger, }) {
96
- const fetchFn = createTracedFetch(logger);
94
+ export async function updateStack({ stackId, stackMutation, auth, fetchFn, }) {
97
95
  try {
98
96
  const response = await fetchFn(`${stacksUrl}/${stackId}`, {
99
97
  method: 'PUT',
@@ -1,15 +1,6 @@
1
+ import { type OperationOutcome } from '../../utils/blueprints/wait-for.js';
1
2
  import type { Logger } from '../../utils/logger.js';
2
3
  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
4
  export interface WaitForOperationOptions {
14
5
  stackId: string;
15
6
  operationId: string;
@@ -1,10 +1,7 @@
1
- import { setTimeout as sleep } from 'node:timers/promises';
2
- import { createHintCollector } from '../../utils/blueprints/hints.js';
3
- import { runCommand } from '../../utils/package-manager.js';
1
+ import { createWaitFor } from '../../utils/blueprints/wait-for.js';
2
+ import { createTracedFetch } from '../../utils/traced-fetch.js';
4
3
  import { setupLogPolling } from './logs-polling.js';
5
- import { classifyOperationPoll, getOperation, OPERATION_POLL_INITIAL_DELAY_MS, OPERATION_POLL_INTERVAL_MS, OPERATION_POLL_MAX_CONSECUTIVE_ERRORS, } from './operations.js';
6
- const IDLE_SPINNER_DELAY_MS = 30_000;
7
- const IDLE_SPINNER_UPDATE_INTERVAL_MS = 15_000;
4
+ import { classifyOperationPoll, getOperation } from './operations.js';
8
5
  /**
9
6
  * Stream logs and poll an operation until it settles.
10
7
  * handles log-stream lifecycle, retry budget, and idle messaging;
@@ -12,107 +9,65 @@ const IDLE_SPINNER_UPDATE_INTERVAL_MS = 15_000;
12
9
  */
13
10
  export async function waitForOperation(options) {
14
11
  const { stackId, operationId, auth, log, bin, verbose = false, includeDestroyed, progressNoun, } = options;
15
- const logHints = createHintCollector(bin);
16
- let logStreamCleanup = null;
17
- let idleSpinner;
18
- const stopIdleSpinner = (restoreOperationProgress = false) => {
19
- if (!idleSpinner)
20
- return;
21
- idleSpinner.stop();
22
- idleSpinner = undefined;
23
- if (restoreOperationProgress)
24
- log.showProgress();
25
- };
26
- try {
27
- log.showProgress();
28
- let lastLogAt = Date.now();
29
- let nextIdleSpinnerUpdateAt = IDLE_SPINNER_DELAY_MS;
30
- let consecutiveErrors = 0;
31
- let lastStatus;
32
- logStreamCleanup = setupLogPolling({
33
- stackId,
34
- operationId,
35
- auth,
36
- log,
37
- verbose,
38
- onActivity: () => {
39
- lastLogAt = Date.now();
40
- nextIdleSpinnerUpdateAt = IDLE_SPINNER_DELAY_MS;
41
- stopIdleSpinner(true);
42
- },
43
- onLogEntry: (logEntry) => logHints.inspectLog(logEntry),
44
- });
45
- // balk in case read replica lags
46
- await sleep(OPERATION_POLL_INITIAL_DELAY_MS);
47
- while (true) {
48
- const { ok, error: pollError, operation, response, } = await getOperation({
12
+ const fetchFn = createTracedFetch(log);
13
+ const waitFor = createWaitFor({
14
+ bin,
15
+ log,
16
+ verbose,
17
+ });
18
+ return await waitFor({
19
+ async onRequestLatestState() {
20
+ const { ok, error, operation, response } = await getOperation({
49
21
  stackId,
50
22
  operationId,
51
23
  auth,
52
- logger: log,
24
+ fetchFn,
53
25
  includeDestroyed,
54
26
  });
55
27
  const httpStatus = response?.status;
56
28
  const state = classifyOperationPoll({ ok, httpStatus, operation });
57
- if (state === 'completed') {
58
- if (logStreamCleanup)
59
- logStreamCleanup();
60
- stopIdleSpinner();
61
- log('');
62
- return { type: 'completed' };
29
+ switch (state) {
30
+ case 'completed':
31
+ case 'failed':
32
+ return {
33
+ state,
34
+ };
35
+ case 'error':
36
+ return {
37
+ state,
38
+ error,
39
+ verboseError: `Could not read ${progressNoun} status (HTTP ${httpStatus ?? 'unknown'}${error ? `: ${error}` : ''})`,
40
+ };
41
+ case 'pending':
42
+ if (httpStatus === 404 || !operation) {
43
+ return {
44
+ state: 'not_found',
45
+ verboseError: `Waiting for ${progressNoun} status (HTTP 404); retrying.`,
46
+ };
47
+ }
48
+ else {
49
+ return {
50
+ state,
51
+ latestStatus: operation.status,
52
+ verboseMessage: `${progressNoun} operation status: ${operation.status}.`,
53
+ };
54
+ }
55
+ default:
56
+ throw new Error(`Unexpected poll state: ${state}`);
63
57
  }
64
- if (state === 'failed') {
65
- if (logStreamCleanup)
66
- logStreamCleanup();
67
- stopIdleSpinner();
68
- log('');
69
- return { type: 'failed', logHints: logHints.getSuggestions() };
70
- }
71
- // tolerate a few consecutive errors; the operation was accepted and may still be running
72
- if (state === 'error') {
73
- consecutiveErrors += 1;
74
- log.verbose(`Could not read ${progressNoun} status (HTTP ${httpStatus ?? 'unknown'}${pollError ? `: ${pollError}` : ''}); retrying (${consecutiveErrors}/${OPERATION_POLL_MAX_CONSECUTIVE_ERRORS}).`);
75
- if (consecutiveErrors >= OPERATION_POLL_MAX_CONSECUTIVE_ERRORS) {
76
- if (logStreamCleanup)
77
- logStreamCleanup();
78
- stopIdleSpinner();
79
- log('');
80
- return { type: 'unconfirmed', error: pollError ?? undefined };
81
- }
82
- }
83
- else {
84
- consecutiveErrors = 0;
85
- if (httpStatus === 404) {
86
- log.verbose(`Waiting for ${progressNoun} status (HTTP 404); retrying.`);
87
- }
88
- else if (operation && operation.status !== lastStatus) {
89
- log.verbose(`${progressNoun} operation status: ${operation.status}.`);
90
- lastStatus = operation.status;
91
- }
92
- }
93
- const idleFor = Date.now() - lastLogAt;
94
- if (idleFor >= nextIdleSpinnerUpdateAt) {
95
- const elapsedIdleMs = IDLE_SPINNER_DELAY_MS +
96
- Math.floor((idleFor - IDLE_SPINNER_DELAY_MS) / IDLE_SPINNER_UPDATE_INTERVAL_MS) *
97
- IDLE_SPINNER_UPDATE_INTERVAL_MS;
98
- const elapsedIdleSeconds = elapsedIdleMs / 1000;
99
- const text = `No new activity for ${elapsedIdleSeconds} seconds. The ${progressNoun} is still running on Sanity servers. You can safely exit and check status later with \`${runCommand(bin)} blueprints info\`.`;
100
- if (idleSpinner)
101
- idleSpinner.start(text);
102
- else
103
- idleSpinner = log.ora(text).start();
104
- nextIdleSpinnerUpdateAt = elapsedIdleMs + IDLE_SPINNER_UPDATE_INTERVAL_MS;
105
- }
106
- await sleep(OPERATION_POLL_INTERVAL_MS);
107
- }
108
- }
109
- catch (error) {
110
- if (logStreamCleanup)
111
- logStreamCleanup();
112
- throw error;
113
- }
114
- finally {
115
- stopIdleSpinner();
116
- log.clearProgress();
117
- }
58
+ },
59
+ onStartLogPolling(options) {
60
+ return setupLogPolling({
61
+ stackId,
62
+ operationId,
63
+ auth,
64
+ fetchFn,
65
+ onActivity: options.onActivity,
66
+ onLogEntry: options.onLogEntry,
67
+ });
68
+ },
69
+ getIdleMessage() {
70
+ return `The ${progressNoun} is still running on Sanity servers.`;
71
+ },
72
+ });
118
73
  }
@@ -9,6 +9,7 @@ import { CI_SETUP_DOCS_URL } from './constants.js';
9
9
  import { presentBlueprintParserErrors } from './utils/display/errors.js';
10
10
  import { Logger } from './utils/logger.js';
11
11
  import { styleText } from './utils/style-text.js';
12
+ import { createTracedFetch } from './utils/traced-fetch.js';
12
13
  import { validTokenOrErrorMessage } from './utils/validated-token.js';
13
14
  /**
14
15
  * Fallback error-to-hint patterns for RuntimeCommand.catch().
@@ -378,7 +379,11 @@ export class ResolvedCommand extends RuntimeCommand {
378
379
  // 8. Deployed Stack
379
380
  if (needsDeployedStack) {
380
381
  const spinner = log.ora('Loading Stack deployment...').start();
381
- const response = await getStack({ stackId: this.stackId, auth: this.auth, logger: log });
382
+ const response = await getStack({
383
+ stackId: this.stackId,
384
+ auth: this.auth,
385
+ fetchFn: createTracedFetch(log),
386
+ });
382
387
  if (!response.ok) {
383
388
  spinner.fail('Could not load Stack deployment');
384
389
  this.error('Missing Stack deployment', {
@@ -5,10 +5,12 @@ import { waitForOperation } from '../../actions/blueprints/wait-for-operation.js
5
5
  import { checkUserPermission } from '../../actions/sanity/access.js';
6
6
  import { extractDurableConfig } from '../../utils/blueprints/extract-durable-config.js';
7
7
  import { createHintCollector } from '../../utils/blueprints/hints.js';
8
+ import { createWaitFor } from '../../utils/blueprints/wait-for.js';
8
9
  import { check, niceId, severe } from '../../utils/display/presenters.js';
9
10
  import { CODE_OPERATION_UNCONFIRMED, EXIT_OPERATION_UNCONFIRMED } from '../../utils/exit-codes.js';
10
11
  import { runCommand } from '../../utils/package-manager.js';
11
12
  import { styleText } from '../../utils/style-text.js';
13
+ import { createTracedFetch } from '../../utils/traced-fetch.js';
12
14
  import { getWarnings } from '../../utils/warnings.js';
13
15
  export async function blueprintDeployCore(options) {
14
16
  const { bin = 'sanity', log: logger, auth, stackId, scopeType, scopeId, deployedStack, blueprint, flags, } = options;
@@ -20,7 +22,8 @@ export async function blueprintDeployCore(options) {
20
22
  const workspaceWarnings = getWarnings({ dir: blueprintFileDir });
21
23
  logger(`Deploying "${stackName}" ${niceId(deployedStack.id)}...`);
22
24
  try {
23
- const { ok: checkOk, stack: currentStack } = await getStack({ stackId, auth, logger });
25
+ const fetchFn = createTracedFetch(logger);
26
+ const { ok: checkOk, stack: currentStack } = await getStack({ stackId, auth, fetchFn });
24
27
  if (!checkOk) {
25
28
  return { success: false, error: 'Failed to check current Stack status' };
26
29
  }
@@ -37,6 +40,7 @@ export async function blueprintDeployCore(options) {
37
40
  }
38
41
  const resources = [...blueprint.resources];
39
42
  await extractDurableConfig(resources, blueprintFileDir);
43
+ let spinner;
40
44
  const preDeployResult = await preDeploy({
41
45
  scopeType,
42
46
  scopeId,
@@ -46,10 +50,26 @@ export async function blueprintDeployCore(options) {
46
50
  auth,
47
51
  blueprintFileDir,
48
52
  installer,
49
- logger,
50
53
  bin,
51
- verbose,
54
+ fetchFn,
55
+ waitForFn: createWaitFor({ bin, log: logger, verbose }),
52
56
  onAssetEvent: createAssetEventHandler(logger),
57
+ onPreDeployStackStart() {
58
+ spinner = logger.ora('Starting pre-deploy...').start();
59
+ },
60
+ onPreDeployStackFail() {
61
+ spinner.fail(`${styleText('red', 'Failed')} to complete Stack pre-deployment`);
62
+ },
63
+ onPreDeployStackDone() {
64
+ spinner.stop().clear();
65
+ },
66
+ onPreDeployOperationStart() {
67
+ logger(styleText('dim', 'Stack pre-deployment progress:'));
68
+ logger('');
69
+ },
70
+ onPreDeployOperationDone() {
71
+ logger(styleText(['bold', 'green'], 'Stack pre-deployment completed!'));
72
+ },
53
73
  });
54
74
  if (!preDeployResult?.success)
55
75
  return preDeployResult;
@@ -64,7 +84,7 @@ export async function blueprintDeployCore(options) {
64
84
  userMessage: flags.message,
65
85
  },
66
86
  auth,
67
- logger,
87
+ fetchFn,
68
88
  });
69
89
  if (!deployOk) {
70
90
  deploySpinner.fail(`${styleText('red', 'Failed')} to update Stack deployment`);
@@ -192,8 +212,10 @@ function createAssetEventHandler(logger) {
192
212
  logger.error(` ${result.error}`);
193
213
  continue;
194
214
  }
195
- logger(` ${check(`${resource.name} ${niceId(resource.src)}`)}`);
196
- logger(` Source: ${resource.src}`);
215
+ const r = resource;
216
+ const assetParameter = String(r[result.assetParameter]);
217
+ logger(` ${check(`${resource.name} ${niceId(assetParameter)}`)}`);
218
+ logger(` Source: ${assetParameter}`);
197
219
  if (result.hash) {
198
220
  const hash = result.hash;
199
221
  const h = hash.length > 24 ? `${hash.slice(0, 8)}...${hash.slice(-12)}` : hash;
@@ -7,6 +7,7 @@ import { niceId } from '../../utils/display/presenters.js';
7
7
  import { CODE_OPERATION_UNCONFIRMED, EXIT_OPERATION_UNCONFIRMED } from '../../utils/exit-codes.js';
8
8
  import { runCommand } from '../../utils/package-manager.js';
9
9
  import { styleText } from '../../utils/style-text.js';
10
+ import { createTracedFetch } from '../../utils/traced-fetch.js';
10
11
  export async function blueprintDestroyCore(options) {
11
12
  const { bin = 'sanity', log, token, scopeType, scopeId, localStackId, blueprintConfig, flags, } = options;
12
13
  const { force = false, 'project-id': flagProjectId, 'organization-id': flagOrganizationId, stack: flagStack, 'no-wait': noWait = false, verbose = false, } = flags;
@@ -48,14 +49,15 @@ export async function blueprintDestroyCore(options) {
48
49
  : undefined;
49
50
  let stack;
50
51
  try {
52
+ const fetchFn = createTracedFetch(log);
51
53
  if (resolvedStackId) {
52
- const overrideStack = await getStack({ stackId: resolvedStackId, auth, logger: log });
54
+ const overrideStack = await getStack({ stackId: resolvedStackId, auth, fetchFn });
53
55
  if (!overrideStack.ok)
54
56
  return { success: false, error: overrideStack.error || 'Failed to get Stack' };
55
57
  stack = overrideStack.stack;
56
58
  }
57
59
  else if (localStackId) {
58
- const blueprintStack = await getStack({ stackId: localStackId, auth, logger: log });
60
+ const blueprintStack = await getStack({ stackId: localStackId, auth, fetchFn });
59
61
  if (!blueprintStack.ok)
60
62
  return { success: false, error: blueprintStack.error || 'Failed to get Stack' };
61
63
  stack = blueprintStack.stack;
@@ -172,7 +172,7 @@ export async function blueprintDoctorCore(options) {
172
172
  const stackResponse = await getStack({
173
173
  auth: { token, scopeType, scopeId },
174
174
  stackId,
175
- logger: log,
175
+ fetchFn,
176
176
  });
177
177
  if (stackResponse.ok) {
178
178
  diagnostics.stackReady = { status: true };
@@ -15,6 +15,7 @@ import { CancelledError, promptForBlueprintType, promptForInitLocation, promptFo
15
15
  import { isInteractive } from '../../utils/interactive.js';
16
16
  import { installCommand, runCommand } from '../../utils/package-manager.js';
17
17
  import { styleText } from '../../utils/style-text.js';
18
+ import { createTracedFetch } from '../../utils/traced-fetch.js';
18
19
  import { blueprintConfigCore } from './config.js';
19
20
  /** Derive scope type and ID from mutually exclusive flag values. */
20
21
  function resolveScopeFromFlags(projectId, organizationId) {
@@ -288,11 +289,12 @@ export async function resolveScopeAndStack(params) {
288
289
  }
289
290
  // --stack-id: validate it exists
290
291
  if (resolvedStackId && resolvedStackId === stackId) {
292
+ const fetchFn = createTracedFetch(log);
291
293
  const spinner = log.ora('Validating Stack...').start();
292
294
  const { ok, error } = await getStack({
293
295
  stackId: resolvedStackId,
294
296
  auth: { token, scopeType, scopeId },
295
- logger: log,
297
+ fetchFn,
296
298
  });
297
299
  spinner.stop();
298
300
  if (!ok) {