@sanity/runtime-cli 13.4.1 → 14.0.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 (40) hide show
  1. package/README.md +43 -35
  2. package/dist/actions/blueprints/config.d.ts +3 -7
  3. package/dist/actions/blueprints/config.js +1 -1
  4. package/dist/actions/blueprints/logs-streaming.d.ts +1 -0
  5. package/dist/actions/blueprints/logs-streaming.js +1 -0
  6. package/dist/actions/blueprints/stacks.d.ts +1 -0
  7. package/dist/actions/blueprints/stacks.js +11 -0
  8. package/dist/baseCommands.d.ts +2 -0
  9. package/dist/baseCommands.js +6 -1
  10. package/dist/commands/blueprints/config.d.ts +1 -1
  11. package/dist/commands/blueprints/config.js +5 -5
  12. package/dist/commands/blueprints/deploy.d.ts +1 -0
  13. package/dist/commands/blueprints/deploy.js +5 -2
  14. package/dist/commands/blueprints/destroy.d.ts +1 -1
  15. package/dist/commands/blueprints/destroy.js +6 -6
  16. package/dist/commands/blueprints/doctor.js +5 -1
  17. package/dist/commands/blueprints/info.d.ts +1 -1
  18. package/dist/commands/blueprints/info.js +4 -4
  19. package/dist/commands/blueprints/init.js +1 -1
  20. package/dist/commands/blueprints/logs.d.ts +1 -0
  21. package/dist/commands/blueprints/logs.js +2 -1
  22. package/dist/commands/blueprints/plan.d.ts +3 -0
  23. package/dist/commands/blueprints/plan.js +4 -1
  24. package/dist/commands/functions/logs.d.ts +1 -0
  25. package/dist/commands/functions/logs.js +2 -1
  26. package/dist/cores/blueprints/config.d.ts +1 -1
  27. package/dist/cores/blueprints/config.js +27 -6
  28. package/dist/cores/blueprints/deploy.js +36 -3
  29. package/dist/cores/blueprints/destroy.d.ts +1 -1
  30. package/dist/cores/blueprints/destroy.js +30 -15
  31. package/dist/cores/blueprints/doctor.js +184 -90
  32. package/dist/cores/blueprints/info.d.ts +1 -3
  33. package/dist/cores/blueprints/info.js +5 -20
  34. package/dist/cores/blueprints/init.js +1 -1
  35. package/dist/cores/blueprints/plan.d.ts +1 -0
  36. package/dist/cores/blueprints/plan.js +20 -15
  37. package/dist/cores/index.d.ts +1 -0
  38. package/dist/cores/index.js +12 -7
  39. package/oclif.manifest.json +81 -24
  40. package/package.json +1 -1
@@ -1,202 +1,295 @@
1
- import { cwd } from 'node:process';
1
+ import { readFileSync } from 'node:fs';
2
+ import { arch, cwd, version as nodeVersion, platform } from 'node:process';
3
+ import * as resolve from 'empathic/resolve';
4
+ import ora from 'ora';
2
5
  import { readLocalBlueprint, } from '../../actions/blueprints/blueprint.js';
3
6
  import { getStack } from '../../actions/blueprints/stacks.js';
4
- import config from '../../config.js';
5
- import { capitalize, check, filePathRelativeToCwd, indent, niceId, severe, unsure, } from '../../utils/display/presenters.js';
7
+ import config, { RUNTIME_CLI_VERSION } from '../../config.js';
8
+ import { check, filePathRelativeToCwd, niceId, severe, unsure, } from '../../utils/display/presenters.js';
6
9
  import { styleText } from '../../utils/style-text.js';
7
10
  import { createTracedFetch } from '../../utils/traced-fetch.js';
8
11
  import { validTokenOrErrorMessage } from '../../utils/validated-token.js';
9
12
  import { blueprintConfigCore } from './config.js';
10
13
  const diagLookup = {
11
- online: 'Online',
14
+ online: 'Host online',
12
15
  tokenValid: 'Authenticated',
13
16
  blueprintValid: 'Blueprint valid',
14
17
  stackReady: 'Stack ready',
15
18
  userHasAccess: 'User has access',
16
19
  };
20
+ function sourceLabel(source) {
21
+ switch (source) {
22
+ case 'env':
23
+ return 'environment';
24
+ case 'module':
25
+ return 'blueprint module';
26
+ case 'config':
27
+ return 'config file';
28
+ case 'inferred':
29
+ return 'inferred';
30
+ default:
31
+ return 'unknown';
32
+ }
33
+ }
34
+ function renderSection(emit, title, rows) {
35
+ const pad = Math.max(...rows.map(([l]) => l.length)) + 3;
36
+ emit(styleText('bold', title));
37
+ for (const [label, value] of rows) {
38
+ emit(` ${styleText('dim', label.padEnd(pad))}${value}`);
39
+ }
40
+ }
17
41
  export async function blueprintDoctorCore(options) {
18
42
  const { bin, log, token, validateResources, flags: { verbose: v, path: p, fix }, } = options;
19
- const yikes = (s) => {
20
- log.error(styleText(['bgRedBright', 'whiteBright', 'bold'], ` ${s} `));
21
- };
22
- const here = cwd();
23
- const path = p || here;
43
+ const path = p || cwd();
24
44
  let tokenOrError;
25
- log.verbose(`Checking ${filePathRelativeToCwd(path)}`);
26
- // 3 states: null == unknown, true == good, false == bad
27
45
  const diagnostics = {};
28
46
  for (const key in diagLookup) {
29
- diagnostics[key] = null;
47
+ diagnostics[key] = { status: null };
30
48
  }
31
- // ONLINE
49
+ const envRows = [['Directory', p ? filePathRelativeToCwd(path) : path]];
50
+ const configRows = [];
51
+ const stackRows = [];
52
+ const spinner = ora('Running diagnostics...').start();
53
+ // --- ONLINE ---
32
54
  const fetchFn = createTracedFetch(log);
33
55
  try {
34
56
  const res = await fetchFn(config.apiUrl);
35
57
  if (res.ok) {
36
- log.verbose(`Successfully pinged ${config.apiUrl}`);
37
- diagnostics.online = res.ok;
58
+ diagnostics.online = { status: true };
38
59
  }
39
60
  else {
40
- yikes(`Failed to ping ${config.apiUrl}: ${res.status} ${res.statusText}`);
41
- diagnostics.online = false;
61
+ diagnostics.online = { status: false, detail: `${res.status} ${res.statusText}` };
42
62
  }
43
63
  }
44
- catch {
45
- yikes(`Failed to ping ${config.apiUrl}`);
64
+ catch (err) {
65
+ const reason = err instanceof Error ? err.message : 'unknown error';
66
+ diagnostics.online = { status: false, detail: reason };
46
67
  }
47
- // TOKEN
68
+ // --- TOKEN ---
48
69
  if (token) {
49
70
  tokenOrError = await validTokenOrErrorMessage(log, token);
50
71
  if (tokenOrError.ok) {
51
- diagnostics.tokenValid = true;
72
+ diagnostics.tokenValid = { status: true };
52
73
  }
53
74
  else {
54
- yikes(`Token error: ${tokenOrError.error.message}`);
55
- diagnostics.tokenValid = false;
75
+ diagnostics.tokenValid = { status: false, detail: tokenOrError.error.message };
56
76
  }
57
77
  }
58
78
  else {
59
- yikes('No authentication token found');
60
- diagnostics.tokenValid = false;
79
+ diagnostics.tokenValid = { status: false, detail: 'no token found' };
61
80
  }
62
- // BLUEPRINT file
81
+ // --- BLUEPRINT ---
63
82
  let localBlueprint;
64
83
  try {
65
84
  localBlueprint = await readLocalBlueprint(log, { resources: options.validateResources || false }, path);
66
- log.verbose(`Found blueprint file at ${filePathRelativeToCwd(localBlueprint.fileInfo.blueprintFilePath)}`);
85
+ envRows.push(['Blueprint', filePathRelativeToCwd(localBlueprint.fileInfo.blueprintFilePath)]);
67
86
  if (localBlueprint.errors.length === 0) {
68
- log.verbose(`Blueprint has no errors`);
69
- diagnostics.blueprintValid = true;
87
+ diagnostics.blueprintValid = { status: true };
70
88
  }
71
89
  else {
72
- log.verbose(`Blueprint errors: \n${localBlueprint.errors.join('\n ')}`);
73
- diagnostics.blueprintValid = false;
90
+ diagnostics.blueprintValid = {
91
+ status: false,
92
+ detail: `${localBlueprint.errors.length} error(s)`,
93
+ };
74
94
  }
75
95
  }
76
96
  catch {
77
- yikes(`Unable to read blueprint`);
78
- diagnostics.blueprintValid = false;
97
+ diagnostics.blueprintValid = { status: false, detail: 'unable to read file' };
79
98
  }
99
+ envRows.push(['API URL', config.apiUrl]);
100
+ envRows.push(['Runtime', `Node.js ${nodeVersion} (${platform}-${arch})`]);
101
+ if (RUNTIME_CLI_VERSION) {
102
+ envRows.push(['Internals', `v${RUNTIME_CLI_VERSION}`]);
103
+ }
104
+ const sanityCliPkgPath = resolve.from(path, '@sanity/cli/package.json', true);
105
+ if (sanityCliPkgPath) {
106
+ try {
107
+ const sanityCliPkg = JSON.parse(readFileSync(sanityCliPkgPath, 'utf8'));
108
+ envRows.push(['Sanity CLI', sanityCliPkg.version]);
109
+ }
110
+ catch { }
111
+ }
112
+ // --- CONFIGURATION ---
80
113
  if (localBlueprint) {
81
- const { scopeType, scopeId, stackId, sources } = localBlueprint;
82
- const sourceLabel = (source) => {
83
- switch (source) {
84
- case 'env':
85
- return 'environment';
86
- case 'module':
87
- return 'blueprint module';
88
- case 'config':
89
- return 'config file';
90
- case 'inferred':
91
- return 'inferred';
92
- default:
93
- return 'unknown';
94
- }
95
- };
114
+ const { scopeType, scopeId, stackId, sources, blueprintConfig } = localBlueprint;
115
+ if (blueprintConfig && 'configPath' in blueprintConfig && blueprintConfig.configPath) {
116
+ configRows.push(['Source', filePathRelativeToCwd(blueprintConfig.configPath)]);
117
+ }
118
+ else if (!blueprintConfig) {
119
+ configRows.push(['Source', styleText('dim', 'no config file')]);
120
+ }
121
+ if (blueprintConfig?.updatedAt) {
122
+ configRows.push(['Updated', new Date(blueprintConfig.updatedAt).toLocaleString('sv-SE')]);
123
+ }
96
124
  if (scopeType && scopeId) {
97
125
  const scopeSourceKey = scopeType === 'project' ? 'projectId' : 'organizationId';
98
- log.verbose(indent(`${capitalize(scopeType)}: ${niceId(scopeId)} (from ${sourceLabel(sources?.[scopeSourceKey])})`));
126
+ const label = scopeType === 'project' ? 'Project' : 'Organization';
127
+ configRows.push([
128
+ label,
129
+ `${niceId(scopeId)} ${styleText('dim', sourceLabel(sources?.[scopeSourceKey]))}`,
130
+ ]);
99
131
  }
100
132
  if (stackId) {
101
- log.verbose(indent(`Deployment: ${niceId(stackId)} (from ${sourceLabel(sources?.stackId)})`));
133
+ configRows.push([
134
+ 'Stack',
135
+ `${niceId(stackId)} ${styleText('dim', sourceLabel(sources?.stackId))}`,
136
+ ]);
102
137
  }
103
- // STACK + ACCESS
104
- if (diagnostics.online && diagnostics.tokenValid && token && stackId && scopeType && scopeId) {
138
+ // --- STACK + ACCESS ---
139
+ if (diagnostics.online.status &&
140
+ diagnostics.tokenValid.status &&
141
+ token &&
142
+ stackId &&
143
+ scopeType &&
144
+ scopeId) {
105
145
  const stackResponse = await getStack({
106
146
  auth: { token, scopeType, scopeId },
107
147
  stackId,
108
148
  logger: log,
109
149
  });
110
150
  if (stackResponse.ok) {
111
- log.verbose(`Deployment "Stack" ${niceId(stackId)} ready`);
112
- diagnostics.stackReady = true;
113
- diagnostics.userHasAccess = true;
151
+ diagnostics.stackReady = { status: true };
152
+ diagnostics.userHasAccess = { status: true };
153
+ const stack = stackResponse.stack;
154
+ if (stack) {
155
+ const label = stack.name ? `"${stack.name}" ${niceId(stackId)}` : niceId(stackId);
156
+ stackRows.push(['Stack', label]);
157
+ if (stack.recentOperation) {
158
+ const op = stack.recentOperation;
159
+ const time = op.completedAt || op.createdAt;
160
+ const timestamp = time
161
+ ? ` ${styleText('dim', new Date(time).toLocaleString('sv-SE'))}`
162
+ : '';
163
+ stackRows.push(['Operation', `${op.status}${timestamp}`]);
164
+ }
165
+ }
114
166
  }
115
167
  else if (stackResponse.response?.status === 404) {
116
- yikes(`Deployment "Stack" <${stackId}> not found`);
117
- diagnostics.stackReady = false;
168
+ diagnostics.stackReady = {
169
+ status: false,
170
+ detail: `Stack ${niceId(stackId)} not found (404)`,
171
+ };
118
172
  }
119
173
  else if (stackResponse.response?.status === 403 || stackResponse.response?.status === 401) {
120
- yikes(`User does not have access to "Stack" <${stackId}>`);
121
- diagnostics.userHasAccess = false;
174
+ diagnostics.userHasAccess = {
175
+ status: false,
176
+ detail: `no access to Stack ${niceId(stackId)} (${stackResponse.response.status})`,
177
+ };
122
178
  }
123
179
  else {
124
- yikes(`Unknown error with "Stack" <${stackId}>: ${stackResponse.error}`);
180
+ const statusSuffix = stackResponse.response?.status
181
+ ? ` (${stackResponse.response.status})`
182
+ : '';
183
+ diagnostics.stackReady = {
184
+ status: null,
185
+ detail: (stackResponse.error || 'unknown error') + statusSuffix,
186
+ };
187
+ diagnostics.userHasAccess = { status: null, detail: `unknown error${statusSuffix}` };
125
188
  }
126
189
  }
190
+ else if (stackId && scopeType && scopeId) {
191
+ diagnostics.stackReady = { status: null, detail: 'requires online + authenticated' };
192
+ diagnostics.userHasAccess = { status: null, detail: 'requires online + authenticated' };
193
+ }
194
+ else if (!stackId && !scopeType && !scopeId) {
195
+ diagnostics.stackReady = { status: null, detail: 'missing configuration' };
196
+ diagnostics.userHasAccess = { status: null, detail: 'missing configuration' };
197
+ }
127
198
  else {
199
+ const missing = [];
128
200
  if (!stackId)
129
- yikes(`Blueprints configuration is missing a Stack ID`);
130
- if (scopeType === 'project') {
131
- if (!scopeId)
132
- yikes(`Blueprints configuration is missing a Project ID`);
133
- }
134
- else if (scopeType === 'organization') {
135
- if (!scopeId)
136
- yikes(`Blueprints configuration is missing an Organization ID`);
137
- }
138
- else {
139
- if (!scopeType)
140
- yikes(`Blueprints configuration is missing a Scope Type`);
141
- if (!scopeId)
142
- yikes(`Blueprints configuration is missing a Scope ID`);
201
+ missing.push('Stack ID');
202
+ if (!scopeType)
203
+ missing.push('Scope Type');
204
+ if (!scopeId) {
205
+ const scopeLabel = scopeType === 'project'
206
+ ? 'Project ID'
207
+ : scopeType === 'organization'
208
+ ? 'Organization ID'
209
+ : 'Scope ID';
210
+ missing.push(scopeLabel);
143
211
  }
212
+ const detail = `missing ${missing.join(', ')}`;
213
+ diagnostics.stackReady = { status: null, detail };
214
+ diagnostics.userHasAccess = { status: null, detail };
144
215
  }
145
216
  }
146
- log('');
217
+ // --- RENDER REPORT ---
218
+ spinner.stop();
219
+ // Environment (verbose)
220
+ renderSection((msg) => log.verbose(msg), 'Environment', envRows);
221
+ log.verbose('');
222
+ // Configuration (verbose)
223
+ if (configRows.length > 0) {
224
+ renderSection((msg) => log.verbose(msg), 'Configuration', configRows);
225
+ log.verbose('');
226
+ }
227
+ // Deployment (verbose)
228
+ if (stackRows.length > 0) {
229
+ renderSection((msg) => log.verbose(msg), 'Deployment', stackRows);
230
+ log.verbose('');
231
+ }
232
+ // Checks (always visible)
233
+ const maxLabel = Math.max(...Object.values(diagLookup).map((l) => l.length));
234
+ log(styleText('bold', 'Checks'));
147
235
  let allGood = true;
148
- for (const [key, value] of Object.entries(diagnostics)) {
149
- switch (value) {
236
+ for (const [key, entry] of Object.entries(diagnostics)) {
237
+ const label = diagLookup[key].padEnd(maxLabel);
238
+ const detail = entry.detail ? ` ${styleText('dim', entry.detail)}` : '';
239
+ switch (entry.status) {
150
240
  case true:
151
- log(check(diagLookup[key]));
241
+ log(` ${check(label)}${detail}`);
152
242
  break;
153
243
  case false:
154
244
  allGood = false;
155
- log(severe(diagLookup[key]));
245
+ log(` ${severe(label)}${detail}`);
156
246
  break;
157
247
  case null:
158
248
  allGood = false;
159
- log(unsure(diagLookup[key]));
249
+ log(` ${unsure(label)}${detail}`);
160
250
  break;
161
- default:
162
- allGood = false;
163
- log(severe(`${key} is ${value}`));
164
251
  }
165
252
  }
253
+ // Result
254
+ const flatDiagnostics = {};
255
+ for (const [key, entry] of Object.entries(diagnostics)) {
256
+ flatDiagnostics[key] = entry.status;
257
+ }
258
+ log('');
166
259
  const errorMessage = 'One or more checks failed';
167
260
  if (allGood) {
168
261
  log(styleText(['bold', 'green'], 'All checks passed'));
169
262
  if (fix)
170
263
  log(styleText(['bold', 'yellow'], 'Nothing to fix; --fix flag is ignored'));
171
- return { success: true, data: { diagnostics } };
264
+ return { success: true, data: { diagnostics: flatDiagnostics } };
172
265
  }
173
- else if (fix) {
266
+ if (fix) {
174
267
  if (p) {
175
268
  return {
176
269
  success: false,
177
270
  error: `${errorMessage}. --fix cannot be used with --path`,
178
- data: { diagnostics },
271
+ data: { diagnostics: flatDiagnostics },
179
272
  };
180
273
  }
181
274
  if (!tokenOrError) {
182
275
  return {
183
276
  success: false,
184
277
  error: `${errorMessage}. Unable to fix: Missing authentication token`,
185
- data: { diagnostics },
278
+ data: { diagnostics: flatDiagnostics },
186
279
  };
187
280
  }
188
281
  if (tokenOrError?.ok === false) {
189
282
  return {
190
283
  success: false,
191
284
  error: `${errorMessage}. Unable to fix: ${tokenOrError.error.message}`,
192
- data: { diagnostics },
285
+ data: { diagnostics: flatDiagnostics },
193
286
  };
194
287
  }
195
288
  if (!localBlueprint) {
196
289
  return {
197
290
  success: false,
198
291
  error: `${errorMessage}. Unable to fix: Blueprint is missing or invalid`,
199
- data: { diagnostics },
292
+ data: { diagnostics: flatDiagnostics },
200
293
  };
201
294
  }
202
295
  return blueprintConfigCore({
@@ -208,5 +301,6 @@ export async function blueprintDoctorCore(options) {
208
301
  flags: { edit: true, verbose: v },
209
302
  });
210
303
  }
211
- return { success: false, error: errorMessage, data: { diagnostics } };
304
+ log(styleText('dim', ` Run \`${bin} blueprints doctor --fix\` to resolve configuration issues.`));
305
+ return { success: false, error: errorMessage, data: { diagnostics: flatDiagnostics } };
212
306
  }
@@ -1,11 +1,9 @@
1
- import type { AuthParams, Stack } from '../../utils/types.js';
1
+ import type { Stack } from '../../utils/types.js';
2
2
  import type { CoreConfig, CoreResult } from '../index.js';
3
3
  export interface BlueprintInfoOptions extends CoreConfig {
4
- auth: AuthParams;
5
4
  stackId: string;
6
5
  deployedStack: Stack;
7
6
  flags: {
8
- id?: string;
9
7
  verbose?: boolean;
10
8
  };
11
9
  }
@@ -1,26 +1,11 @@
1
- import { getStack } from '../../actions/blueprints/stacks.js';
2
1
  import { formatDeployedResourceTree, formatStackInfo, } from '../../utils/display/blueprints-formatting.js';
3
- import { niceId } from '../../utils/display/presenters.js';
4
2
  export async function blueprintInfoCore(options) {
5
- const { log, auth, stackId, flags, deployedStack } = options;
6
- const { id: flagStackId, verbose = false } = flags;
3
+ const { log, deployedStack, flags } = options;
4
+ const { verbose = false } = flags;
7
5
  try {
8
- const targetStackId = flagStackId || stackId;
9
- let stack = deployedStack;
10
- if (flagStackId) {
11
- const existingStackResponse = await getStack({ stackId: targetStackId, auth, logger: log });
12
- if (!existingStackResponse.ok) {
13
- log.error(`Could not retrieve Stack deployment info for ${niceId(targetStackId)}`);
14
- return {
15
- success: false,
16
- error: existingStackResponse.error || 'Failed to retrieve Stack deployment',
17
- };
18
- }
19
- stack = existingStackResponse.stack;
20
- }
21
- log(formatStackInfo(stack, true));
22
- if (stack.resources)
23
- log(formatDeployedResourceTree(stack.resources, verbose));
6
+ log(formatStackInfo(deployedStack, true));
7
+ if (deployedStack.resources)
8
+ log(formatDeployedResourceTree(deployedStack.resources, verbose));
24
9
  return { success: true };
25
10
  }
26
11
  catch (error) {
@@ -66,7 +66,7 @@ export async function blueprintInitCore(options) {
66
66
  edit: true,
67
67
  'project-id': flagProjectId,
68
68
  'organization-id': flagOrganizationId,
69
- 'stack-id': flagStackId,
69
+ stack: flagStackId,
70
70
  verbose: v,
71
71
  },
72
72
  });
@@ -4,6 +4,7 @@ export interface BlueprintPlanOptions extends CoreConfig {
4
4
  blueprint: ReadBlueprintResult;
5
5
  token?: string;
6
6
  flags: {
7
+ stack?: string;
7
8
  verbose?: boolean;
8
9
  };
9
10
  }
@@ -1,25 +1,30 @@
1
- import { getStack } from '../../actions/blueprints/stacks.js';
1
+ import { getStack, resolveStackIdByNameOrId } from '../../actions/blueprints/stacks.js';
2
2
  import { formatResourceTree, stackDeployDiff } from '../../utils/display/blueprints-formatting.js';
3
3
  import { styleText } from '../../utils/style-text.js';
4
4
  export async function blueprintPlanCore(options) {
5
5
  const { bin = 'sanity', log, blueprint, token, flags } = options;
6
6
  const { verbose: _verbose = false } = flags;
7
- const { scopeType, scopeId, stackId, parsedBlueprint, fileInfo } = blueprint;
7
+ const { scopeType, scopeId, stackId: blueprintStackId, parsedBlueprint, fileInfo } = blueprint;
8
8
  log(`${styleText(['bold', 'blueBright'], 'Blueprint Stack deployment plan')} ${styleText('dim', `(${fileInfo.fileName})`)}`);
9
9
  log(formatResourceTree(parsedBlueprint.resources));
10
- if (token && scopeType && scopeId && stackId) {
11
- const stackResponse = await getStack({
12
- auth: { token, scopeType, scopeId },
13
- stackId,
14
- logger: log,
15
- });
16
- if (!stackResponse.ok) {
17
- log(styleText('dim', 'Unable to retrieve live Stack deployment for comparison'));
18
- }
19
- else {
20
- const diff = stackDeployDiff(parsedBlueprint, stackResponse.stack);
21
- if (diff)
22
- log(diff);
10
+ if (token && scopeType && scopeId) {
11
+ const stackId = flags.stack
12
+ ? await resolveStackIdByNameOrId(flags.stack, { token, scopeType, scopeId }, log)
13
+ : blueprintStackId;
14
+ if (stackId) {
15
+ const stackResponse = await getStack({
16
+ auth: { token, scopeType, scopeId },
17
+ stackId,
18
+ logger: log,
19
+ });
20
+ if (!stackResponse.ok) {
21
+ log(styleText('dim', 'Unable to retrieve live Stack deployment for comparison'));
22
+ }
23
+ else {
24
+ const diff = stackDeployDiff(parsedBlueprint, stackResponse.stack);
25
+ if (diff)
26
+ log(diff);
27
+ }
23
28
  }
24
29
  }
25
30
  log(`\n Run "${styleText(['bold', 'magenta'], `${bin} blueprints deploy`)}" to deploy these Stack changes`);
@@ -50,4 +50,5 @@ type InitBlueprintConfigParams = CoreConfig & ({
50
50
  export declare function initBlueprintConfig({ bin, log, token, validateResources, validateToken, blueprintPath, }: InitBlueprintConfigParams): Promise<Result<BlueprintConfig>>;
51
51
  export declare function initDeployedBlueprintConfig(config: Partial<BlueprintConfig> & Pick<BlueprintConfig, 'bin' | 'log' | 'token' | 'validateResources'> & {
52
52
  validateToken?: boolean;
53
+ stackOverride?: string;
53
54
  }): Promise<Result<DeployedBlueprintConfig>>;
@@ -1,5 +1,5 @@
1
1
  import { readLocalBlueprint } from '../actions/blueprints/blueprint.js';
2
- import { getStack } from '../actions/blueprints/stacks.js';
2
+ import { getStack, resolveStackIdByNameOrId } from '../actions/blueprints/stacks.js';
3
3
  import { presentBlueprintParserErrors } from '../utils/display/errors.js';
4
4
  import { niceId } from '../utils/display/presenters.js';
5
5
  import { validTokenOrErrorMessage } from '../utils/validated-token.js';
@@ -41,15 +41,20 @@ export async function initDeployedBlueprintConfig(config) {
41
41
  config.blueprint = blueprintResult.value.blueprint;
42
42
  config.token = blueprintResult.value.token;
43
43
  }
44
- const { scopeType, scopeId, stackId } = config.blueprint;
45
- if (!(scopeType && scopeId && stackId)) {
44
+ const { scopeType, scopeId, stackId: blueprintStackId } = config.blueprint;
45
+ if (!scopeType || !scopeId) {
46
46
  config.log(`Incomplete configuration. Run \`${config.bin} blueprints doctor\` for diagnostics.`);
47
- if (!scopeType || !scopeId)
48
- return { ok: false, error: 'Missing scope configuration for Blueprint' };
49
- if (!stackId)
50
- return { ok: false, error: 'Missing Stack deployment configuration for Blueprint' };
47
+ return { ok: false, error: 'Missing scope configuration for Blueprint' };
51
48
  }
52
49
  const auth = { token: config.token, scopeType, scopeId };
50
+ let stackId = blueprintStackId;
51
+ if (config.stackOverride) {
52
+ stackId = await resolveStackIdByNameOrId(config.stackOverride, auth, config.log);
53
+ }
54
+ if (!stackId) {
55
+ config.log(`Incomplete configuration. Run \`${config.bin} blueprints doctor\` for diagnostics.`);
56
+ return { ok: false, error: 'Missing Stack deployment configuration for Blueprint' };
57
+ }
53
58
  const stackResponse = await getStack({ stackId, auth, logger: config.log });
54
59
  if (!stackResponse.ok) {
55
60
  config.log(`Could not retrieve Stack deployment info for ${niceId(stackId)}.`);