@sanity/runtime-cli 17.10.0 → 17.11.1-next.gc1820a0

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.
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
3
  import { cwd } from 'node:process';
4
4
  import { confirm } from '@inquirer/prompts';
5
5
  import { findBlueprintFile, readLocalBlueprint, writeBlueprintToDisk, } from '../../actions/blueprints/blueprint.js';
@@ -11,8 +11,9 @@ import { verifyExampleExists, writeExample } from '../../actions/sanity/examples
11
11
  import { getProject } from '../../actions/sanity/projects.js';
12
12
  import { BLUEPRINT_CONFIG_DIR, BLUEPRINT_CONFIG_FILE } from '../../config.js';
13
13
  import { check, filePathRelativeToCwd, labeledId, warn } from '../../utils/display/presenters.js';
14
- import { promptForBlueprintType, promptForOrganization, promptForProjectInOrg, promptForStack, runScopeAndStackWizard, } from '../../utils/display/prompt.js';
14
+ import { CancelledError, promptForBlueprintType, promptForInitLocation, promptForOrganization, promptForProjectInOrg, promptForStack, runScopeAndStackWizard, } from '../../utils/display/prompt.js';
15
15
  import { isInteractive } from '../../utils/interactive.js';
16
+ import { installCommand, runCommand } from '../../utils/package-manager.js';
16
17
  import { styleText } from '../../utils/style-text.js';
17
18
  import { blueprintConfigCore } from './config.js';
18
19
  /** Derive scope type and ID from mutually exclusive flag values. */
@@ -29,8 +30,8 @@ export async function blueprintInitCore(options) {
29
30
  const { dir: argDir } = args;
30
31
  const explicitDir = argDir || flagDir;
31
32
  const isHere = !explicitDir || explicitDir === '.' || explicitDir === './';
32
- const blueprintDir = isHere ? '.' : explicitDir;
33
- const userProvidedDirName = isHere ? undefined : explicitDir;
33
+ let blueprintDir = isHere ? '.' : explicitDir;
34
+ let userProvidedDirName = isHere ? undefined : explicitDir;
34
35
  const validationError = validateFlags({
35
36
  stackId: flagStackId,
36
37
  stackName: flagStackName,
@@ -43,7 +44,7 @@ export async function blueprintInitCore(options) {
43
44
  const existingBlueprintFile = findBlueprintFile(blueprintDir);
44
45
  // existing blueprint file on disk → configure it for deployment
45
46
  if (existingBlueprintFile) {
46
- return handleExistingBlueprint({
47
+ const result = await handleExistingBlueprint({
47
48
  existingBlueprintFile,
48
49
  bin,
49
50
  log,
@@ -56,6 +57,11 @@ export async function blueprintInitCore(options) {
56
57
  flagStackName,
57
58
  verbose,
58
59
  });
60
+ // the config core owns the wizard, but the user ran `init`
61
+ if (!result.success && result.cancelled) {
62
+ return { success: false, error: 'Initialization cancelled.', cancelled: true };
63
+ }
64
+ return result;
59
65
  }
60
66
  // --example flag → scaffold from an example template
61
67
  if (flagExample) {
@@ -70,13 +76,21 @@ export async function blueprintInitCore(options) {
70
76
  }
71
77
  // no blueprint on disk → create new blueprint files from scratch
72
78
  if (!explicitDir && isInteractive) {
73
- const proceed = await confirm({
74
- message: `Initialize a Blueprint here in ${cwd()}?`,
75
- default: true,
76
- });
77
- if (!proceed) {
78
- log(`Run \`${bin} blueprints init <directory>\` to initialize in a different location.`);
79
- return { success: false, error: 'Initialization cancelled.' };
79
+ const chosenDir = await promptForInitLocation(cwd());
80
+ if (chosenDir) {
81
+ blueprintDir = chosenDir;
82
+ userProvidedDirName = chosenDir;
83
+ // findBlueprintFile walks up, so only refuse on a manifest in the folder itself
84
+ const existing = findBlueprintFile(chosenDir);
85
+ if (existing && dirname(resolve(existing.blueprintFilePath)) === resolve(chosenDir)) {
86
+ return {
87
+ success: false,
88
+ error: `A Blueprint already exists in "${chosenDir}".`,
89
+ suggestions: [
90
+ `Run \`${runCommand(bin)} blueprints init ${chosenDir}\` to configure it for deployment.`,
91
+ ],
92
+ };
93
+ }
80
94
  }
81
95
  }
82
96
  return handleFreshInitialization({
@@ -94,6 +108,9 @@ export async function blueprintInitCore(options) {
94
108
  });
95
109
  }
96
110
  catch (error) {
111
+ if (error instanceof CancelledError) {
112
+ return { success: false, error: 'Initialization cancelled.', cancelled: true };
113
+ }
97
114
  const errorMessage = error instanceof Error ? error.message : String(error);
98
115
  return { success: false, error: errorMessage };
99
116
  }
@@ -156,6 +173,7 @@ async function handleFreshInitialization(options) {
156
173
  stackId: flagStackId,
157
174
  stackName: flagStackName,
158
175
  knownProjectId,
176
+ blueprintDir,
159
177
  log,
160
178
  token,
161
179
  });
@@ -241,7 +259,7 @@ async function handleExampleInitialization(options) {
241
259
  const { blueprintFilePath } = discoveredBlueprint;
242
260
  writeConfigFile(blueprintFilePath, { projectId: resolvedProjectId, stackId: stack.id });
243
261
  log(check(`${styleText('bold', 'Configured:')} ${exampleDir}/${BLUEPRINT_CONFIG_DIR}/${BLUEPRINT_CONFIG_FILE}`));
244
- log(`\n Run "${styleText(['bold', 'magenta'], `cd ${exampleDir} && npm i`)}" and check out the README`);
262
+ log(`\n Run "${styleText(['bold', 'magenta'], `cd ${exampleDir} && ${installCommand(exampleDir)}`)}" and check out the README`);
245
263
  if (instructions) {
246
264
  log('');
247
265
  log(instructions);
@@ -251,6 +269,7 @@ async function handleExampleInitialization(options) {
251
269
  export async function resolveScopeAndStack(params) {
252
270
  const { projectId, organizationId, stackId, stackName, knownProjectId, log, token } = params;
253
271
  const flagScope = resolveScopeFromFlags(projectId, organizationId);
272
+ const suggestedName = params.blueprintDir ? basename(resolve(params.blueprintDir)) : undefined;
254
273
  // --- Branch 1: Scope provided via flags ---
255
274
  if (flagScope) {
256
275
  const { scopeType, scopeId } = flagScope;
@@ -283,7 +302,13 @@ export async function resolveScopeAndStack(params) {
283
302
  // No stack flag: prompt for stack selection
284
303
  if (!resolvedStackId) {
285
304
  log('\nBlueprints are deployed to a "Stack".');
286
- const stackResult = await promptForStack({ scopeType, scopeId, token, logger: log });
305
+ const stackResult = await promptForStack({
306
+ scopeType,
307
+ scopeId,
308
+ token,
309
+ logger: log,
310
+ suggestedName,
311
+ });
287
312
  if (typeof stackResult === 'symbol')
288
313
  throw new Error('Unexpected back navigation');
289
314
  resolvedStackId = stackResult.stackId;
@@ -315,6 +340,7 @@ export async function resolveScopeAndStack(params) {
315
340
  scopeId: knownProjectId,
316
341
  token,
317
342
  logger: log,
343
+ suggestedName,
318
344
  });
319
345
  if (typeof stackResult === 'symbol')
320
346
  throw new Error('Unexpected back navigation');
@@ -323,7 +349,11 @@ export async function resolveScopeAndStack(params) {
323
349
  }
324
350
  }
325
351
  // Full wizard: org -> scope -> project (if needed) -> stack
326
- const wizard = await runScopeAndStackWizard({ token, logger: log });
352
+ const wizard = await runScopeAndStackWizard({
353
+ token,
354
+ logger: log,
355
+ suggestedStackName: suggestedName,
356
+ });
327
357
  return { scopeType: wizard.scopeType, scopeId: wizard.scopeId, stackId: wizard.stackId };
328
358
  }
329
359
  export async function determineBlueprintExtension(params) {
@@ -386,12 +416,18 @@ export async function createBlueprintFiles(params) {
386
416
  log.warn(warn(`Unable to add ${blueprintsPackage} to your project.`));
387
417
  }
388
418
  }
389
- const nextStepParts = [];
419
+ const runLine = (command, purpose) => ` Run "${styleText(['bold', 'magenta'], command)}" ${purpose}`;
420
+ // project setup, only when there is something to do
421
+ const setupParts = [];
390
422
  if (userProvidedDirName)
391
- nextStepParts.push(`cd ${userProvidedDirName}`);
423
+ setupParts.push(`cd ${userProvidedDirName}`);
392
424
  if (blueprintExtension !== 'json')
393
- nextStepParts.push('npm install');
394
- nextStepParts.push(`npx ${bin} blueprints --help`);
395
- log(`\n Run "${styleText(['bold', 'magenta'], nextStepParts.join(' && '))}" to get started`);
425
+ setupParts.push(installCommand(blueprintDir));
426
+ log('');
427
+ if (setupParts.length > 0)
428
+ log(runLine(setupParts.join(' && '), 'first'));
429
+ // the manifest starts empty, so name the command that fills it
430
+ log(runLine(`${runCommand(bin)} functions add`, 'to add a Function'));
431
+ log(runLine(`${runCommand(bin)} blueprints --help`, 'to see every command'));
396
432
  return { success: true };
397
433
  }
@@ -3,6 +3,7 @@ import { planStack, resolveStackIdByNameOrId } from '../../actions/blueprints/st
3
3
  import { extractDurableConfig } from '../../utils/blueprints/extract-durable-config.js';
4
4
  import { createHintCollector } from '../../utils/blueprints/hints.js';
5
5
  import { formatDeploymentPlan, formatResourceTree, hasActionableChanges, } from '../../utils/display/blueprints-formatting.js';
6
+ import { runCommand } from '../../utils/package-manager.js';
6
7
  import { styleText } from '../../utils/style-text.js';
7
8
  import { getWarnings } from '../../utils/warnings.js';
8
9
  export async function blueprintPlanCore(options) {
@@ -18,12 +19,14 @@ export async function blueprintPlanCore(options) {
18
19
  ? {
19
20
  success: false,
20
21
  error: 'Missing authentication token',
21
- suggestions: [`Run \`npx ${bin} login\` to authenticate.`],
22
+ suggestions: [`Run \`${runCommand(bin)} login\` to authenticate.`],
22
23
  }
23
24
  : {
24
25
  success: false,
25
26
  error: 'Missing Blueprint configuration',
26
- suggestions: [`Run \`npx ${bin} blueprints doctor --fix\` to fix your configuration.`],
27
+ suggestions: [
28
+ `Run \`${runCommand(bin)} blueprints doctor --fix\` to fix your configuration.`,
29
+ ],
27
30
  };
28
31
  }
29
32
  const stackId = flags.stack
@@ -34,7 +37,9 @@ export async function blueprintPlanCore(options) {
34
37
  return {
35
38
  success: false,
36
39
  error: 'Missing Stack deployment configuration',
37
- suggestions: [`Run \`npx ${bin} blueprints doctor --fix\` to fix your configuration.`],
40
+ suggestions: [
41
+ `Run \`${runCommand(bin)} blueprints doctor --fix\` to fix your configuration.`,
42
+ ],
38
43
  };
39
44
  }
40
45
  const auth = { token, scopeType, scopeId };
@@ -69,7 +74,7 @@ export async function blueprintPlanCore(options) {
69
74
  error: 'Deployment plan has problems',
70
75
  suggestions: [
71
76
  ...hints.getSuggestions(),
72
- `Fix the issues above, then run \`npx ${bin} blueprints deploy\`.`,
77
+ `Fix the issues above, then run \`${runCommand(bin)} blueprints plan\` again.`,
73
78
  ],
74
79
  };
75
80
  }
@@ -77,10 +82,10 @@ export async function blueprintPlanCore(options) {
77
82
  log('');
78
83
  log(formatDeploymentPlan(planResponse.deploymentPlan));
79
84
  if (hasActionableChanges(planResponse.deploymentPlan)) {
80
- log(`\n Run "${styleText(['bold', 'magenta'], `npx ${bin} blueprints deploy`)}" to apply these Stack changes`);
85
+ log(`\n Run "${styleText(['bold', 'magenta'], `${runCommand(bin)} blueprints deploy`)}" to apply these Stack changes`);
81
86
  }
82
87
  else {
83
- log(`\n ${styleText('dim', `No significant changes to deploy. Run \`npx ${bin} blueprints deploy\` to apply.`)}`);
88
+ log(`\n ${styleText('dim', `No significant changes to deploy. Run \`${runCommand(bin)} blueprints deploy\` to apply.`)}`);
84
89
  }
85
90
  return {
86
91
  success: true,
@@ -2,6 +2,7 @@ import { confirm } from '@inquirer/prompts';
2
2
  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
+ import { runCommand } from '../../utils/package-manager.js';
5
6
  export async function blueprintPromoteCore(options) {
6
7
  const { bin = 'sanity', log, stackId, auth, flags, deployedStack, blueprint } = options;
7
8
  const newStackName = flags['new-stack-name'];
@@ -47,7 +48,7 @@ export async function blueprintPromoteCore(options) {
47
48
  if (error && /access|permission|forbidden/i.test(error)) {
48
49
  suggestions.push('Verify you have organization admin access.');
49
50
  }
50
- suggestions.push(`Run \`npx ${bin} blueprints info\` to check the current Stack scope.`);
51
+ suggestions.push(`Run \`${runCommand(bin)} blueprints info\` to check the current Stack scope.`);
51
52
  return { success: false, error: error || 'Failed to promote Stack', suggestions };
52
53
  }
53
54
  spinner.stop().clear();
@@ -66,7 +67,7 @@ export async function blueprintPromoteCore(options) {
66
67
  success: false,
67
68
  error: 'Stack promoted, but could not update Blueprint config (.sanity/blueprint.config.json).',
68
69
  suggestions: [
69
- `Run \`npx ${bin} blueprints config --edit\` to update your Blueprint config manually.`,
70
+ `Run \`${runCommand(bin)} blueprints config --edit\` to update your Blueprint config manually.`,
70
71
  ],
71
72
  };
72
73
  }
@@ -8,7 +8,9 @@ import { verifyExampleExists, writeExample } from '../../actions/sanity/examples
8
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_PUBSUB, 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_PUBSUB, LABEL_SCHEDULED, LABEL_SYNC_TAG_INVALIDATE, MAP_EVENT_TO_FUNCTION_TYPE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_PUBSUB, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, } from '../../constants.js';
9
9
  import { check, indent, warn } from '../../utils/display/presenters.js';
10
10
  import { validateFunctionName } from '../../utils/functions/validate-name.js';
11
+ import { detectPackageManager } from '../../utils/package-manager.js';
11
12
  import { styleText } from '../../utils/style-text.js';
13
+ import { INSTALLER_OPTIONS } from '../../utils/types.js';
12
14
  import { getWarnings } from '../../utils/warnings.js';
13
15
  export const generateFunctionBlueprintResourceTemplate = (fnName, eventNames) => {
14
16
  const functionType = MAP_EVENT_TO_FUNCTION_TYPE[eventNames[0]];
@@ -44,7 +46,8 @@ export async function functionAddCore(options) {
44
46
  const root = cwd();
45
47
  const { log, blueprint, flags } = options;
46
48
  const { blueprintFilePath } = blueprint.fileInfo;
47
- const warnings = getWarnings({ dir: dirname(blueprintFilePath) });
49
+ const blueprintFileDir = dirname(blueprintFilePath);
50
+ const warnings = getWarnings({ dir: blueprintFileDir });
48
51
  const { example: flagExample, name: flagResourceName, type: flagType, javascript: flagJs, helpers: flagHelpers, install: flagI, installer: flagInstaller, } = flags;
49
52
  let { language: flagLang } = flags;
50
53
  flagLang = flagJs ? 'js' : flagLang;
@@ -183,7 +186,7 @@ export async function functionAddCore(options) {
183
186
  }
184
187
  else {
185
188
  addHelpers = flagHelpers || (await promptForAddHelpers());
186
- installCommand = addHelpers ? await promptForInstallCommand() : null;
189
+ installCommand = addHelpers ? await promptForInstallCommand(blueprintFileDir) : null;
187
190
  }
188
191
  if (installCommand)
189
192
  log(`${styleText('magenta', 'Installing')} with ${installCommand}...`);
@@ -286,16 +289,17 @@ async function promptForAddHelpers() {
286
289
  });
287
290
  return addHelpers;
288
291
  }
289
- async function promptForInstallCommand() {
292
+ async function promptForInstallCommand(dir) {
293
+ // hydrate only supports these three; a detected bun falls back to npm
294
+ const detected = detectPackageManager(dir);
295
+ const preferred = INSTALLER_OPTIONS.find((option) => option === detected) ?? 'npm';
290
296
  const command = await select({
291
297
  message: 'How to install the @sanity/functions helpers:',
292
298
  choices: [
293
- { name: 'npm', value: 'npm' },
294
- { name: 'pnpm', value: 'pnpm' },
295
- { name: 'yarn', value: 'yarn' },
299
+ ...INSTALLER_OPTIONS.map((option) => ({ name: option, value: option })),
296
300
  { name: 'Skip install', value: null },
297
301
  ],
298
- default: 'npm',
302
+ default: preferred,
299
303
  });
300
304
  return command;
301
305
  }
@@ -1,4 +1,5 @@
1
1
  import { update } from '../../../actions/functions/env/update.js';
2
+ import { ENV_MASK } from '../../../constants.js';
2
3
  import { findFunctionInStack } from '../../../utils/functions/find.js';
3
4
  import { resolveFunctionAuth } from '../../../utils/functions/resolve-auth.js';
4
5
  import { styleText } from '../../../utils/style-text.js';
@@ -17,5 +18,5 @@ export async function functionEnvAddCore(options) {
17
18
  };
18
19
  }
19
20
  spinner.succeed(`Update of ${args.key} succeeded`);
20
- return { success: true, json: { name: args.name, key: args.key, value: args.value } };
21
+ return { success: true, json: { name: args.name, key: args.key, value: ENV_MASK } };
21
22
  }
@@ -58,8 +58,7 @@ async function streamLogs({ name, externalId, auth, log, }) {
58
58
  alreadyOpened = true;
59
59
  };
60
60
  const renderLog = (logEntry) => {
61
- const { time, level, message } = logEntry;
62
- log(formatLog(time, level, message, true));
61
+ log(formatLog(logEntry, true));
63
62
  };
64
63
  streamLogsAction(externalId, auth, renderLog, onOpen, (error) => log.error(`${styleText('red', 'Error:')} ${error}`), log);
65
64
  // Return a special key for streaming mode
@@ -92,27 +91,72 @@ async function getLogs({ name, externalId, auth, limit, json, utc, log, }) {
92
91
  const logLength = filteredLogs.length;
93
92
  log(`Found ${styleText('bold', logLength.toString())} log ${logLength === 1 ? 'entry' : 'entries'} for function ${styleText('yellow', name)}`);
94
93
  log('\n');
95
- for (const { time, level, message } of filteredLogs) {
96
- log(formatLog(time, level, message, utc));
94
+ for (const entry of filteredLogs) {
95
+ log(formatLog(entry, utc));
97
96
  }
98
97
  }
99
98
  else {
100
- return { success: true, json: { logs: filteredLogs } };
99
+ const cleaned = filteredLogs.map(({ time, level, message, operationName, attempt, requestId, executionArn, }) => {
100
+ const isDurable = executionArn !== undefined;
101
+ const isStep = operationName !== undefined || attempt !== undefined;
102
+ const durableContext = isDurable ? (isStep ? 'step' : 'handler') : undefined;
103
+ return {
104
+ time,
105
+ level,
106
+ message,
107
+ requestId,
108
+ ...(durableContext ? { durableContext } : {}),
109
+ ...(operationName !== undefined ? { step: operationName } : {}),
110
+ ...(attempt !== undefined ? { attempt } : {}),
111
+ };
112
+ });
113
+ return { success: true, json: { logs: cleaned } };
101
114
  }
102
115
  return { success: true };
103
116
  }
104
- function formatLog(time, level, message, utc) {
117
+ /**
118
+ * Formats a log entry for display in the console.
119
+ * @param entry
120
+ * @param utc
121
+ */
122
+ function formatLog(entry, utc) {
123
+ const { time, level, message, operationName, attempt, executionArn } = entry;
105
124
  const date = new Date(time);
106
125
  const [dateString, timeString] = utc
107
126
  ? date.toISOString().slice(0, 19).split('T')
108
127
  : [date.toLocaleDateString(), date.toLocaleTimeString()];
128
+ const durableAnnotation = durableLogAnnotation({ operationName, attempt, executionArn });
109
129
  return [
110
130
  styleText('bold', dateString),
111
131
  styleText(['bold', 'blue'], timeString),
112
132
  logLevel(level.toUpperCase()),
133
+ ...(durableAnnotation ? [durableAnnotation] : []),
113
134
  message,
114
135
  ].join(' ');
115
136
  }
137
+ /**
138
+ * Log annotation for durable functions.
139
+ * @param operationName
140
+ * @param attempt
141
+ * @param executionArn
142
+ */
143
+ function durableLogAnnotation({ operationName, attempt, executionArn, }) {
144
+ /**
145
+ * executionArn is included on durable function logs, but not on regular function logs.
146
+ * If executionArn is defined, we can assume the log is from a durable function.
147
+ * If executionArn is defined and operationName and attempt are undefined, the logger is in the handler context
148
+ */
149
+ const annotationParts = [
150
+ executionArn !== undefined && operationName === undefined && attempt === undefined
151
+ ? 'handler'
152
+ : undefined,
153
+ ...(operationName ? [operationName] : []),
154
+ attempt !== undefined ? `attempt:${attempt}` : undefined,
155
+ ].filter((part) => part !== undefined);
156
+ return annotationParts.length > 0
157
+ ? styleText('grey', `[${annotationParts.join('|')}]`)
158
+ : undefined;
159
+ }
116
160
  function logLevel(level) {
117
161
  switch (level) {
118
162
  case 'ERROR':
@@ -39,6 +39,11 @@ export type CoreResult = {
39
39
  ref?: string;
40
40
  /** A machine-readable error code. */
41
41
  code?: string;
42
+ /**
43
+ * The user cancelled a prompt. A calling core reads it to keep its own message.
44
+ * Not printed: `code` would surface as `Code:` in the command output.
45
+ */
46
+ cancelled?: true;
42
47
  /** Overrides the process exit code (defaults to oclif's 2). */
43
48
  exitCode?: number;
44
49
  streaming?: never;
@@ -27598,17 +27598,23 @@ function prettyBytes(number, options) {
27598
27598
  }
27599
27599
 
27600
27600
  const localeOptions = buildLocaleOptions(options);
27601
+ const base = options.binary ? 1024 : 1000;
27601
27602
 
27602
27603
  let exponent = 0;
27603
27604
  if (number >= 1) {
27604
- exponent = Math.min(Math.floor(options.binary ? log(number) / Math.log(1024) : log10(number) / 3), UNITS.length - 1);
27605
+ exponent = Math.min(Math.floor(options.binary ? log(number) / Math.log(base) : log10(number) / 3), UNITS.length - 1);
27605
27606
  }
27606
27607
 
27607
- number = divide(number, (options.binary ? 1024 : 1000) ** exponent);
27608
+ number = divide(number, base ** exponent);
27608
27609
 
27609
27610
  if (!localeOptions) {
27610
- const minPrecision = Math.max(3, Math.floor(number).toString().length);
27611
- number = Number(number.toPrecision(minPrecision));
27611
+ const minimumPrecision = Math.max(3, Math.floor(number).toString().length);
27612
+ number = Number(number.toPrecision(minimumPrecision));
27613
+
27614
+ if (number >= base && exponent < UNITS.length - 1) {
27615
+ number /= base;
27616
+ exponent += 1;
27617
+ }
27612
27618
  }
27613
27619
 
27614
27620
  const numberString = toLocaleString(number, options.locale, localeOptions);
@@ -27769,10 +27775,11 @@ function prettyMilliseconds(milliseconds, options) {
27769
27775
  const roundedMilliseconds = millisecondsAndBelow >= 1
27770
27776
  ? Math.round(millisecondsAndBelow)
27771
27777
  : Math.ceil(millisecondsAndBelow);
27778
+ const maximumMilliseconds = 1000 - (10 ** -millisecondsDecimalDigits);
27772
27779
 
27773
27780
  const millisecondsString = millisecondsDecimalDigits
27774
- ? millisecondsAndBelow.toFixed(millisecondsDecimalDigits)
27775
- : roundedMilliseconds;
27781
+ ? Math.min(millisecondsAndBelow, maximumMilliseconds).toFixed(millisecondsDecimalDigits)
27782
+ : Math.min(roundedMilliseconds, maximumMilliseconds);
27776
27783
 
27777
27784
  add(
27778
27785
  Number.parseFloat(millisecondsString),
@@ -1,3 +1,5 @@
1
+ import { runCommand } from '../package-manager.js';
2
+ const promoteToOrganization = (bin) => `Run \`${runCommand(bin)} blueprints promote\` to move from project to organization scope.`;
1
3
  /**
2
4
  * Patterns matched against ERROR/FATAL log entries during streaming operations
3
5
  * like deploy and destroy. Each entry is a [regex, suggestionFactory] tuple.
@@ -13,9 +15,12 @@ export const HINT_PATTERNS = [
13
15
  [
14
16
  // Function requires organization scope
15
17
  /must be attached to an organization scoped stack/,
16
- (_match, bin) => [
17
- `Run \`npx ${bin} blueprints promote\` to move from project to organization scope.`,
18
- ],
18
+ (_match, bin) => [promoteToOrganization(bin)],
19
+ ],
20
+ [
21
+ // Resource needs an organization the project-scoped Stack cannot supply
22
+ /[Cc]annot resolve organization scope/,
23
+ (_match, bin) => [promoteToOrganization(bin)],
19
24
  ],
20
25
  ];
21
26
  /**
@@ -3,6 +3,16 @@ import type { Logger } from '../logger.js';
3
3
  import type { ScopeType } from '../types.js';
4
4
  export declare const BACK: unique symbol;
5
5
  export type Back = typeof BACK;
6
+ /** Thrown when the user picks "Cancel" in a prompt. Cores translate it to a clean result. */
7
+ export declare class CancelledError extends Error {
8
+ constructor(message?: string);
9
+ }
10
+ /**
11
+ * Ask where a new Blueprint should go.
12
+ * Returns the chosen directory, or null to use the current one.
13
+ * @throws CancelledError when the user cancels.
14
+ */
15
+ export declare function promptForInitLocation(currentDir: string): Promise<string | null>;
6
16
  export declare function promptForBlueprintType(): Promise<string>;
7
17
  /**
8
18
  * Fetches organizations and prompts the user to pick one (if multiple).
@@ -48,12 +58,14 @@ export declare function promptForProjectInOrg({ projects, knownProjectId, logger
48
58
  * Prompt the user for a Stack after scope has been determined.
49
59
  * Can create a new Stack or select an existing one.
50
60
  */
51
- export declare function promptForStack({ scopeType, scopeId, token, logger, allowBack, }: {
61
+ export declare function promptForStack({ scopeType, scopeId, token, logger, allowBack, suggestedName, }: {
52
62
  scopeType: ScopeType;
53
63
  scopeId: string;
54
64
  token: string;
55
65
  logger: Logger;
56
66
  allowBack?: boolean;
67
+ /** Prefilled name for a new Stack, usually the project directory name. */
68
+ suggestedName?: string;
57
69
  }): Promise<{
58
70
  stackId: string;
59
71
  name: string;
@@ -62,11 +74,12 @@ export declare function promptForStack({ scopeType, scopeId, token, logger, allo
62
74
  * Full wizard: Organization -> Scope -> Project (if project-scoped) -> Stack.
63
75
  * Supports back-navigation between steps.
64
76
  */
65
- export declare function runScopeAndStackWizard({ token, knownOrganizationId, knownProjectId, logger, }: {
77
+ export declare function runScopeAndStackWizard({ token, knownOrganizationId, knownProjectId, logger, suggestedStackName, }: {
66
78
  token: string;
67
79
  knownOrganizationId?: string;
68
80
  knownProjectId?: string;
69
81
  logger: Logger;
82
+ suggestedStackName?: string;
70
83
  }): Promise<{
71
84
  scopeType: ScopeType;
72
85
  scopeId: string;