@sanity/runtime-cli 14.13.4 → 15.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 (63) hide show
  1. package/README.md +215 -118
  2. package/dist/actions/blueprints/logs.d.ts +2 -1
  3. package/dist/actions/blueprints/logs.js +4 -5
  4. package/dist/actions/blueprints/resources.js +1 -0
  5. package/dist/actions/blueprints/stacks.d.ts +3 -1
  6. package/dist/actions/blueprints/stacks.js +11 -2
  7. package/dist/actions/functions/test.js +1 -1
  8. package/dist/actions/sanity/access.d.ts +38 -0
  9. package/dist/actions/sanity/access.js +23 -0
  10. package/dist/actions/sanity/projects.d.ts +1 -1
  11. package/dist/baseCommands.d.ts +2 -0
  12. package/dist/baseCommands.js +3 -0
  13. package/dist/commands/blueprints/add.js +1 -1
  14. package/dist/commands/blueprints/deploy.d.ts +2 -0
  15. package/dist/commands/blueprints/deploy.js +6 -2
  16. package/dist/commands/blueprints/destroy.js +0 -2
  17. package/dist/commands/blueprints/info.d.ts +1 -0
  18. package/dist/commands/blueprints/info.js +3 -1
  19. package/dist/commands/blueprints/init.js +2 -0
  20. package/dist/commands/blueprints/logs.d.ts +5 -0
  21. package/dist/commands/blueprints/logs.js +26 -3
  22. package/dist/commands/blueprints/mint-deploy-token.d.ts +14 -0
  23. package/dist/commands/blueprints/mint-deploy-token.js +47 -0
  24. package/dist/commands/blueprints/plan.d.ts +2 -0
  25. package/dist/commands/blueprints/plan.js +8 -2
  26. package/dist/commands/blueprints/promote.d.ts +2 -1
  27. package/dist/commands/blueprints/promote.js +7 -2
  28. package/dist/commands/blueprints/stacks.js +1 -1
  29. package/dist/commands/functions/add.js +1 -1
  30. package/dist/cores/blueprints/config.js +23 -29
  31. package/dist/cores/blueprints/init.js +99 -76
  32. package/dist/cores/blueprints/logs.d.ts +3 -0
  33. package/dist/cores/blueprints/logs.js +15 -9
  34. package/dist/cores/blueprints/mint-deploy-token.d.ts +16 -0
  35. package/dist/cores/blueprints/mint-deploy-token.js +123 -0
  36. package/dist/cores/blueprints/promote.d.ts +1 -0
  37. package/dist/cores/blueprints/promote.js +22 -1
  38. package/dist/cores/functions/add.js +4 -5
  39. package/dist/cores/index.d.ts +1 -2
  40. package/dist/cores/index.js +0 -2
  41. package/dist/index.d.ts +0 -18
  42. package/dist/index.js +0 -20
  43. package/dist/utils/clipboard.d.ts +14 -0
  44. package/dist/utils/clipboard.js +73 -0
  45. package/dist/utils/display/errors.d.ts +5 -1
  46. package/dist/utils/display/prompt.d.ts +55 -15
  47. package/dist/utils/display/prompt.js +271 -45
  48. package/oclif.manifest.json +320 -18
  49. package/package.json +21 -67
  50. package/dist/actions/blueprints/index.d.ts +0 -16
  51. package/dist/actions/blueprints/index.js +0 -10
  52. package/dist/actions/functions/index.d.ts +0 -4
  53. package/dist/actions/functions/index.js +0 -4
  54. package/dist/actions/sanity/index.d.ts +0 -1
  55. package/dist/actions/sanity/index.js +0 -1
  56. package/dist/cores/blueprints/index.d.ts +0 -20
  57. package/dist/cores/blueprints/index.js +0 -10
  58. package/dist/cores/functions/index.d.ts +0 -16
  59. package/dist/cores/functions/index.js +0 -8
  60. package/dist/utils/display/index.d.ts +0 -5
  61. package/dist/utils/display/index.js +0 -5
  62. package/dist/utils/index.d.ts +0 -8
  63. package/dist/utils/index.js +0 -8
@@ -1,7 +1,7 @@
1
1
  import { patchConfigFile, writeConfigFile, } from '../../actions/blueprints/config.js';
2
2
  import { resolveStackIdByNameOrId } from '../../actions/blueprints/stacks.js';
3
3
  import { filePathRelativeToCwd, labeledId, warn } from '../../utils/display/presenters.js';
4
- import { promptForProject, promptForStack } from '../../utils/display/prompt.js';
4
+ import { runScopeAndStackWizard } from '../../utils/display/prompt.js';
5
5
  import { styleText } from '../../utils/style-text.js';
6
6
  export async function blueprintConfigCore(options) {
7
7
  const { bin = 'sanity', blueprint, log, token, flags } = options;
@@ -77,35 +77,29 @@ export async function blueprintConfigCore(options) {
77
77
  }
78
78
  }
79
79
  }
80
- // prompt for values interactively
81
- // do not yet offer organization as scope option
82
- let updatedProjectId = flagProjectId;
83
- if (!updatedProjectId) {
84
- const pickedProject = await promptForProject({
85
- token,
86
- knownProjectId: configScopeId,
87
- logger: log,
88
- });
89
- updatedProjectId = pickedProject.projectId;
90
- }
91
- if (!updatedProjectId)
92
- return { success: false, error: 'Project ID is required.' };
93
- let updatedStackId;
94
- if (flagStack) {
95
- updatedStackId = await resolveStackIdByNameOrId(flagStack, { token, scopeType: 'project', scopeId: updatedProjectId }, log);
96
- }
97
- if (!updatedStackId) {
98
- const pickedStack = await promptForStack({ projectId: updatedProjectId, token, logger: log });
99
- updatedStackId = pickedStack.stackId;
100
- }
101
- if (!updatedStackId)
102
- return { success: false, error: 'Stack is required.' };
80
+ // prompt for values interactively via wizard: org -> scope -> project (if needed) -> stack
81
+ log('');
82
+ const knownOrganizationId = configScopeType === 'organization' ? configScopeId : undefined;
83
+ const knownProjectId = configScopeType === 'project' ? configScopeId : undefined;
84
+ const wizardResult = await runScopeAndStackWizard({
85
+ token,
86
+ knownOrganizationId,
87
+ knownProjectId,
88
+ logger: log,
89
+ });
90
+ if (!wizardResult.scopeId)
91
+ return { success: false, error: 'Scope selection is required.' };
103
92
  try {
104
- // update or create config JSON
105
- const newConfig = writeConfigFile(blueprintFilePath, {
106
- projectId: updatedProjectId,
107
- stackId: updatedStackId,
108
- });
93
+ // update or create config JSON with correct scope fields
94
+ const configUpdate = { stackId: wizardResult.stackId };
95
+ if (wizardResult.scopeType === 'organization') {
96
+ configUpdate.organizationId = wizardResult.scopeId;
97
+ }
98
+ else {
99
+ configUpdate.projectId = wizardResult.scopeId;
100
+ }
101
+ const newConfig = writeConfigFile(blueprintFilePath, configUpdate);
102
+ log('');
109
103
  printConfig({ configLabel: hasConfigFile ? 'Updated' : 'New', log, config: newConfig });
110
104
  return { success: true, json: { config: newConfig } };
111
105
  }
@@ -3,18 +3,24 @@ import { join } from 'node:path';
3
3
  import { confirm } from '@inquirer/prompts';
4
4
  import { findBlueprintFile, readLocalBlueprint, writeBlueprintToDisk, } from '../../actions/blueprints/blueprint.js';
5
5
  import { writeConfigFile } from '../../actions/blueprints/config.js';
6
- import { createEmptyStack } from '../../actions/blueprints/stacks.js';
6
+ import { createEmptyStack, getStack } from '../../actions/blueprints/stacks.js';
7
7
  import { writeGitignoreFile } from '../../actions/git.js';
8
8
  import { writeOrUpdateNodeDependency } from '../../actions/node.js';
9
9
  import { verifyExampleExists, writeExample } from '../../actions/sanity/examples.js';
10
10
  import { getProject } from '../../actions/sanity/projects.js';
11
11
  import { BLUEPRINT_CONFIG_DIR, BLUEPRINT_CONFIG_FILE } from '../../config.js';
12
12
  import { check, filePathRelativeToCwd, labeledId, warn } from '../../utils/display/presenters.js';
13
- import { promptForBlueprintType, promptForProject, promptForStack, } from '../../utils/display/prompt.js';
13
+ import { promptForBlueprintType, promptForOrganization, promptForProjectInOrg, promptForStack, runScopeAndStackWizard, } from '../../utils/display/prompt.js';
14
14
  import { styleText } from '../../utils/style-text.js';
15
15
  import { blueprintConfigCore } from './config.js';
16
- const SCOPE_PROJECT = 'project';
17
- const SCOPE_ORGANIZATION = 'organization';
16
+ /** Derive scope type and ID from mutually exclusive flag values. */
17
+ function resolveScopeFromFlags(projectId, organizationId) {
18
+ if (projectId)
19
+ return { scopeType: 'project', scopeId: projectId };
20
+ if (organizationId)
21
+ return { scopeType: 'organization', scopeId: organizationId };
22
+ return undefined;
23
+ }
18
24
  export async function blueprintInitCore(options) {
19
25
  const { bin = 'sanity', log, token, knownProjectId, args, flags, validateResources } = options;
20
26
  const { dir: flagDir, example: flagExample, 'blueprint-type': flagBlueprintType, 'project-id': flagProjectId, 'organization-id': flagOrganizationId, 'stack-id': flagStackId, 'stack-name': flagStackName, verbose = false, } = flags;
@@ -87,23 +93,15 @@ async function handleExistingBlueprint(options) {
87
93
  error: 'Flag --example cannot be used with an existing Blueprint.',
88
94
  };
89
95
  }
90
- // existing blueprint file, user wants to configure a stack
91
- // --stack-name means they want a new stack created
96
+ // --stack-name with a scope flag means create a new stack before configuring
92
97
  let resolvedStackId = flagStackId;
93
98
  if (!resolvedStackId && flagStackName) {
94
- const scopeId = flagProjectId || flagOrganizationId;
95
- let scopeType;
96
- if (flagProjectId)
97
- scopeType = SCOPE_PROJECT;
98
- else if (flagOrganizationId)
99
- scopeType = SCOPE_ORGANIZATION;
100
- if (scopeType && scopeId) {
101
- // have what we need to create a new stack
102
- log(`\nCreating new Stack "${flagStackName}" scoped to ${labeledId(scopeType, scopeId)}`);
99
+ const scope = resolveScopeFromFlags(flagProjectId, flagOrganizationId);
100
+ if (scope) {
101
+ log(`\nCreating new Stack "${flagStackName}" scoped to ${labeledId(scope.scopeType, scope.scopeId)}`);
103
102
  const stack = await createEmptyStack({
104
103
  token,
105
- scopeType,
106
- scopeId,
104
+ ...scope,
107
105
  name: flagStackName,
108
106
  logger: log,
109
107
  });
@@ -168,8 +166,11 @@ export function validateFlags(flags) {
168
166
  if (stackId && stackName) {
169
167
  return { success: false, error: 'Cannot specify both --stack-id and --stack-name' };
170
168
  }
171
- if (organizationId && projectId) {
172
- return { success: false, error: 'Cannot specify both --organization-id and --project-id' };
169
+ if ((stackId || stackName) && !organizationId && !projectId) {
170
+ return {
171
+ success: false,
172
+ error: `${stackId ? '--stack-id' : '--stack-name'} requires --project-id or --organization-id`,
173
+ };
173
174
  }
174
175
  return null;
175
176
  }
@@ -184,10 +185,20 @@ async function handleExampleInitialization(options) {
184
185
  if (!exampleExists) {
185
186
  return { success: false, error: `Blueprint example "${exampleName}" does not exist.` };
186
187
  }
187
- const resolvedProjectId = projectId || (await promptForProject({ token, logger: log })).projectId;
188
+ let resolvedProjectId = projectId;
189
+ if (!resolvedProjectId) {
190
+ // examples are project-scoped; use the composable prompts to pick an org then a project
191
+ const orgResult = await promptForOrganization({ token, logger: log });
192
+ if (typeof orgResult === 'symbol')
193
+ throw new Error('Unexpected back navigation');
194
+ const picked = await promptForProjectInOrg({ projects: orgResult.projects, logger: log });
195
+ if (typeof picked === 'symbol')
196
+ throw new Error('Unexpected back navigation');
197
+ resolvedProjectId = picked.projectId;
198
+ }
188
199
  const stack = await createEmptyStack({
189
200
  token,
190
- scopeType: SCOPE_PROJECT,
201
+ scopeType: 'project',
191
202
  scopeId: resolvedProjectId,
192
203
  name: `example-${exampleName}`,
193
204
  logger: log,
@@ -225,69 +236,81 @@ async function handleExampleInitialization(options) {
225
236
  }
226
237
  export async function resolveScopeAndStack(params) {
227
238
  const { projectId, organizationId, stackId, stackName, knownProjectId, log, token } = params;
228
- let scopeType = SCOPE_PROJECT;
229
- let scopeId;
230
- if (projectId) {
231
- scopeType = SCOPE_PROJECT;
232
- scopeId = projectId;
233
- }
234
- else if (organizationId) {
235
- scopeType = SCOPE_ORGANIZATION;
236
- scopeId = organizationId;
239
+ const flagScope = resolveScopeFromFlags(projectId, organizationId);
240
+ // --- Branch 1: Scope provided via flags ---
241
+ if (flagScope) {
242
+ const { scopeType, scopeId } = flagScope;
243
+ let resolvedStackId = stackId;
244
+ // --stack-name: create a new stack
245
+ if (!resolvedStackId && stackName) {
246
+ log(`\nCreating new Stack "${stackName}" scoped to ${labeledId(scopeType, scopeId)}`);
247
+ const stack = await createEmptyStack({
248
+ token,
249
+ scopeType,
250
+ scopeId,
251
+ name: stackName,
252
+ logger: log,
253
+ });
254
+ resolvedStackId = stack.id;
255
+ }
256
+ // --stack-id: validate it exists
257
+ if (resolvedStackId && resolvedStackId === stackId) {
258
+ const spinner = log.ora('Validating Stack...').start();
259
+ const { ok, error } = await getStack({
260
+ stackId: resolvedStackId,
261
+ auth: { token, scopeType, scopeId },
262
+ logger: log,
263
+ });
264
+ spinner.stop();
265
+ if (!ok) {
266
+ throw new Error(error || `Stack "${resolvedStackId}" not found for ${scopeType} "${scopeId}".`);
267
+ }
268
+ }
269
+ // No stack flag: prompt for stack selection
270
+ if (!resolvedStackId) {
271
+ log('\nBlueprints are deployed to a "Stack".');
272
+ const stackResult = await promptForStack({ scopeType, scopeId, token, logger: log });
273
+ if (typeof stackResult === 'symbol')
274
+ throw new Error('Unexpected back navigation');
275
+ resolvedStackId = stackResult.stackId;
276
+ }
277
+ return { scopeType, scopeId, stackId: resolvedStackId };
237
278
  }
238
- let resolvedStackId = stackId;
239
- if (!resolvedStackId && stackName && scopeType && scopeId) {
240
- // sending stackName will assume you want to create a new stack
241
- // essentially the only way to create an org-scoped stack
242
- log(`\nCreating new Stack "${stackName}" scoped to ${labeledId(scopeType, scopeId)}`);
243
- const stack = await createEmptyStack({
279
+ // --- Branch 2: No scope flags -- interactive resolution ---
280
+ log('\nBlueprints are scoped to an organization or project.');
281
+ log(styleText('dim', 'Scope determines which resources your Blueprint can manage.\n' +
282
+ 'Organization scope covers all projects; project scope is limited to one.'));
283
+ // Offer the CLI's configured project as a shortcut
284
+ if (knownProjectId) {
285
+ const { ok, project } = await getProject({
244
286
  token,
245
- scopeType,
246
- scopeId,
247
- name: stackName,
287
+ scopeId: knownProjectId,
288
+ scopeType: 'project',
248
289
  logger: log,
249
290
  });
250
- resolvedStackId = stack.id;
251
- }
252
- if (!scopeId) {
253
- log('\nBlueprints are associated with a Sanity project.');
254
- // If we have a CLI project ID, offer it as a suggestion
255
- if (knownProjectId) {
256
- const { ok, project } = await getProject({
257
- token,
258
- scopeId: knownProjectId,
259
- scopeType: 'project',
260
- logger: log,
291
+ if (ok && project) {
292
+ const useCliProject = await confirm({
293
+ message: `The CLI is configured to use "${project.displayName}" (${knownProjectId}). Use this for the blueprint?`,
294
+ default: true,
261
295
  });
262
- if (ok && project) {
263
- const useCliProject = await confirm({
264
- message: `The CLI is configured to use "${project.displayName}" (${knownProjectId}). Use this for the blueprint?`,
265
- default: true,
296
+ if (useCliProject) {
297
+ // Scope locked to project; just need a stack
298
+ log('\nBlueprints are deployed to a "Stack".');
299
+ const stackResult = await promptForStack({
300
+ scopeType: 'project',
301
+ scopeId: knownProjectId,
302
+ token,
303
+ logger: log,
266
304
  });
267
- if (useCliProject) {
268
- scopeType = SCOPE_PROJECT;
269
- scopeId = knownProjectId;
270
- }
305
+ if (typeof stackResult === 'symbol')
306
+ throw new Error('Unexpected back navigation');
307
+ return { scopeType: 'project', scopeId: knownProjectId, stackId: stackResult.stackId };
271
308
  }
272
309
  }
273
- // If still no scope (no knownProjectId, lookup failed, or user declined), prompt for selection
274
- if (!scopeId) {
275
- log('Select a project:');
276
- const pickedProject = await promptForProject({ token, logger: log });
277
- scopeType = SCOPE_PROJECT;
278
- scopeId = pickedProject.projectId;
279
- }
280
310
  }
281
- if (!resolvedStackId) {
282
- log('\nBlueprints are deployed to a "Stack".');
283
- const { stackId } = await promptForStack({ projectId: scopeId, token, logger: log });
284
- resolvedStackId = stackId;
285
- }
286
- return {
287
- scopeType,
288
- scopeId,
289
- stackId: resolvedStackId,
290
- };
311
+ // Full wizard: org -> scope -> project (if needed) -> stack
312
+ const wizard = await runScopeAndStackWizard({ token, logger: log });
313
+ return { scopeType: wizard.scopeType, scopeId: wizard.scopeId, stackId: wizard.stackId };
291
314
  }
292
315
  export async function determineBlueprintExtension(params) {
293
316
  const { requestedType, blueprintDir, log } = params;
@@ -329,7 +352,7 @@ export async function createBlueprintFiles(params) {
329
352
  log(check(`${styleText('bold', 'Created Blueprint:')} ${displayPath}/${blueprintFileName}`));
330
353
  writeConfigFile(blueprintFilePath, {
331
354
  stackId,
332
- ...(scopeType === SCOPE_ORGANIZATION ? { organizationId: scopeId } : { projectId: scopeId }),
355
+ ...(scopeType === 'organization' ? { organizationId: scopeId } : { projectId: scopeId }),
333
356
  });
334
357
  log(check(`${styleText('bold', 'Added configuration:')} ${displayPath}/${BLUEPRINT_CONFIG_DIR}/${BLUEPRINT_CONFIG_FILE}`));
335
358
  const gitignoreResult = writeGitignoreFile(blueprintFilePath);
@@ -7,6 +7,9 @@ export interface BlueprintLogsOptions extends CoreConfig {
7
7
  flags: {
8
8
  watch?: boolean;
9
9
  verbose?: boolean;
10
+ limit?: number;
11
+ since?: string;
12
+ before?: string;
10
13
  };
11
14
  }
12
15
  export declare function blueprintLogsCore(options: BlueprintLogsOptions): Promise<CoreResult>;
@@ -1,16 +1,18 @@
1
- import { getLogs, getRecentLogs } from '../../actions/blueprints/logs.js';
1
+ import { getLogs } from '../../actions/blueprints/logs.js';
2
2
  import { setupLogPolling } from '../../actions/blueprints/logs-polling.js';
3
3
  import { formatTitle } from '../../utils/display/blueprints-formatting.js';
4
4
  import { formatLogEntry, formatLogs } from '../../utils/display/logs-formatting.js';
5
5
  import { niceId } from '../../utils/display/presenters.js';
6
6
  import { styleText } from '../../utils/style-text.js';
7
+ /** Number of recent logs to show before entering watch mode */
8
+ const WATCH_PREFETCH_LIMIT = 10;
7
9
  export async function blueprintLogsCore(options) {
8
10
  const { log, auth, stackId, deployedStack, flags } = options;
9
- const { watch = false, verbose = false } = flags;
11
+ const { watch = false, verbose = false, limit, since, before } = flags;
10
12
  const spinner = log.ora(`Fetching recent logs for Stack deployment ${niceId(stackId)}`).start();
11
13
  try {
12
14
  if (watch) {
13
- const { ok, logs, error } = await getLogs({ stackId }, auth, log);
15
+ const { ok, logs, error } = await getLogs({ stackId, limit: WATCH_PREFETCH_LIMIT }, auth, log);
14
16
  if (!ok) {
15
17
  spinner.fail(`${styleText('red', 'Failed')} to retrieve logs`);
16
18
  log.error(`Error: ${error || 'Unknown error'}`);
@@ -20,9 +22,10 @@ export async function blueprintLogsCore(options) {
20
22
  log(`${formatTitle('Blueprint', deployedStack.name)} ${niceId(stackId)} logs`);
21
23
  if (logs.length > 0) {
22
24
  log('\nMost recent logs:');
23
- const recentLogs = getRecentLogs(logs);
25
+ // API returns newest first; display in chronological order
26
+ const orderedLogs = [...logs].reverse();
24
27
  let previousLog;
25
- for (const logEntry of recentLogs) {
28
+ for (const logEntry of orderedLogs) {
26
29
  log(formatLogEntry(logEntry, verbose, previousLog));
27
30
  previousLog = logEntry;
28
31
  }
@@ -39,14 +42,14 @@ export async function blueprintLogsCore(options) {
39
42
  showBanner: true,
40
43
  verbose,
41
44
  });
42
- // Return a special key for polling mode
45
+ // Return a never-resolving promise so the polling loop keeps running
43
46
  return {
44
47
  success: true,
45
48
  streaming: new Promise(() => { }),
46
49
  };
47
50
  }
48
- // Regular non-streaming logs
49
- const { ok, logs, error } = await getLogs({ stackId }, auth, log);
51
+ // One-shot fetch (no watch)
52
+ const { ok, logs, error, hasMore } = await getLogs({ stackId, limit, before, after: since }, auth, log);
50
53
  if (!ok) {
51
54
  spinner.fail(`${styleText('red', 'Failed')} to retrieve Stack deployment logs`);
52
55
  log.error(`Error: ${error || 'Unknown error'}`);
@@ -59,7 +62,10 @@ export async function blueprintLogsCore(options) {
59
62
  spinner.succeed(`${formatTitle('Blueprint', deployedStack.name)} Logs`);
60
63
  log(`Found ${styleText('bold', logs.length.toString())} log entries for Stack deployment ${niceId(stackId)}\n`);
61
64
  log(formatLogs(logs, verbose));
62
- return { success: true, json: { logs } };
65
+ if (hasMore) {
66
+ log(`\n${styleText('dim', 'More logs available. Narrow the range with --before <timestamp> or raise --limit (max 500).')}`);
67
+ }
68
+ return { success: true, json: { logs, hasMore } };
63
69
  }
64
70
  catch (err) {
65
71
  spinner.fail('Failed to retrieve Stack deployment logs');
@@ -0,0 +1,16 @@
1
+ import type { AuthParams, ScopeType } from '../../utils/types.js';
2
+ import type { CoreConfig, CoreResult } from '../index.js';
3
+ export interface BlueprintMintDeployTokenOptions extends CoreConfig {
4
+ auth: AuthParams;
5
+ scopeType: ScopeType;
6
+ scopeId: string;
7
+ flags: {
8
+ label?: string;
9
+ print?: boolean;
10
+ json?: boolean;
11
+ verbose?: boolean;
12
+ };
13
+ }
14
+ export declare function isRoleUnavailableError(error: string): boolean;
15
+ export declare function defaultLabel(scopeType: ScopeType): string;
16
+ export declare function blueprintMintDeployTokenCore(options: BlueprintMintDeployTokenOptions): Promise<CoreResult>;
@@ -0,0 +1,123 @@
1
+ import { hostname } from 'node:os';
2
+ import { confirm, input, select } from '@inquirer/prompts';
3
+ import { createRobotToken } from '../../actions/sanity/access.js';
4
+ import { ClipboardUnavailableError, write as clipboardWrite } from '../../utils/clipboard.js';
5
+ import { styleText } from '../../utils/style-text.js';
6
+ const ROLE_BY_SCOPE = {
7
+ project: 'blueprints-deployer',
8
+ organization: 'blueprints-deployer-robot',
9
+ };
10
+ // Matches Access API's "Role not found: <name>" 404 message.
11
+ export function isRoleUnavailableError(error) {
12
+ return /^\s*role not found\b/i.test(error);
13
+ }
14
+ export function defaultLabel(scopeType) {
15
+ const date = new Date().toISOString().slice(0, 10);
16
+ let host = 'unknown-host';
17
+ try {
18
+ host = hostname();
19
+ }
20
+ catch { }
21
+ return `blueprints-deployer @ ${host} ${date} (${scopeType})`;
22
+ }
23
+ export async function blueprintMintDeployTokenCore(options) {
24
+ const { log, auth, scopeType, scopeId, flags } = options;
25
+ const label = flags.label ?? defaultLabel(scopeType);
26
+ const roleName = ROLE_BY_SCOPE[scopeType];
27
+ const spinner = log.ora('Minting deploy token...').start();
28
+ const { ok, error, robot } = await createRobotToken({
29
+ auth,
30
+ resourceType: scopeType,
31
+ resourceId: scopeId,
32
+ body: {
33
+ label,
34
+ memberships: [{ resourceType: scopeType, resourceId: scopeId, roleNames: [roleName] }],
35
+ },
36
+ logger: log,
37
+ });
38
+ if (!ok || !robot) {
39
+ spinner.fail('Failed to mint deploy token');
40
+ if (error && isRoleUnavailableError(error)) {
41
+ return {
42
+ success: false,
43
+ error: `The "${roleName}" role is not available in this ${scopeType}.`,
44
+ code: 'ROLE_UNAVAILABLE',
45
+ suggestions: [],
46
+ };
47
+ }
48
+ const suggestions = [];
49
+ if (error && /access|permission|forbidden|role/i.test(error)) {
50
+ suggestions.push(scopeType === 'organization'
51
+ ? 'Verify you have organization admin access.'
52
+ : 'Verify you have project admin access.');
53
+ }
54
+ return { success: false, error: error || 'Failed to mint deploy token', suggestions };
55
+ }
56
+ spinner.stop().clear();
57
+ if (flags.print) {
58
+ log(robot.token);
59
+ return { success: true, json: serializeRobot(robot) };
60
+ }
61
+ if (flags.json) {
62
+ return { success: true, json: serializeRobot(robot) };
63
+ }
64
+ log(styleText('green', `Minted "${robot.label}" (${robot.id})`));
65
+ if (robot.expiresAt) {
66
+ log(styleText('dim', `Expires: ${robot.expiresAt}`));
67
+ }
68
+ const action = await select({
69
+ message: 'What would you like to do with the token?',
70
+ choices: [
71
+ { name: 'Copy to clipboard', value: 'copy' },
72
+ { name: 'Print to terminal', value: 'print' },
73
+ { name: 'Exit (token will be lost)', value: 'exit' },
74
+ ],
75
+ default: 'copy',
76
+ });
77
+ if (action === 'copy') {
78
+ try {
79
+ await clipboardWrite(robot.token);
80
+ log(styleText('green', 'Token copied to clipboard.'));
81
+ }
82
+ catch (err) {
83
+ const msg = err instanceof ClipboardUnavailableError
84
+ ? err.message
85
+ : err instanceof Error
86
+ ? err.message
87
+ : String(err);
88
+ log.warn(styleText('yellow', `Could not copy to clipboard: ${msg}`));
89
+ const printInstead = await confirm({
90
+ message: 'Print the token to the terminal instead?',
91
+ default: false,
92
+ });
93
+ if (printInstead) {
94
+ await printAndConfirm(log, robot.token);
95
+ }
96
+ else {
97
+ log(styleText('yellow', 'Token discarded. You can mint a new one anytime.'));
98
+ }
99
+ }
100
+ }
101
+ else if (action === 'print') {
102
+ await printAndConfirm(log, robot.token);
103
+ }
104
+ else {
105
+ log(styleText('yellow', 'Token discarded. You can mint a new one anytime.'));
106
+ }
107
+ return { success: true, json: serializeRobot(robot) };
108
+ }
109
+ /**
110
+ * Show the token inside an inquirer prompt and rely on `clearPromptOnDone`
111
+ * so inquirer wipes the entire rendered block (token included) on Enter.
112
+ */
113
+ async function printAndConfirm(log, token) {
114
+ await input({
115
+ message: `\n${token}\n\nPress Enter once you have copied the token (it will be erased):`,
116
+ default: '',
117
+ theme: { prefix: '' },
118
+ }, { clearPromptOnDone: true });
119
+ log(styleText('dim', 'Token erased.'));
120
+ }
121
+ function serializeRobot(robot) {
122
+ return robot;
123
+ }
@@ -3,6 +3,7 @@ export interface BlueprintPromoteOptions extends DeployedBlueprintConfig {
3
3
  flags: {
4
4
  force?: boolean;
5
5
  verbose?: boolean;
6
+ 'new-stack-name'?: string;
6
7
  };
7
8
  }
8
9
  export declare function blueprintPromoteCore(options: BlueprintPromoteOptions): Promise<CoreResult>;
@@ -4,10 +4,23 @@ import { promoteStack } from '../../actions/blueprints/stacks.js';
4
4
  import { niceId } from '../../utils/display/presenters.js';
5
5
  export async function blueprintPromoteCore(options) {
6
6
  const { bin = 'sanity', log, stackId, auth, flags, deployedStack, blueprint } = options;
7
+ const newStackName = flags['new-stack-name'];
8
+ if (newStackName && deployedStack.scopeType === 'organization') {
9
+ return {
10
+ success: false,
11
+ error: '--new-stack-name can only be used when promoting a project-scoped Stack to organization scope.',
12
+ suggestions: [
13
+ `Stack "${deployedStack.name}" ${niceId(deployedStack.id)} is already org-scoped.`,
14
+ ],
15
+ };
16
+ }
7
17
  let message = `"${deployedStack.name}" ${niceId(deployedStack.id)}`;
8
18
  if (deployedStack.scopeType === 'organization') {
9
19
  message = `Stack ${message} is already org-scoped. Promote again?`;
10
20
  }
21
+ else if (newStackName) {
22
+ message = `Promote Stack ${message} from project to organization scope and rename to "${newStackName}"? You will need admin access to your organization.`;
23
+ }
11
24
  else {
12
25
  message = `Promote Stack ${message} from project to organization scope? You will need admin access to your organization.`;
13
26
  }
@@ -19,10 +32,18 @@ export async function blueprintPromoteCore(options) {
19
32
  }
20
33
  try {
21
34
  const spinner = log.ora('Promoting Stack...').start();
22
- const { ok, error, stack } = await promoteStack({ stackId, auth, logger: log });
35
+ const { ok, error, stack, response } = await promoteStack({
36
+ stackId,
37
+ auth,
38
+ logger: log,
39
+ name: newStackName,
40
+ });
23
41
  if (!ok) {
24
42
  spinner.fail('Failed to promote Stack');
25
43
  const suggestions = [];
44
+ if (response?.status === 409) {
45
+ suggestions.push('A Stack with this name already exists in the target organization. Use --new-stack-name <name> to rename while promoting.');
46
+ }
26
47
  if (error && /access|permission|forbidden/i.test(error)) {
27
48
  suggestions.push('Verify you have organization admin access.');
28
49
  }
@@ -5,9 +5,7 @@ import { checkbox, confirm, input, select } from '@inquirer/prompts';
5
5
  import { highlight } from 'cardinal';
6
6
  import { createFunctionResource } from '../../actions/blueprints/resources.js';
7
7
  import { verifyExampleExists, writeExample } from '../../actions/sanity/examples.js';
8
- import { EVENT_DOCUMENT_CREATE, EVENT_DOCUMENT_DELETE, EVENT_DOCUMENT_UPDATE, EVENT_MEDIA_LIBRARY_ASSET_CREATE, EVENT_MEDIA_LIBRARY_ASSET_DELETE, EVENT_MEDIA_LIBRARY_ASSET_UPDATE, EVENT_SYNC_TAG_INVALIDATE,
9
- // EVENT_SCHEDULED,
10
- FUNCTION_TYPES, LABEL_DOCUMENT_CREATE, LABEL_DOCUMENT_DELETE, LABEL_DOCUMENT_UPDATE, LABEL_MEDIA_LIBRARY_ASSET_CREATE, LABEL_MEDIA_LIBRARY_ASSET_DELETE, LABEL_MEDIA_LIBRARY_ASSET_UPDATE, LABEL_SCHEDULED, LABEL_SYNC_TAG_INVALIDATE, MAP_EVENT_TO_FUNCTION_TYPE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, } from '../../constants.js';
8
+ import { EVENT_DOCUMENT_CREATE, EVENT_DOCUMENT_DELETE, EVENT_DOCUMENT_UPDATE, EVENT_MEDIA_LIBRARY_ASSET_CREATE, EVENT_MEDIA_LIBRARY_ASSET_DELETE, EVENT_MEDIA_LIBRARY_ASSET_UPDATE, EVENT_SCHEDULED, EVENT_SYNC_TAG_INVALIDATE, FUNCTION_TYPES, LABEL_DOCUMENT_CREATE, LABEL_DOCUMENT_DELETE, LABEL_DOCUMENT_UPDATE, LABEL_MEDIA_LIBRARY_ASSET_CREATE, LABEL_MEDIA_LIBRARY_ASSET_DELETE, LABEL_MEDIA_LIBRARY_ASSET_UPDATE, LABEL_SCHEDULED, LABEL_SYNC_TAG_INVALIDATE, MAP_EVENT_TO_FUNCTION_TYPE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, } from '../../constants.js';
11
9
  import { check, indent, warn } from '../../utils/display/presenters.js';
12
10
  import { styleText } from '../../utils/style-text.js';
13
11
  import { validateFunctionName } from '../../utils/validate/resource.js';
@@ -152,7 +150,8 @@ export async function functionAddCore(options) {
152
150
  if (fnTypes.length === 0) {
153
151
  throw new Error('At least one function type must be provided.');
154
152
  }
155
- if (!fnTypes.every((evt) => FUNCTION_TYPES.includes(evt))) {
153
+ const validTypes = new Set(FUNCTION_TYPES);
154
+ if (!fnTypes.every((evt) => validTypes.has(evt))) {
156
155
  throw new Error(`Invalid function type. Must be one of: ${FUNCTION_TYPES.join(', ').trim()}`);
157
156
  }
158
157
  const eventSources = new Set(fnTypes.map((t) => t.substring(0, t.lastIndexOf('-'))));
@@ -241,8 +240,8 @@ async function promptForFunctionType() {
241
240
  { name: LABEL_MEDIA_LIBRARY_ASSET_CREATE, value: EVENT_MEDIA_LIBRARY_ASSET_CREATE },
242
241
  { name: LABEL_MEDIA_LIBRARY_ASSET_UPDATE, value: EVENT_MEDIA_LIBRARY_ASSET_UPDATE },
243
242
  { name: LABEL_MEDIA_LIBRARY_ASSET_DELETE, value: EVENT_MEDIA_LIBRARY_ASSET_DELETE },
243
+ { name: LABEL_SCHEDULED, value: EVENT_SCHEDULED },
244
244
  { name: LABEL_SYNC_TAG_INVALIDATE, value: EVENT_SYNC_TAG_INVALIDATE },
245
- // {name: LABEL_SCHEDULED, value: EVENT_SCHEDULED},
246
245
  ],
247
246
  validate(choices) {
248
247
  if (choices.length === 0) {
@@ -1,8 +1,6 @@
1
1
  import { type ReadBlueprintResult } from '../actions/blueprints/blueprint.js';
2
2
  import type { Logger } from '../utils/logger.js';
3
3
  import type { AuthParams, Result, ScopeType, Stack } from '../utils/types.js';
4
- export * as blueprintsCores from './blueprints/index.js';
5
- export * as functionsCores from './functions/index.js';
6
4
  export interface CoreConfig {
7
5
  /** The CLI binary name. */
8
6
  bin: string;
@@ -67,3 +65,4 @@ export declare function initDeployedBlueprintConfig(config: Partial<BlueprintCon
67
65
  validateToken?: boolean;
68
66
  stackOverride?: string;
69
67
  }): Promise<Result<DeployedBlueprintConfig>>;
68
+ export {};
@@ -2,8 +2,6 @@ import { readLocalBlueprint } from '../actions/blueprints/blueprint.js';
2
2
  import { getStack, resolveStackIdByNameOrId } from '../actions/blueprints/stacks.js';
3
3
  import { presentBlueprintParserErrors } from '../utils/display/errors.js';
4
4
  import { validTokenOrErrorMessage } from '../utils/validated-token.js';
5
- export * as blueprintsCores from './blueprints/index.js';
6
- export * as functionsCores from './functions/index.js';
7
5
  export async function initBlueprintConfig({ bin, log, token, validateResources = false, validateToken = true, blueprintPath, }) {
8
6
  let checkedToken = token;
9
7
  if (!token || (token && validateToken)) {