@sanity/runtime-cli 14.13.3 → 15.0.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 (65) hide show
  1. package/README.md +215 -118
  2. package/dist/actions/blueprints/blueprint.js +13 -11
  3. package/dist/actions/blueprints/logs.d.ts +2 -1
  4. package/dist/actions/blueprints/logs.js +4 -5
  5. package/dist/actions/blueprints/resources.js +1 -0
  6. package/dist/actions/blueprints/stacks.d.ts +3 -1
  7. package/dist/actions/blueprints/stacks.js +11 -2
  8. package/dist/actions/functions/test.js +1 -1
  9. package/dist/actions/sanity/access.d.ts +38 -0
  10. package/dist/actions/sanity/access.js +23 -0
  11. package/dist/actions/sanity/projects.d.ts +1 -1
  12. package/dist/baseCommands.d.ts +2 -0
  13. package/dist/baseCommands.js +8 -5
  14. package/dist/commands/blueprints/add.js +1 -1
  15. package/dist/commands/blueprints/deploy.d.ts +2 -0
  16. package/dist/commands/blueprints/deploy.js +6 -2
  17. package/dist/commands/blueprints/destroy.js +0 -2
  18. package/dist/commands/blueprints/info.d.ts +1 -0
  19. package/dist/commands/blueprints/info.js +3 -1
  20. package/dist/commands/blueprints/init.js +2 -0
  21. package/dist/commands/blueprints/logs.d.ts +5 -0
  22. package/dist/commands/blueprints/logs.js +26 -3
  23. package/dist/commands/blueprints/mint-deploy-token.d.ts +14 -0
  24. package/dist/commands/blueprints/mint-deploy-token.js +47 -0
  25. package/dist/commands/blueprints/plan.d.ts +2 -0
  26. package/dist/commands/blueprints/plan.js +8 -2
  27. package/dist/commands/blueprints/promote.d.ts +2 -1
  28. package/dist/commands/blueprints/promote.js +7 -2
  29. package/dist/commands/blueprints/stacks.js +1 -1
  30. package/dist/commands/functions/add.js +1 -1
  31. package/dist/cores/blueprints/config.js +34 -34
  32. package/dist/cores/blueprints/doctor.js +7 -7
  33. package/dist/cores/blueprints/init.js +99 -76
  34. package/dist/cores/blueprints/logs.d.ts +3 -0
  35. package/dist/cores/blueprints/logs.js +15 -9
  36. package/dist/cores/blueprints/mint-deploy-token.d.ts +15 -0
  37. package/dist/cores/blueprints/mint-deploy-token.js +111 -0
  38. package/dist/cores/blueprints/promote.d.ts +1 -0
  39. package/dist/cores/blueprints/promote.js +25 -4
  40. package/dist/cores/functions/add.js +4 -5
  41. package/dist/cores/index.d.ts +1 -2
  42. package/dist/cores/index.js +1 -3
  43. package/dist/index.d.ts +0 -18
  44. package/dist/index.js +0 -20
  45. package/dist/utils/clipboard.d.ts +14 -0
  46. package/dist/utils/clipboard.js +73 -0
  47. package/dist/utils/display/errors.d.ts +5 -1
  48. package/dist/utils/display/prompt.d.ts +55 -15
  49. package/dist/utils/display/prompt.js +271 -45
  50. package/oclif.manifest.json +320 -18
  51. package/package.json +21 -67
  52. package/dist/actions/blueprints/index.d.ts +0 -16
  53. package/dist/actions/blueprints/index.js +0 -10
  54. package/dist/actions/functions/index.d.ts +0 -4
  55. package/dist/actions/functions/index.js +0 -4
  56. package/dist/actions/sanity/index.d.ts +0 -1
  57. package/dist/actions/sanity/index.js +0 -1
  58. package/dist/cores/blueprints/index.d.ts +0 -20
  59. package/dist/cores/blueprints/index.js +0 -10
  60. package/dist/cores/functions/index.d.ts +0 -16
  61. package/dist/cores/functions/index.js +0 -8
  62. package/dist/utils/display/index.d.ts +0 -5
  63. package/dist/utils/display/index.js +0 -5
  64. package/dist/utils/index.d.ts +0 -8
  65. package/dist/utils/index.js +0 -8
@@ -3,6 +3,60 @@ import { createEmptyStack, listStacks } from '../../actions/blueprints/stacks.js
3
3
  import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
4
4
  import { styleText } from '../style-text.js';
5
5
  import { niceId } from './presenters.js';
6
+ // --- Wizard infrastructure ---
7
+ export const BACK = Symbol('back');
8
+ /**
9
+ * Runs a sequence of prompt steps with back-navigation support.
10
+ * Each step can return BACK to go to the previous non-skipped step.
11
+ * When going back, stale results from the current step onward are cleared.
12
+ */
13
+ async function runWizard(steps) {
14
+ const results = {};
15
+ let i = 0;
16
+ while (i < steps.length) {
17
+ const step = steps[i];
18
+ if (step.shouldSkip?.(results)) {
19
+ i++;
20
+ continue;
21
+ }
22
+ const answer = await step.prompt(results);
23
+ if (answer === BACK) {
24
+ if (i === 0) {
25
+ // Cannot go back from the first step; re-prompt it
26
+ continue;
27
+ }
28
+ // Walk back to the previous non-skipped step
29
+ do {
30
+ i--;
31
+ } while (i > 0 && steps[i].shouldSkip?.(results));
32
+ // Erase 2 lines: the "← Back" answer line and the previous step's
33
+ // answered prompt. Back is only offered on steps where the previous
34
+ // step rendered a prompt, so this is always correct.
35
+ process.stdout.write('\x1b[1A\x1b[2K\x1b[1A\x1b[2K');
36
+ // Clear stale results from this step forward
37
+ for (let j = i; j < steps.length; j++) {
38
+ delete results[steps[j].key];
39
+ }
40
+ continue;
41
+ }
42
+ results[step.key] = answer;
43
+ i++;
44
+ }
45
+ return results;
46
+ }
47
+ /**
48
+ * Appends a "Back" choice to a list of select choices.
49
+ * Skipped for the first step in a wizard (nowhere to go back to).
50
+ */
51
+ function withBackChoice(choices, allowBack) {
52
+ if (!allowBack)
53
+ return choices;
54
+ return [
55
+ ...choices,
56
+ { name: styleText('dim', '\u2190 Back'), value: BACK, description: 'Return to previous step' },
57
+ ];
58
+ }
59
+ // --- Standalone prompts ---
6
60
  export async function promptForBlueprintType() {
7
61
  return await select({
8
62
  message: 'Choose a Blueprint file type:',
@@ -14,60 +68,117 @@ export async function promptForBlueprintType() {
14
68
  default: 'ts',
15
69
  });
16
70
  }
71
+ // --- Composable prompt functions for scope selection ---
17
72
  /**
18
- * Prompt the user for a Project after selecting an Organization.
19
- * @param token - The Sanity API token
20
- * @returns The selected project, with the projectId and displayName
21
- * @throws {Error} If the user does not have any projects or if the API call fails
73
+ * Fetches organizations and prompts the user to pick one (if multiple).
74
+ * Returns the selected organization along with its projects.
22
75
  */
23
- export async function promptForProject({ token, knownOrganizationId, knownProjectId, logger, }) {
76
+ export async function promptForOrganization({ token, knownOrganizationId, logger, allowBack = false, }) {
77
+ const spinner = logger.ora('Loading organizations...').start();
24
78
  const { ok, error, organizations } = await groupProjectsByOrganization({ token, logger });
79
+ spinner.stop();
25
80
  if (!ok) {
26
- throw new Error(error ?? 'Unknown error listing projects');
81
+ throw new Error(error ?? 'Unknown error listing organizations');
27
82
  }
28
83
  if (organizations.length === 0) {
29
- throw new Error('No Sanity projects found. Use `npx sanity init` to create one.');
84
+ throw new Error('No Sanity organizations found. Use `npx sanity init` to create a project.');
30
85
  }
31
- let projects;
86
+ let picked;
87
+ let wasPrompted = false;
32
88
  if (organizations.length > 1) {
33
- const orgChoices = organizations.map(({ organization, projects }) => ({
34
- name: `"${organization.name}" ${niceId(organization.id)}`,
35
- value: organization.id,
36
- disabled: !projects || projects.length === 0 ? '(0 Projects)' : false,
37
- }));
89
+ wasPrompted = true;
90
+ const orgChoices = organizations.map(({ organization, projects }) => {
91
+ const projectCount = projects?.length ?? 0;
92
+ return {
93
+ name: `"${organization.name}" ${niceId(organization.id)}`,
94
+ value: organization.id,
95
+ description: `${projectCount} ${projectCount === 1 ? 'project' : 'projects'}`,
96
+ };
97
+ });
38
98
  const pickedOrgId = await select({
39
- message: 'Which Organization would you like to use?',
40
- choices: orgChoices,
99
+ message: 'Which organization would you like to use?',
100
+ choices: withBackChoice(orgChoices, allowBack),
41
101
  default: knownOrganizationId,
42
102
  });
103
+ if (pickedOrgId === BACK)
104
+ return BACK;
43
105
  const pickedOrg = organizations.find((o) => o.organization.id === pickedOrgId);
44
- projects = pickedOrg?.projects;
106
+ if (!pickedOrg) {
107
+ throw new Error('No organization found with the given ID');
108
+ }
109
+ picked = pickedOrg;
45
110
  }
46
111
  else {
47
- projects = organizations[0].projects;
112
+ picked = organizations[0];
113
+ logger(`Using organization "${picked.organization.name}" ${niceId(picked.organization.id)}`);
114
+ }
115
+ return {
116
+ organization: picked.organization,
117
+ projects: picked.projects ?? [],
118
+ wasPrompted,
119
+ };
120
+ }
121
+ /**
122
+ * Asks the user whether to scope a Blueprint to an organization or a project.
123
+ */
124
+ export async function promptForScope({ allowBack = false, hasProjects = true, } = {}) {
125
+ const choices = [
126
+ {
127
+ name: 'Organization',
128
+ value: 'organization',
129
+ description: 'Manage resources across all projects in the organization',
130
+ },
131
+ {
132
+ name: 'Project',
133
+ value: 'project',
134
+ description: 'Manage resources within a single project',
135
+ disabled: hasProjects ? false : '(no projects in this organization)',
136
+ },
137
+ ];
138
+ const answer = await select({
139
+ message: 'How would you like to scope your Blueprint?',
140
+ choices: withBackChoice(choices, allowBack),
141
+ default: 'organization',
142
+ });
143
+ return answer;
144
+ }
145
+ /**
146
+ * Prompts the user to pick a project from a pre-fetched list of projects.
147
+ * Use after promptForOrganization when the user chose project scope.
148
+ */
149
+ export async function promptForProjectInOrg({ projects, knownProjectId, logger, allowBack = false, }) {
150
+ if (projects.length === 0) {
151
+ throw new Error('No projects found in this organization.');
48
152
  }
49
153
  const projectChoices = projects.map(({ displayName, id: projectId }) => ({
50
154
  name: `"${displayName}" ${niceId(projectId)}`,
51
155
  value: projectId,
52
156
  }));
53
157
  let pickedProjectId;
54
- if (projectChoices.length === 1) {
158
+ if (projectChoices.length === 1 && !allowBack) {
159
+ // Single-project confirm only when not in wizard (wizard has Back for navigation)
55
160
  const onlyProject = projectChoices[0];
56
161
  const confirmed = await confirm({
57
- message: `Found 1 project. Use ${onlyProject.name}?`,
162
+ message: `Found 1 project in this organization. Use ${onlyProject.name}?`,
58
163
  default: true,
59
164
  });
60
- if (confirmed)
165
+ if (confirmed) {
61
166
  pickedProjectId = onlyProject.value;
62
- else
167
+ logger?.(`Using project ${onlyProject.name}`);
168
+ }
169
+ else {
63
170
  throw new Error('No project selected');
171
+ }
64
172
  }
65
173
  else {
66
- pickedProjectId = await select({
67
- message: 'Choose a Sanity Project:',
68
- choices: projectChoices,
174
+ const answer = await select({
175
+ message: 'Choose a Sanity project:',
176
+ choices: withBackChoice(projectChoices, allowBack),
69
177
  default: knownProjectId,
70
178
  });
179
+ if (answer === BACK)
180
+ return BACK;
181
+ pickedProjectId = answer;
71
182
  }
72
183
  const pickedProject = projects.find((p) => p.id === pickedProjectId);
73
184
  if (!pickedProject)
@@ -75,44 +186,69 @@ export async function promptForProject({ token, knownOrganizationId, knownProjec
75
186
  return { projectId: pickedProject.id, displayName: pickedProject.displayName };
76
187
  }
77
188
  /**
78
- * Prompt the user for a Stack ID after selecting a Project.
79
- * Can be used to create a new Stack or select an existing one.
80
- * @param projectId - The ID of the Project
81
- * @param token - The Sanity API token
82
- * @returns The selected Stack ID
83
- * @throws {Error} If the user does not have any Stacks or if the API call fails
189
+ * Prompt the user for a Stack after scope has been determined.
190
+ * Can create a new Stack or select an existing one.
84
191
  */
85
- export async function promptForStack({ projectId, token, logger, }) {
86
- const { ok: stacksOk, error: stacksErr, stacks, } = await listStacks({ token, scopeType: 'project', scopeId: projectId }, logger);
192
+ export async function promptForStack({ scopeType, scopeId, token, logger, allowBack = false, }) {
193
+ if (!scopeId) {
194
+ throw new Error(`Cannot list Stacks: no ${scopeType} ID provided. ` +
195
+ 'Provide --project-id or --organization-id, or select a scope in the wizard.');
196
+ }
197
+ const spinner = logger.ora('Loading Stacks...').start();
198
+ const { ok: stacksOk, error: stacksErr, stacks, } = await listStacks({ token, scopeType, scopeId }, logger);
199
+ spinner.stop();
87
200
  if (!stacksOk) {
88
- throw new Error(stacksErr || 'Failed to list Stacks');
201
+ const scopeLabel = scopeType === 'organization' ? 'organization' : 'project';
202
+ throw new Error(stacksErr || `Failed to list Stacks for ${scopeLabel} "${scopeId}".`);
89
203
  }
90
204
  const NEW_STACK_ID = 'new';
91
205
  let pickedStackId = NEW_STACK_ID;
206
+ if (stacks.length === 0) {
207
+ logger('No existing Stacks found. Creating a new one.');
208
+ }
92
209
  if (stacks.length > 0) {
93
- const stackChoices = [];
94
- stackChoices.push(new Separator(styleText('underline', 'Create a new Stack:')));
95
- stackChoices.push({ name: styleText('bold', 'New Stack'), value: NEW_STACK_ID });
96
- stackChoices.push(new Separator(styleText('underline', 'Use an existing Stack:')));
97
- stackChoices.push(...stacks.map((s) => ({
98
- name: `"${s.name}" ${niceId(s.id)} ${styleText('dim', `(${s.resourceCount} ${s.resourceCount === 1 ? 'resource' : 'resources'})`)}`,
99
- value: s.id,
100
- })));
101
- pickedStackId = await select({
210
+ const stackChoices = [
211
+ new Separator(styleText('underline', 'Create a new Stack:')),
212
+ { name: styleText('bold', 'New Stack'), value: NEW_STACK_ID },
213
+ new Separator(styleText('underline', 'Use an existing Stack:')),
214
+ ...stacks.map((s) => ({
215
+ name: `"${s.name}" ${niceId(s.id)} ${styleText('dim', `(${s.resourceCount} ${s.resourceCount === 1 ? 'resource' : 'resources'})`)}`,
216
+ value: s.id,
217
+ })),
218
+ ];
219
+ if (allowBack) {
220
+ stackChoices.push(new Separator(), {
221
+ name: styleText('dim', '\u2190 Back'),
222
+ value: BACK,
223
+ description: 'Return to previous step',
224
+ });
225
+ }
226
+ const answer = await select({
102
227
  message: 'Select a deployment Stack:',
103
228
  choices: stackChoices,
104
229
  default: NEW_STACK_ID,
105
230
  });
231
+ if (answer === BACK)
232
+ return BACK;
233
+ pickedStackId = answer;
234
+ }
235
+ else if (allowBack) {
236
+ const proceed = await select({
237
+ message: 'No existing Stacks. Create a new one?',
238
+ choices: withBackChoice([{ name: 'Yes, create a new Stack', value: 'yes' }], allowBack),
239
+ });
240
+ if (proceed === BACK)
241
+ return BACK;
106
242
  }
107
243
  if (pickedStackId === NEW_STACK_ID) {
108
244
  const stackName = await input({
109
245
  message: 'Enter a name for your new Stack:',
110
- validate: (input) => input.length > 0 || 'Stack name is required',
246
+ validate: (val) => val.length > 0 || 'Stack name is required',
111
247
  });
112
248
  const stack = await createEmptyStack({
113
249
  token,
114
- scopeType: 'project',
115
- scopeId: projectId,
250
+ scopeType,
251
+ scopeId,
116
252
  name: stackName,
117
253
  logger,
118
254
  });
@@ -123,3 +259,93 @@ export async function promptForStack({ projectId, token, logger, }) {
123
259
  throw new Error('Stack not found');
124
260
  return { stackId: pickedStack.id, name: pickedStack.name };
125
261
  }
262
+ /**
263
+ * Full wizard: Organization -> Scope -> Project (if project-scoped) -> Stack.
264
+ * Supports back-navigation between steps.
265
+ */
266
+ export async function runScopeAndStackWizard({ token, knownOrganizationId, knownProjectId, logger, }) {
267
+ const steps = [
268
+ {
269
+ key: 'organization',
270
+ prompt: async () => {
271
+ return await promptForOrganization({
272
+ token,
273
+ knownOrganizationId,
274
+ logger,
275
+ // First step -- no back
276
+ });
277
+ },
278
+ },
279
+ {
280
+ key: 'scopeType',
281
+ prompt: async (results) => {
282
+ const orgResult = results.organization;
283
+ return await promptForScope({
284
+ allowBack: orgResult.wasPrompted,
285
+ hasProjects: orgResult.projects.length > 0,
286
+ });
287
+ },
288
+ },
289
+ {
290
+ key: 'project',
291
+ shouldSkip: (results) => results.scopeType === 'organization',
292
+ prompt: async (results) => {
293
+ const orgResult = results.organization;
294
+ if (!orgResult)
295
+ throw new Error('Organization not resolved');
296
+ return await promptForProjectInOrg({
297
+ projects: orgResult.projects,
298
+ knownProjectId,
299
+ logger,
300
+ allowBack: true,
301
+ });
302
+ },
303
+ },
304
+ {
305
+ key: 'stack',
306
+ prompt: async (results) => {
307
+ const scopeType = results.scopeType;
308
+ if (!scopeType)
309
+ throw new Error('Scope type not resolved');
310
+ let scopeId;
311
+ if (scopeType === 'organization') {
312
+ const orgResult = results.organization;
313
+ scopeId = orgResult.organization.id;
314
+ }
315
+ else {
316
+ const project = results.project;
317
+ scopeId = project.projectId;
318
+ }
319
+ return await promptForStack({
320
+ scopeType,
321
+ scopeId,
322
+ token,
323
+ logger,
324
+ allowBack: true,
325
+ });
326
+ },
327
+ },
328
+ ];
329
+ const result = await runWizard(steps);
330
+ const orgResult = result.organization;
331
+ const scopeType = result.scopeType;
332
+ const stack = result.stack;
333
+ let scopeId;
334
+ let displayName;
335
+ if (scopeType === 'organization') {
336
+ scopeId = orgResult.organization.id;
337
+ displayName = orgResult.organization.name;
338
+ }
339
+ else {
340
+ const project = result.project;
341
+ scopeId = project.projectId;
342
+ displayName = project.displayName;
343
+ }
344
+ return {
345
+ scopeType,
346
+ scopeId,
347
+ displayName,
348
+ stackId: stack.stackId,
349
+ stackName: stack.name,
350
+ };
351
+ }