@sanity/runtime-cli 14.8.6 → 14.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +43 -35
  2. package/dist/baseCommands.d.ts +21 -8
  3. package/dist/baseCommands.js +54 -15
  4. package/dist/commands/blueprints/add.js +3 -3
  5. package/dist/commands/blueprints/config.js +4 -4
  6. package/dist/commands/blueprints/deploy.d.ts +1 -0
  7. package/dist/commands/blueprints/deploy.js +10 -5
  8. package/dist/commands/blueprints/destroy.js +5 -5
  9. package/dist/commands/blueprints/doctor.d.ts +1 -1
  10. package/dist/commands/blueprints/doctor.js +7 -5
  11. package/dist/commands/blueprints/info.js +5 -5
  12. package/dist/commands/blueprints/init.js +4 -4
  13. package/dist/commands/blueprints/logs.js +6 -6
  14. package/dist/commands/blueprints/plan.js +5 -5
  15. package/dist/commands/blueprints/promote.js +8 -6
  16. package/dist/commands/blueprints/stacks.js +5 -5
  17. package/dist/commands/functions/add.js +3 -3
  18. package/dist/commands/functions/build.js +4 -4
  19. package/dist/commands/functions/dev.js +5 -5
  20. package/dist/commands/functions/env/add.js +4 -4
  21. package/dist/commands/functions/env/list.js +4 -4
  22. package/dist/commands/functions/env/remove.js +4 -4
  23. package/dist/commands/functions/logs.js +4 -4
  24. package/dist/commands/functions/test.js +4 -4
  25. package/dist/constants.d.ts +1 -0
  26. package/dist/constants.js +2 -0
  27. package/dist/cores/blueprints/config.js +0 -1
  28. package/dist/cores/blueprints/deploy.d.ts +1 -0
  29. package/dist/cores/blueprints/deploy.js +30 -4
  30. package/dist/cores/blueprints/destroy.js +18 -3
  31. package/dist/cores/blueprints/doctor.js +11 -2
  32. package/dist/cores/blueprints/plan.js +18 -10
  33. package/dist/cores/blueprints/promote.js +11 -3
  34. package/dist/cores/blueprints/stacks.js +14 -3
  35. package/dist/cores/functions/test.js +7 -1
  36. package/dist/cores/index.d.ts +6 -0
  37. package/dist/cores/index.js +0 -3
  38. package/dist/utils/display/blueprints-formatting.js +4 -1
  39. package/dist/utils/display/resources-formatting.js +8 -3
  40. package/dist/utils/invoke-local.js +1 -1
  41. package/dist/utils/types.d.ts +5 -2
  42. package/oclif.manifest.json +22 -10
  43. package/package.json +1 -1
@@ -4,7 +4,7 @@ import { blueprintInitCore } from '../../cores/blueprints/init.js';
4
4
  import { Logger } from '../../utils/logger.js';
5
5
  import { validTokenOrErrorMessage } from '../../utils/validated-token.js';
6
6
  export default class InitCommand extends RuntimeCommand {
7
- static summary = 'Initialize a local Blueprint and optionally provision a remote Stack deployment';
7
+ static summary = 'Initialize a Blueprint and create a remote Stack';
8
8
  static description = `A Blueprint is your local infrastructure-as-code configuration that defines Sanity resources (datasets, functions, etc.). A Stack is the remote deployment target where your Blueprint is applied.
9
9
  [NOTE: Currently, accounts are limited to three (3) Stacks per project scope.]
10
10
 
@@ -57,7 +57,7 @@ After initialization, use 'blueprints plan' to preview changes, then 'blueprints
57
57
  const result = await validTokenOrErrorMessage(log);
58
58
  if (!result.ok)
59
59
  this.error(result.error.message);
60
- const { success, error } = await blueprintInitCore({
60
+ const initResult = await blueprintInitCore({
61
61
  bin: this.config.bin,
62
62
  log,
63
63
  token: result.value,
@@ -65,7 +65,7 @@ After initialization, use 'blueprints plan' to preview changes, then 'blueprints
65
65
  args: this.args,
66
66
  flags: this.flags,
67
67
  });
68
- if (!success)
69
- this.error(error);
68
+ if (!initResult.success)
69
+ this.coreError(initResult);
70
70
  }
71
71
  }
@@ -22,7 +22,7 @@ If you're not seeing expected logs, verify your Stack is deployed with 'blueprin
22
22
  }),
23
23
  };
24
24
  async run() {
25
- const { success, streaming, error, json } = await blueprintLogsCore({
25
+ const result = await blueprintLogsCore({
26
26
  bin: this.config.bin,
27
27
  log: Logger(this.log.bind(this), this.flags),
28
28
  auth: this.auth,
@@ -31,10 +31,10 @@ If you're not seeing expected logs, verify your Stack is deployed with 'blueprin
31
31
  validateResources: this.flags['validate-resources'],
32
32
  flags: this.flags,
33
33
  });
34
- if (streaming)
35
- return streaming;
36
- if (!success)
37
- this.error(error);
38
- return json;
34
+ if (result.streaming)
35
+ return result.streaming;
36
+ if (!result.success)
37
+ this.coreError(result);
38
+ return result.json;
39
39
  }
40
40
  }
@@ -2,7 +2,7 @@ import { LocalBlueprintCommand, stackFlag, unhide } from '../../baseCommands.js'
2
2
  import { blueprintPlanCore } from '../../cores/blueprints/plan.js';
3
3
  import { Logger } from '../../utils/logger.js';
4
4
  export default class PlanCommand extends LocalBlueprintCommand {
5
- static summary = 'Enumerate resources to be deployed to the remote Stack - will not modify any resources';
5
+ static summary = 'Preview changes that will be applied to the remote Stack';
6
6
  static description = `Use this command to preview what changes will be applied to your remote Stack before deploying. This is a safe, read-only operation—no resources are created, modified, or deleted.
7
7
 
8
8
  Run 'blueprints plan' after making local changes to your Blueprint manifest to verify the expected diff. When ready, run 'blueprints deploy' to apply changes.`;
@@ -11,7 +11,7 @@ Run 'blueprints plan' after making local changes to your Blueprint manifest to v
11
11
  stack: unhide(stackFlag),
12
12
  };
13
13
  async run() {
14
- const { success, error, json } = await blueprintPlanCore({
14
+ const result = await blueprintPlanCore({
15
15
  bin: this.config.bin,
16
16
  log: Logger(this.log.bind(this), this.flags),
17
17
  token: this.sanityToken,
@@ -19,8 +19,8 @@ Run 'blueprints plan' after making local changes to your Blueprint manifest to v
19
19
  validateResources: this.flags['validate-resources'],
20
20
  flags: this.flags,
21
21
  });
22
- if (!success)
23
- this.error(error);
24
- return json;
22
+ if (!result.success)
23
+ this.coreError(result);
24
+ return result.json;
25
25
  }
26
26
  }
@@ -3,8 +3,10 @@ import { DeployedStackCommand } from '../../baseCommands.js';
3
3
  import { blueprintPromoteCore } from '../../cores/blueprints/promote.js';
4
4
  import { Logger } from '../../utils/logger.js';
5
5
  export default class PromoteCommand extends DeployedStackCommand {
6
- static summary = 'Promote a Stack deployment to a broader scope';
7
- static description = `EXPERIMENTAL! Promotes a Stack, changing its scope from project to organization. This is a one-way trip.`;
6
+ static summary = 'Promote a Stack from project scope to organization scope';
7
+ static description = `Promotes a deployed Stack to organization scope, enabling management of org-level resources. Promotion cannot be reversed.
8
+
9
+ Your local Blueprint configuration will be updated to reflect the new scope.`;
8
10
  static hidden = true;
9
11
  static examples = [
10
12
  '<%= config.bin %> <%= command.id %>',
@@ -21,7 +23,7 @@ export default class PromoteCommand extends DeployedStackCommand {
21
23
  }),
22
24
  };
23
25
  async run() {
24
- const { success, error, json } = await blueprintPromoteCore({
26
+ const result = await blueprintPromoteCore({
25
27
  bin: this.config.bin,
26
28
  log: Logger(this.log.bind(this), this.flags),
27
29
  token: this.sanityToken,
@@ -34,8 +36,8 @@ export default class PromoteCommand extends DeployedStackCommand {
34
36
  validateResources: this.flags['validate-resources'],
35
37
  flags: this.flags,
36
38
  });
37
- if (!success)
38
- this.error(error);
39
- return json;
39
+ if (!result.success)
40
+ this.coreError(result);
41
+ return result.json;
40
42
  }
41
43
  }
@@ -3,7 +3,7 @@ import { LocalBlueprintCommand, organizationIdFlagConfig, projectIdFlagConfig, }
3
3
  import { blueprintStacksCore } from '../../cores/blueprints/stacks.js';
4
4
  import { Logger } from '../../utils/logger.js';
5
5
  export default class StacksCommand extends LocalBlueprintCommand {
6
- static summary = "List all remote Stack deployments (defaults to the current Blueprint's project scope)";
6
+ static summary = 'List remote Stack deployments for your project or organization';
7
7
  static description = `Shows all Stacks associated with a project or organization. By default, lists Stacks scoped to the local Blueprint.
8
8
 
9
9
  Use this to discover existing Stacks you can scope a local Blueprint to (using 'blueprints config --edit'), or to audit what's deployed across your project.`;
@@ -17,7 +17,7 @@ Use this to discover existing Stacks you can scope a local Blueprint to (using '
17
17
  'organization-id': Flags.string({ ...organizationIdFlagConfig, exclusive: ['project-id'] }),
18
18
  };
19
19
  async run() {
20
- const { success, error, json } = await blueprintStacksCore({
20
+ const result = await blueprintStacksCore({
21
21
  bin: this.config.bin,
22
22
  log: Logger(this.log.bind(this), this.flags),
23
23
  token: this.sanityToken,
@@ -25,8 +25,8 @@ Use this to discover existing Stacks you can scope a local Blueprint to (using '
25
25
  validateResources: this.flags['validate-resources'],
26
26
  flags: this.flags,
27
27
  });
28
- if (!success)
29
- this.error(error);
30
- return json;
28
+ if (!result.success)
29
+ this.coreError(result);
30
+ return result.json;
31
31
  }
32
32
  }
@@ -61,14 +61,14 @@ After adding, use 'functions dev' to test locally, then 'blueprints deploy' to p
61
61
  }),
62
62
  };
63
63
  async run() {
64
- const { success, error } = await functionAddCore({
64
+ const result = await functionAddCore({
65
65
  bin: this.config.bin,
66
66
  log: Logger(this.log.bind(this), this.flags),
67
67
  blueprint: this.blueprint,
68
68
  validateResources: this.flags['validate-resources'],
69
69
  flags: this.flags,
70
70
  });
71
- if (!success)
72
- this.error(error);
71
+ if (!result.success)
72
+ this.coreError(result);
73
73
  }
74
74
  }
@@ -19,7 +19,7 @@ export default class BuildCommand extends LocalBlueprintCommand {
19
19
  'out-dir': Flags.string({ char: 'o', description: 'Output directory for zip files' }),
20
20
  };
21
21
  async run() {
22
- const { success, error, json } = await functionBuildCore({
22
+ const result = await functionBuildCore({
23
23
  bin: this.config.bin,
24
24
  log: Logger(this.log.bind(this), this.flags),
25
25
  args: { name: this.args.name },
@@ -27,8 +27,8 @@ export default class BuildCommand extends LocalBlueprintCommand {
27
27
  blueprint: this.blueprint,
28
28
  validateResources: this.flags['validate-resources'],
29
29
  });
30
- if (!success)
31
- this.error(error);
32
- return json;
30
+ if (!result.success)
31
+ this.coreError(result);
32
+ return result.json;
33
33
  }
34
34
  }
@@ -34,15 +34,15 @@ To invoke a function with the CLI, use 'functions test'.`;
34
34
  };
35
35
  async run() {
36
36
  const { flags } = await this.parse(DevCommand);
37
- const { success, error, streaming } = await functionDevCore({
37
+ const result = await functionDevCore({
38
38
  bin: this.config.bin,
39
39
  log: Logger(this.log.bind(this), this.flags),
40
40
  validateResources: this.flags['validate-resources'],
41
41
  flags,
42
42
  });
43
- if (!success)
44
- this.error(error);
45
- if (streaming)
46
- await streaming;
43
+ if (!result.success)
44
+ this.coreError(result);
45
+ if (result.streaming)
46
+ await result.streaming;
47
47
  }
48
48
  }
@@ -16,7 +16,7 @@ Environment variables are useful for API keys, configuration values, and other s
16
16
  '<%= config.bin %> <%= command.id %> MyFunction API_URL https://api.example.com/',
17
17
  ];
18
18
  async run() {
19
- const { success, error, json } = await functionEnvAddCore({
19
+ const result = await functionEnvAddCore({
20
20
  bin: this.config.bin,
21
21
  log: Logger(this.log.bind(this), this.flags),
22
22
  args: this.args,
@@ -29,8 +29,8 @@ Environment variables are useful for API keys, configuration values, and other s
29
29
  stackId: this.stackId,
30
30
  validateResources: this.flags['validate-resources'],
31
31
  });
32
- if (!success)
33
- this.error(error);
34
- return json;
32
+ if (!result.success)
33
+ this.coreError(result);
34
+ return result.json;
35
35
  }
36
36
  }
@@ -12,7 +12,7 @@ Use 'functions env add' to set variables or 'functions env remove' to delete the
12
12
  };
13
13
  static examples = ['<%= config.bin %> <%= command.id %> MyFunction'];
14
14
  async run() {
15
- const { success, error, json } = await functionEnvListCore({
15
+ const result = await functionEnvListCore({
16
16
  bin: this.config.bin,
17
17
  log: Logger(this.log.bind(this), this.flags),
18
18
  args: this.args,
@@ -25,8 +25,8 @@ Use 'functions env add' to set variables or 'functions env remove' to delete the
25
25
  stackId: this.stackId,
26
26
  validateResources: this.flags['validate-resources'],
27
27
  });
28
- if (!success)
29
- this.error(error);
30
- return json;
28
+ if (!result.success)
29
+ this.coreError(result);
30
+ return result.json;
31
31
  }
32
32
  }
@@ -13,7 +13,7 @@ Use 'functions env list' to see current variables before removing.`;
13
13
  };
14
14
  static examples = ['<%= config.bin %> <%= command.id %> MyFunction API_URL'];
15
15
  async run() {
16
- const { success, error, json } = await functionEnvRemoveCore({
16
+ const result = await functionEnvRemoveCore({
17
17
  bin: this.config.bin,
18
18
  log: Logger(this.log.bind(this), this.flags),
19
19
  args: this.args,
@@ -26,8 +26,8 @@ Use 'functions env list' to see current variables before removing.`;
26
26
  stackId: this.stackId,
27
27
  validateResources: this.flags['validate-resources'],
28
28
  });
29
- if (!success)
30
- this.error(error);
31
- return json;
29
+ if (!result.success)
30
+ this.coreError(result);
31
+ return result.json;
32
32
  }
33
33
  }
@@ -48,7 +48,7 @@ Use --watch (-w) to stream logs in real-time. Use --delete to clear all logs for
48
48
  }),
49
49
  };
50
50
  async run() {
51
- const { success, error, json } = await functionLogsCore({
51
+ const result = await functionLogsCore({
52
52
  bin: this.config.bin,
53
53
  log: Logger(this.log.bind(this), this.flags),
54
54
  error: (msg, options) => this.error(msg, options),
@@ -64,8 +64,8 @@ Use --watch (-w) to stream logs in real-time. Use --delete to clear all logs for
64
64
  helpText: LogsCommand.getHelpText(this.config.bin, 'functions logs'),
65
65
  validateResources: this.flags['validate-resources'],
66
66
  });
67
- if (!success)
68
- this.error(error);
69
- return json;
67
+ if (!result.success)
68
+ this.coreError(result);
69
+ return result.json;
70
70
  }
71
71
  }
@@ -159,7 +159,7 @@ Provide test data via --data (inline JSON), --file (JSON file), or --document-id
159
159
  ' --document-id-before and --document-id-after');
160
160
  }
161
161
  }
162
- const { success, error, json } = await functionTestCore({
162
+ const result = await functionTestCore({
163
163
  bin: this.config.bin,
164
164
  log: Logger(this.log.bind(this), this.flags),
165
165
  error: (msg, options) => this.error(msg, options),
@@ -169,8 +169,8 @@ Provide test data via --data (inline JSON), --file (JSON file), or --document-id
169
169
  helpText: TestCommand.getHelpText(this.config.bin, 'functions test'),
170
170
  validateResources: this.flags['validate-resources'],
171
171
  });
172
- if (!success)
173
- this.error(error);
174
- return json;
172
+ if (!result.success)
173
+ this.coreError(result);
174
+ return result.json;
175
175
  }
176
176
  }
@@ -4,6 +4,7 @@ export declare const SANITY_FUNCTION_PREFIX = "sanity.function.";
4
4
  export declare const SANITY_FUNCTION_DOCUMENT = "sanity.function.document";
5
5
  export declare const SANITY_FUNCTION_MEDIA_LIBRARY_ASSET = "sanity.function.media-library.asset";
6
6
  export declare const SANITY_FUNCTION_SCHEDULED = "sanity.function.cron";
7
+ export declare const SANITY_FUNCTION_SYNC_TAG_INVALIDATE = "sanity.function.sync-tag-invalidate";
7
8
  export declare const SANITY_PROJECT = "sanity.project";
8
9
  export declare const SANITY_PROJECT_CORS = "sanity.project.cors";
9
10
  export declare const SANITY_PROJECT_DATASET = "sanity.project.dataset";
package/dist/constants.js CHANGED
@@ -4,6 +4,7 @@ export const SANITY_FUNCTION_PREFIX = 'sanity.function.';
4
4
  export const SANITY_FUNCTION_DOCUMENT = 'sanity.function.document';
5
5
  export const SANITY_FUNCTION_MEDIA_LIBRARY_ASSET = 'sanity.function.media-library.asset';
6
6
  export const SANITY_FUNCTION_SCHEDULED = 'sanity.function.cron';
7
+ export const SANITY_FUNCTION_SYNC_TAG_INVALIDATE = 'sanity.function.sync-tag-invalidate';
7
8
  export const SANITY_PROJECT = 'sanity.project';
8
9
  export const SANITY_PROJECT_CORS = 'sanity.project.cors';
9
10
  export const SANITY_PROJECT_DATASET = 'sanity.project.dataset';
@@ -29,6 +30,7 @@ export const FUNCTION_TYPES = [
29
30
  export const PROJECT_SCOPED_FUNCTION_TYPES = new Set([
30
31
  SANITY_FUNCTION_DOCUMENT,
31
32
  SANITY_FUNCTION_MEDIA_LIBRARY_ASSET,
33
+ SANITY_FUNCTION_SYNC_TAG_INVALIDATE,
32
34
  ]);
33
35
  export const ORGANIZATION_SCOPED_FUNCTION_TYPES = new Set([
34
36
  SANITY_FUNCTION_SCHEDULED,
@@ -111,7 +111,6 @@ function printConfig(options) {
111
111
  const { projectId, organizationId, stackId, updatedAt } = config;
112
112
  const scopeType = projectId ? 'project' : 'organization';
113
113
  const scopeId = projectId ? projectId : organizationId;
114
- log.verbose(JSON.stringify(config));
115
114
  log(`${styleText('bold', `${configLabel} configuration:`)}`);
116
115
  log(` Deployment: ${labeledId('stack', stackId)}`);
117
116
  log(` Scoped to: ${labeledId(scopeType, scopeId)}`);
@@ -12,6 +12,7 @@ export interface BlueprintDeployOptions extends CoreConfig {
12
12
  'fn-installer'?: string;
13
13
  'no-wait'?: boolean;
14
14
  'new-stack-name'?: string;
15
+ message?: string;
15
16
  verbose?: boolean;
16
17
  };
17
18
  }
@@ -25,7 +25,11 @@ export async function blueprintDeployCore(options) {
25
25
  if (operationStatus === 'QUEUED' || operationStatus === 'IN_PROGRESS') {
26
26
  return {
27
27
  success: false,
28
- error: 'A Stack operation is already in progress. Please wait for it to complete before deploying.',
28
+ error: 'A Stack operation is already in progress.',
29
+ suggestions: [
30
+ `Run \`npx ${bin} blueprints info\` to check operation status.`,
31
+ 'Wait for the current operation to complete, then try again.',
32
+ ],
29
33
  };
30
34
  }
31
35
  const resources = [...blueprint.resources];
@@ -50,13 +54,21 @@ export async function blueprintDeployCore(options) {
50
54
  scopeId,
51
55
  name: stackName,
52
56
  document: { resources },
57
+ userMessage: flags.message,
53
58
  },
54
59
  auth,
55
60
  logger: log,
56
61
  });
57
62
  if (!deployOk) {
58
63
  spinner.fail(`${styleText('red', 'Failed')} to update Stack deployment`);
59
- return { success: false, error: deployError || 'Failed to update Stack deployment' };
64
+ return {
65
+ success: false,
66
+ error: deployError || 'Failed to update Stack deployment',
67
+ suggestions: [
68
+ `Run \`npx ${bin} blueprints plan\` to preview changes before deploying.`,
69
+ `Run \`npx ${bin} blueprints doctor\` to check your configuration.`,
70
+ ],
71
+ };
60
72
  }
61
73
  spinner.stop().clear();
62
74
  if (noWait) {
@@ -88,7 +100,14 @@ export async function blueprintDeployCore(options) {
88
100
  if (!ok) {
89
101
  if (logStreamCleanup)
90
102
  logStreamCleanup();
91
- return { success: false, error: 'Failed to check Stack deployment status' };
103
+ return {
104
+ success: false,
105
+ error: 'Failed to check Stack deployment status',
106
+ suggestions: [
107
+ 'The deployment may still be running on Sanity servers.',
108
+ `Run \`npx ${bin} blueprints info\` to check status.`,
109
+ ],
110
+ };
92
111
  }
93
112
  const operation = currentStack.recentOperation;
94
113
  if (!operation) {
@@ -99,7 +118,14 @@ export async function blueprintDeployCore(options) {
99
118
  if (operation.status === 'FAILED') {
100
119
  if (logStreamCleanup)
101
120
  logStreamCleanup();
102
- return { success: false, error: 'Stack deployment failed' };
121
+ return {
122
+ success: false,
123
+ error: 'Stack deployment failed',
124
+ suggestions: [
125
+ `Run \`npx ${bin} blueprints logs\` to review deployment logs.`,
126
+ `Run \`npx ${bin} blueprints plan\` to identify issues with your Blueprint.`,
127
+ ],
128
+ };
103
129
  }
104
130
  if (operation.status === 'COMPLETED') {
105
131
  if (logStreamCleanup)
@@ -55,8 +55,16 @@ export async function blueprintDestroyCore(options) {
55
55
  return { success: false, error: blueprintStack.error || 'Failed to get Stack' };
56
56
  stack = blueprintStack.stack;
57
57
  }
58
- if (!stack)
59
- return { success: false, error: 'Stack deployment not found' };
58
+ if (!stack) {
59
+ return {
60
+ success: false,
61
+ error: 'Stack deployment not found',
62
+ suggestions: [
63
+ `Run \`npx ${bin} blueprints stacks\` to list available Stacks.`,
64
+ `Run \`npx ${bin} blueprints doctor\` to check your configuration.`,
65
+ ],
66
+ };
67
+ }
60
68
  const destroySpinner = log.ora({
61
69
  text: `Destroying Stack deployment "${styleText('bold', stack.name)}" ${niceId(stack.id)}...`,
62
70
  color: 'red',
@@ -122,7 +130,14 @@ export async function blueprintDestroyCore(options) {
122
130
  if (operation.status === 'FAILED') {
123
131
  if (logStreamCleanup)
124
132
  logStreamCleanup();
125
- return { success: false, error: 'Stack destruction failed' };
133
+ return {
134
+ success: false,
135
+ error: 'Stack destruction failed',
136
+ suggestions: [
137
+ `Run \`npx ${bin} blueprints logs\` to review destruction logs.`,
138
+ `Run \`npx ${bin} blueprints info\` to view current Stack status.`,
139
+ ],
140
+ };
126
141
  }
127
142
  if (!idleMessageShown && Date.now() - lastLogAt > 60_000) {
128
143
  log(`No new activity for 60 seconds. The destruction is still running on Sanity servers.`);
@@ -163,6 +163,8 @@ export async function blueprintDoctorCore(options) {
163
163
  ? ` ${styleText('dim', new Date(time).toLocaleString('sv-SE'))}`
164
164
  : '';
165
165
  stackRows.push(['Operation', `${op.status}${timestamp}`]);
166
+ if (op.userMessage)
167
+ stackRows.push([' Message', `"${op.userMessage}"`]);
166
168
  }
167
169
  if (stack.resources) {
168
170
  stackRows.push(['Resources', `${stack.resources.length}`]);
@@ -284,6 +286,7 @@ export async function blueprintDoctorCore(options) {
284
286
  return {
285
287
  success: false,
286
288
  error: `${errorMessage}. Unable to fix: Missing authentication token`,
289
+ suggestions: ['Run `npx @sanity/cli login` to authenticate, then try again.'],
287
290
  data: { diagnostics: flatDiagnostics },
288
291
  };
289
292
  }
@@ -291,6 +294,7 @@ export async function blueprintDoctorCore(options) {
291
294
  return {
292
295
  success: false,
293
296
  error: `${errorMessage}. Unable to fix: ${tokenOrError.error.message}`,
297
+ suggestions: ['Run `npx @sanity/cli login` to re-authenticate.'],
294
298
  data: { diagnostics: flatDiagnostics },
295
299
  };
296
300
  }
@@ -298,6 +302,7 @@ export async function blueprintDoctorCore(options) {
298
302
  return {
299
303
  success: false,
300
304
  error: `${errorMessage}. Unable to fix: Blueprint is missing or invalid`,
305
+ suggestions: [`Run \`npx ${bin} blueprints init\` to create a new Blueprint.`],
301
306
  data: { diagnostics: flatDiagnostics },
302
307
  };
303
308
  }
@@ -310,6 +315,10 @@ export async function blueprintDoctorCore(options) {
310
315
  flags: { edit: true, verbose: v },
311
316
  });
312
317
  }
313
- log(styleText('dim', ` Run \`npx ${bin} blueprints doctor --fix\` to resolve configuration issues.`));
314
- return { success: false, error: errorMessage, data: { diagnostics: flatDiagnostics } };
318
+ return {
319
+ success: false,
320
+ error: errorMessage,
321
+ suggestions: [`Run \`npx ${bin} blueprints doctor --fix\` to fix configuration issues.`],
322
+ data: { diagnostics: flatDiagnostics },
323
+ };
315
324
  }
@@ -9,13 +9,17 @@ export async function blueprintPlanCore(options) {
9
9
  log(formatResourceTree(resources));
10
10
  if (!token || !scopeType || !scopeId) {
11
11
  log(styleText('dim', 'Unable to retrieve live Stack deployment for comparison'));
12
- const errorMessage = !token
13
- ? `Missing authentication token. Run \`npx ${bin} login\` to authenticate.`
14
- : `Missing Blueprint configuration. Run \`npx ${bin} blueprints doctor --fix\` to repair.`;
15
- return {
16
- success: false,
17
- error: errorMessage,
18
- };
12
+ return !token
13
+ ? {
14
+ success: false,
15
+ error: 'Missing authentication token',
16
+ suggestions: [`Run \`npx ${bin} login\` to authenticate.`],
17
+ }
18
+ : {
19
+ success: false,
20
+ error: 'Missing Blueprint configuration',
21
+ suggestions: [`Run \`npx ${bin} blueprints doctor --fix\` to fix your configuration.`],
22
+ };
19
23
  }
20
24
  const stackId = flags.stack
21
25
  ? await resolveStackIdByNameOrId(flags.stack, { token, scopeType, scopeId }, log)
@@ -24,7 +28,8 @@ export async function blueprintPlanCore(options) {
24
28
  log(styleText('dim', 'Unable to retrieve live Stack deployment for comparison'));
25
29
  return {
26
30
  success: false,
27
- error: `Missing Stack deployment configuration. Run \`npx ${bin} blueprints doctor --fix\` to repair.`,
31
+ error: 'Missing Stack deployment configuration',
32
+ suggestions: [`Run \`npx ${bin} blueprints doctor --fix\` to fix your configuration.`],
28
33
  };
29
34
  }
30
35
  const auth = { token, scopeType, scopeId };
@@ -48,12 +53,15 @@ export async function blueprintPlanCore(options) {
48
53
  log(` ${styleText(['bold', 'red'], '✘')} ${msg}`);
49
54
  }
50
55
  }
51
- log(`\n Fix the issues above before running "${styleText(['bold', 'magenta'], `npx ${bin} blueprints deploy`)}"`);
52
56
  }
53
57
  else {
54
58
  log(styleText('dim', '\nUnable to retrieve deployment plan from server'));
55
59
  }
56
- return { success: false, error: 'Deployment plan has problems' };
60
+ return {
61
+ success: false,
62
+ error: 'Deployment plan has problems',
63
+ suggestions: [`Fix the issues above, then run \`npx ${bin} blueprints deploy\`.`],
64
+ };
57
65
  }
58
66
  log('');
59
67
  log(formatDeploymentPlan(planResponse.deploymentPlan));
@@ -3,13 +3,13 @@ import { patchConfigFile } from '../../actions/blueprints/config.js';
3
3
  import { promoteStack } from '../../actions/blueprints/stacks.js';
4
4
  import { niceId } from '../../utils/display/presenters.js';
5
5
  export async function blueprintPromoteCore(options) {
6
- const { log, stackId, auth, flags, deployedStack, blueprint } = options;
6
+ const { bin = 'sanity', log, stackId, auth, flags, deployedStack, blueprint } = options;
7
7
  let message = `"${deployedStack.name}" ${niceId(deployedStack.id)}`;
8
8
  if (deployedStack.scopeType === 'organization') {
9
9
  message = `Stack ${message} is already org-scoped. Promote again?`;
10
10
  }
11
11
  else {
12
- message = `Are you sure you want to promote Stack ${message}?`;
12
+ message = `Promote Stack ${message} from project to organization scope? You will need admin access to your organization.`;
13
13
  }
14
14
  if (!flags.force) {
15
15
  const confirmed = await confirm({ message });
@@ -22,7 +22,12 @@ export async function blueprintPromoteCore(options) {
22
22
  const { ok, error, stack } = await promoteStack({ stackId, auth, logger: log });
23
23
  if (!ok) {
24
24
  spinner.fail('Failed to promote Stack');
25
- return { success: false, error: error || 'Failed to promote Stack' };
25
+ const suggestions = [];
26
+ if (error && /access|permission|forbidden/i.test(error)) {
27
+ suggestions.push('Verify you have organization admin access.');
28
+ }
29
+ suggestions.push(`Run \`npx ${bin} blueprints info\` to check the current Stack scope.`);
30
+ return { success: false, error: error || 'Failed to promote Stack', suggestions };
26
31
  }
27
32
  spinner.stop().clear();
28
33
  log(`Stack "${stack.name}" ${niceId(stack.id)} promoted successfully`);
@@ -39,6 +44,9 @@ export async function blueprintPromoteCore(options) {
39
44
  return {
40
45
  success: false,
41
46
  error: 'Stack promoted successfully but failed to update local Blueprint configuration. No config file found.',
47
+ suggestions: [
48
+ `Run \`npx ${bin} blueprints config --edit\` to update your local configuration.`,
49
+ ],
42
50
  };
43
51
  }
44
52
  return { success: true, json: { stack }, data: { stack } };
@@ -21,15 +21,26 @@ export async function blueprintStacksCore(options) {
21
21
  scopeId = flagProjectId;
22
22
  }
23
23
  if (!scopeType || !scopeId) {
24
- log.error('Run in a Blueprint directory or provide a Project with --project-id');
25
- return { success: false, error: 'Unable to determine scope for Blueprint Stacks' };
24
+ return {
25
+ success: false,
26
+ error: 'Unable to determine scope for Blueprint Stacks',
27
+ suggestions: ['Run in a Blueprint directory or provide a project with --project-id.'],
28
+ };
26
29
  }
27
30
  try {
28
31
  const spinner = log.ora('Fetching Stacks...').start();
29
32
  const { ok, stacks, error } = await listStacks({ token, scopeType, scopeId }, log);
30
33
  if (!ok) {
31
34
  spinner.fail('Failed to list Stacks');
32
- return { success: false, error: error || 'Failed to list stacks' };
35
+ const suggestions = [];
36
+ if (error && /access|permission|forbidden/i.test(error)) {
37
+ suggestions.push('Verify you have access to this organization or project.', `Try a different scope with --project-id or --organization-id.`);
38
+ }
39
+ return {
40
+ success: false,
41
+ error: error || 'Failed to list stacks',
42
+ suggestions: suggestions.length ? suggestions : undefined,
43
+ };
33
44
  }
34
45
  spinner.stop().clear();
35
46
  if (!stacks || stacks.length === 0) {