@sanity/runtime-cli 17.8.0 → 17.9.0-blueprinit.g14aaebd

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 (81) hide show
  1. package/README.md +28 -22
  2. package/dist/actions/blueprinit/manifest.d.ts +46 -0
  3. package/dist/actions/blueprinit/manifest.js +121 -0
  4. package/dist/actions/blueprinit/quote.d.ts +6 -0
  5. package/dist/actions/blueprinit/quote.js +8 -0
  6. package/dist/actions/blueprinit/studio.d.ts +6 -0
  7. package/dist/actions/blueprinit/studio.js +43 -0
  8. package/dist/actions/blueprinit/templates.d.ts +73 -0
  9. package/dist/actions/blueprinit/templates.js +138 -0
  10. package/dist/actions/blueprinit/versions.d.ts +8 -0
  11. package/dist/actions/blueprinit/versions.js +11 -0
  12. package/dist/actions/blueprinit/workspace.d.ts +27 -0
  13. package/dist/actions/blueprinit/workspace.js +145 -0
  14. package/dist/actions/blueprints/pre-deploy.js +2 -0
  15. package/dist/actions/blueprints/stacks.d.ts +3 -0
  16. package/dist/actions/blueprints/stacks.js +1 -0
  17. package/dist/actions/sanity/create-project.d.ts +19 -0
  18. package/dist/actions/sanity/create-project.js +32 -0
  19. package/dist/actions/sanity/datasets.d.ts +15 -0
  20. package/dist/actions/sanity/datasets.js +26 -0
  21. package/dist/actions/sanity/projects.d.ts +11 -0
  22. package/dist/actions/sanity/projects.js +48 -22
  23. package/dist/baseCommands.d.ts +6 -0
  24. package/dist/baseCommands.js +9 -1
  25. package/dist/commands/blueprints/stacks.d.ts +2 -1
  26. package/dist/commands/blueprints/stacks.js +14 -6
  27. package/dist/commands/functions/test.d.ts +1 -0
  28. package/dist/commands/functions/test.js +5 -0
  29. package/dist/commands/init.d.ts +21 -0
  30. package/dist/commands/init.js +82 -0
  31. package/dist/cores/blueprinit/adopt.d.ts +6 -0
  32. package/dist/cores/blueprinit/adopt.js +137 -0
  33. package/dist/cores/blueprinit/columns.d.ts +26 -0
  34. package/dist/cores/blueprinit/columns.js +27 -0
  35. package/dist/cores/blueprinit/finish.d.ts +15 -0
  36. package/dist/cores/blueprinit/finish.js +72 -0
  37. package/dist/cores/blueprinit/fresh.d.ts +3 -0
  38. package/dist/cores/blueprinit/fresh.js +219 -0
  39. package/dist/cores/blueprinit/naming.d.ts +27 -0
  40. package/dist/cores/blueprinit/naming.js +43 -0
  41. package/dist/cores/blueprinit/preview.d.ts +34 -0
  42. package/dist/cores/blueprinit/preview.js +110 -0
  43. package/dist/cores/blueprinit/project-choice.d.ts +28 -0
  44. package/dist/cores/blueprinit/project-choice.js +82 -0
  45. package/dist/cores/blueprinit/prompts.d.ts +13 -0
  46. package/dist/cores/blueprinit/prompts.js +42 -0
  47. package/dist/cores/blueprinit/results.d.ts +8 -0
  48. package/dist/cores/blueprinit/results.js +4 -0
  49. package/dist/cores/blueprinit/selection.d.ts +15 -0
  50. package/dist/cores/blueprinit/selection.js +90 -0
  51. package/dist/cores/blueprinit/stack-name.d.ts +39 -0
  52. package/dist/cores/blueprinit/stack-name.js +56 -0
  53. package/dist/cores/blueprinit/target.d.ts +33 -0
  54. package/dist/cores/blueprinit/target.js +80 -0
  55. package/dist/cores/blueprinit/types.d.ts +20 -0
  56. package/dist/cores/blueprinit/types.js +1 -0
  57. package/dist/cores/blueprinit.d.ts +7 -0
  58. package/dist/cores/blueprinit.js +64 -0
  59. package/dist/cores/blueprints/stacks.d.ts +8 -3
  60. package/dist/cores/blueprints/stacks.js +282 -85
  61. package/dist/cores/functions/test.d.ts +2 -19
  62. package/dist/cores/functions/test.js +63 -138
  63. package/dist/server/app.js +4 -6
  64. package/dist/server/static/components/payload-panel.js +1 -1
  65. package/dist/utils/display/intro.d.ts +6 -0
  66. package/dist/utils/display/intro.js +63 -0
  67. package/dist/utils/display/prompt.d.ts +4 -1
  68. package/dist/utils/display/prompt.js +16 -2
  69. package/dist/utils/functions/child-process-wrapper.js +3 -2
  70. package/dist/utils/functions/child-runtime/durable.d.ts +2 -2
  71. package/dist/utils/functions/child-runtime/durable.js +53 -3
  72. package/dist/utils/functions/durable-logs.d.ts +16 -0
  73. package/dist/utils/functions/durable-logs.js +52 -0
  74. package/dist/utils/functions/invoke/local.js +16 -8
  75. package/dist/utils/functions/invoke/resolve-invoke-type.d.ts +51 -0
  76. package/dist/utils/functions/invoke/resolve-invoke-type.js +131 -0
  77. package/dist/utils/types.d.ts +39 -6
  78. package/oclif.manifest.json +177 -5
  79. package/package.json +1 -1
  80. package/skills/blueprints-stacks.md +14 -2
  81. package/skills/init.md +199 -0
@@ -1,34 +1,37 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { cwd } from 'node:process';
1
+ import { dirname } from 'node:path';
4
2
  import { testAction } from '../../actions/functions/test.js';
5
3
  import config from '../../config.js';
6
- import { SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_DURABLE, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, STEP_PHASE_GLYPH, } from '../../constants.js';
4
+ import { STEP_PHASE_GLYPH } from '../../constants.js';
7
5
  import { resolveDeployedResources } from '../../utils/blueprints/resolve-deployed-resources.js';
8
- import buildPayload from '../../utils/functions/build-payload.js';
9
- import { fetchAsset, fetchDocument } from '../../utils/functions/fetch-document.js';
6
+ import { createDurableJsonCollector } from '../../utils/functions/durable-logs.js';
10
7
  import { findFunctionInResources, getFunctionNames } from '../../utils/functions/find.js';
11
8
  import { runFunction } from '../../utils/functions/invoke/nested.js';
12
- import { parseJsonObject } from '../../utils/parse-json-object.js';
9
+ import { buildContext, buildInvokeOptions, collectInvokeInput, defaultInvocationDeps, resolveInvocationPlan, } from '../../utils/functions/invoke/resolve-invoke-type.js';
13
10
  import { isEventType, } from '../../utils/types.js';
14
11
  /**
15
12
  * CLI lines for a durable run, in emission order; bare `start` events dropped.
16
13
  * @param events
17
14
  */
18
15
  function renderStepTimeline(events) {
19
- return events
20
- .filter((event) => event.phase !== 'start')
21
- .map((event) => {
16
+ const lines = [];
17
+ for (const event of events) {
18
+ if (event.kind === 'log') {
19
+ lines.push(`${event.timestamp} ${event.level.toUpperCase()} ${event.message}`);
20
+ continue;
21
+ }
22
+ if (event.phase === 'start')
23
+ continue;
22
24
  const glyph = STEP_PHASE_GLYPH[event.phase] ?? '•';
23
25
  const duration = typeof event.durationMs === 'number' ? ` (${Math.round(event.durationMs)}ms)` : '';
24
26
  const detail = event.phase === 'error' && event.error ? ` ${event.error.message}` : '';
25
- return ` ${glyph} ${event.name} [${event.kind}]${duration}${detail}`;
26
- });
27
+ lines.push(` ${glyph} ${event.name} [${event.kind}]${duration}${detail}`);
28
+ }
29
+ return lines;
27
30
  }
28
31
  export async function functionTestCore(options) {
29
32
  const { blueprint, log, error, args, flags, helpText } = options;
30
33
  const { name: fnName } = args;
31
- const { data, event, file, timeout, api, dataset, 'document-id': documentId, 'with-user-token': withUserToken, 'data-before': dataBefore, 'data-after': dataAfter, 'file-before': fileBefore, 'file-after': fileAfter, 'document-id-before': documentIdBefore, 'document-id-after': documentIdAfter, 'media-library-id': mediaLibraryId, } = flags;
34
+ const { event, timeout, api, 'with-user-token': withUserToken, 'no-wait': noWait } = flags;
32
35
  const { 'project-id': projectId = blueprint?.projectId, 'organization-id': organizationId = blueprint?.organizationId, } = flags;
33
36
  if (!fnName) {
34
37
  const functionNames = getFunctionNames(blueprint.resources);
@@ -54,9 +57,6 @@ export async function functionTestCore(options) {
54
57
  }
55
58
  try {
56
59
  const resource = findFunctionInResources(blueprint.resources, fnName); // throws if not found
57
- const docFunction = resource.type === SANITY_FUNCTION_DOCUMENT;
58
- const mediaFunction = resource.type === SANITY_FUNCTION_MEDIA_LIBRARY_ASSET;
59
- const durableFunction = resource.type === SANITY_FUNCTION_DURABLE;
60
60
  const resolvedResources = await resolveDeployedResources({
61
61
  resources: blueprint.resources,
62
62
  stackId: blueprint.stackId,
@@ -65,112 +65,36 @@ export async function functionTestCore(options) {
65
65
  : undefined,
66
66
  logger: log,
67
67
  });
68
- const blueprintFileDir = dirname(blueprint.fileInfo.blueprintFilePath);
69
- const isMediaSource = mediaFunction || (durableFunction && resource.event?.type === 'media-library');
70
- const eventResourceType = isMediaSource ? 'media-library' : 'dataset';
71
- const eventResourceId = (isMediaSource ? mediaLibraryId : `${projectId}.${dataset}`) || '';
72
- const contextOptions = docFunction || mediaFunction || durableFunction
73
- ? {
74
- clientOptions: {
75
- apiVersion: api,
76
- dataset,
77
- projectId,
78
- organizationId,
79
- },
80
- eventResourceType,
81
- eventResourceId,
82
- functionResourceType: 'project',
83
- functionResourceId: projectId || '',
84
- resources: resolvedResources,
85
- blueprintFileDir,
86
- }
87
- : { clientOptions: {}, resources: resolvedResources, blueprintFileDir };
88
- // If the user sets the flag to use the real token set it in our options
89
- if (withUserToken) {
90
- contextOptions.clientOptions.token = config.token || undefined;
91
- }
92
- let before = null;
93
- let after = null;
94
- if (dataBefore && dataAfter) {
95
- before = parseJsonObject(dataBefore);
96
- after = parseJsonObject(dataAfter);
97
- }
98
- else if (fileBefore && fileAfter) {
99
- let fileContents = readFileSync(join(cwd(), fileBefore), 'utf8');
100
- before = parseJsonObject(fileContents);
101
- fileContents = readFileSync(join(cwd(), fileAfter), 'utf8');
102
- after = parseJsonObject(fileContents);
103
- }
104
- else if (documentIdBefore && documentIdAfter && docFunction) {
105
- before = await fetchDocument(documentIdBefore, {
106
- projectId,
107
- dataset,
108
- apiVersion: api,
109
- apiHost: config.apiUrl,
110
- token: config.token || undefined,
111
- }, log);
112
- after = await fetchDocument(documentIdAfter, {
113
- projectId,
114
- dataset,
115
- apiVersion: api,
116
- apiHost: config.apiUrl,
117
- token: config.token || undefined,
118
- }, log);
119
- }
120
- else if (documentIdBefore && documentIdAfter) {
121
- before = await fetchAsset(documentIdBefore, {
122
- mediaLibraryId,
123
- apiVersion: api,
124
- apiHost: config.apiUrl,
125
- token: config.token || undefined,
126
- }, log);
127
- after = await fetchAsset(documentIdAfter, {
128
- mediaLibraryId,
129
- apiVersion: api,
130
- apiHost: config.apiUrl,
131
- token: config.token || undefined,
132
- }, log);
133
- }
134
- let payload;
135
- if (after) {
136
- payload = after;
137
- }
138
- else {
139
- if (documentId && docFunction) {
140
- payload = await fetchDocument(documentId, {
141
- projectId,
142
- dataset,
143
- apiVersion: api,
144
- apiHost: config.apiUrl,
145
- token: config.token || undefined,
146
- }, log);
147
- }
148
- else if (documentId) {
149
- payload = await fetchAsset(documentId, {
150
- mediaLibraryId,
151
- apiVersion: api,
152
- apiHost: config.apiUrl,
153
- token: config.token || undefined,
154
- }, log);
155
- }
156
- else {
157
- payload = buildPayload({ data, file });
158
- }
159
- }
160
- let invokeOptions;
161
- if (docFunction || mediaFunction) {
162
- invokeOptions = { event: eventType, payload, before, after };
163
- }
164
- else {
165
- invokeOptions = { event: durableFunction ? 'durable' : 'scheduled', payload: { data: payload } };
166
- }
68
+ const input = {
69
+ flags,
70
+ projectId,
71
+ organizationId,
72
+ api,
73
+ apiHost: config.apiUrl,
74
+ token: config.token || undefined,
75
+ withUserToken,
76
+ resources: resolvedResources,
77
+ blueprintFileDir: dirname(blueprint.fileInfo.blueprintFilePath),
78
+ log,
79
+ };
80
+ const plan = resolveInvocationPlan(resource);
81
+ const context = buildContext(plan, input);
82
+ const invokeInput = await collectInvokeInput(plan, input, defaultInvocationDeps);
83
+ const invokeOptions = buildInvokeOptions(plan, invokeInput, eventType);
167
84
  // Records each nested context.invoke() so they can be surfaced in order after the spinner resolves.
168
85
  const nestedRuns = [];
169
86
  let nestedOrder = 0;
170
- const stepEvents = [];
171
- const onStepEvent = durableFunction
87
+ const durableJson = createDurableJsonCollector();
88
+ const onStepEvent = plan.isDurable
172
89
  ? (event) => {
173
- stepEvents.push(event);
90
+ if (flags.json) {
91
+ durableJson.record(event);
92
+ return;
93
+ }
94
+ for (const line of renderStepTimeline([event])) {
95
+ if (line !== null)
96
+ log(line);
97
+ }
174
98
  }
175
99
  : undefined;
176
100
  /**
@@ -185,7 +109,7 @@ export async function functionTestCore(options) {
185
109
  const runNested = async (nestedName, nestedPayload, ctx, depth, callStack) => {
186
110
  const order = ++nestedOrder;
187
111
  const nestedResource = findFunctionInResources(blueprint.resources, nestedName);
188
- const result = await runFunction(nestedResource, { payload: { data: nestedPayload }, event: 'nested' }, { ...ctx, resources: resolvedResources }, { depth, callStack, onNestedInvoke: runNested });
112
+ const result = await runFunction(nestedResource, { payload: nestedPayload, event: 'nested' }, { ...ctx, resources: resolvedResources }, { depth, callStack, onNestedInvoke: runNested });
189
113
  nestedRuns.push({ order, name: nestedName, logs: result.logs, error: result.error });
190
114
  return result;
191
115
  };
@@ -204,12 +128,13 @@ export async function functionTestCore(options) {
204
128
  }
205
129
  };
206
130
  const spinner = log.ora('Executing function...').start();
207
- const { json: response, logs, error, } = await testAction(resource, invokeOptions, contextOptions, {
131
+ const { json: response, logs, error, } = await testAction(resource, invokeOptions, context, {
208
132
  timeout: timeout ? timeout : resource.timeout,
209
133
  onNestedInvoke: runNested,
210
134
  depth: 0,
211
135
  callStack: [fnName],
212
136
  onStepEvent,
137
+ noWait,
213
138
  });
214
139
  if (error) {
215
140
  spinner.fail('Function execution failed.');
@@ -221,12 +146,6 @@ export async function functionTestCore(options) {
221
146
  logNestedRuns();
222
147
  const errorMessage = error.stack || error.message || error.name;
223
148
  const isTimeout = error.message?.includes('Timeout');
224
- if (durableFunction && stepEvents.length > 0) {
225
- log('Steps:');
226
- for (const line of renderStepTimeline(stepEvents)) {
227
- log(line);
228
- }
229
- }
230
149
  return {
231
150
  success: false,
232
151
  error: errorMessage,
@@ -237,20 +156,26 @@ export async function functionTestCore(options) {
237
156
  };
238
157
  }
239
158
  spinner.succeed('Function execution succeeded.');
240
- log('Logs:');
241
- log(logs || '');
242
- logNestedRuns();
243
- if (response) {
244
- log('Response:');
245
- log(JSON.stringify(response, null, 2));
246
- }
247
- if (durableFunction && stepEvents.length > 0) {
248
- log('Steps:');
249
- for (const line of renderStepTimeline(stepEvents)) {
250
- log(line);
159
+ if (!plan.isDurable) {
160
+ log('Logs:');
161
+ log(logs || '');
162
+ logNestedRuns();
163
+ if (response) {
164
+ log('Response:');
165
+ log(JSON.stringify(response, null, 2));
251
166
  }
252
167
  }
253
- return { success: true, json: { response, logs } };
168
+ const durableResult = durableJson.result();
169
+ return {
170
+ success: true,
171
+ json: plan.isDurable
172
+ ? {
173
+ response: response ?? null,
174
+ steps: durableResult.steps,
175
+ logs: durableResult.logs,
176
+ }
177
+ : { response, logs },
178
+ };
254
179
  }
255
180
  catch (error) {
256
181
  return {
@@ -28,7 +28,7 @@ const app = (host, port, validateResources, executionOptions) => {
28
28
  */
29
29
  const nestedInvoke = async (name, payload, ctx, depth, callStack) => {
30
30
  const order = ++nestedOrder;
31
- const result = await handleInvokeRequest(name, { data: payload }, { event: 'nested' }, ctx, validateResources, executionOptions, depth, callStack, nestedInvoke);
31
+ const result = await handleInvokeRequest(name, payload, { event: 'nested' }, ctx, validateResources, executionOptions, depth, callStack, nestedInvoke);
32
32
  broadcast({
33
33
  type: 'nested-invoke',
34
34
  name,
@@ -372,13 +372,13 @@ function parseInvokeRequest(body, blueprintFileDir) {
372
372
  }
373
373
  const metadataEvent = metadata.event;
374
374
  if (typeof metadataEvent !== 'string' ||
375
- (!isEventType(metadataEvent) && metadataEvent !== 'scheduled')) {
375
+ (!isEventType(metadataEvent) && metadataEvent !== 'plain')) {
376
376
  throw new Error('Request body is not valid, `metadata.event` field is not one of `create`, `update`, `delete`, or `scheduled`');
377
377
  }
378
378
  let before = null;
379
379
  let after = null;
380
380
  // Only GROQ-based events (create, update, delete) have before and after fields
381
- if (metadataEvent !== 'scheduled') {
381
+ if (metadataEvent !== 'plain') {
382
382
  if (!('before' in metadata)) {
383
383
  throw new Error('Request body is not valid, `metadata.before` field is missing');
384
384
  }
@@ -418,9 +418,7 @@ function parseInvokeRequest(body, blueprintFileDir) {
418
418
  },
419
419
  event,
420
420
  },
421
- metadata: metadataEvent === 'scheduled'
422
- ? { event: metadataEvent }
423
- : { event: metadataEvent, before, after },
421
+ metadata: metadataEvent === 'plain' ? { event: metadataEvent } : { event: metadataEvent, before, after },
424
422
  };
425
423
  }
426
424
  export { app, buildApiUrl, parseDocumentUrl };
@@ -127,7 +127,7 @@ class PayloadPanel extends ApiBaseElement {
127
127
  // for certain other specific function types, set the payload to something specific, or reload whatever was loaded into its document payload before
128
128
  switch (selectedFunction.type) {
129
129
  case this.SANITY_FUNCTION_SYNC_TAG_INVALIDATE:
130
- this.#updateCodeView(this.api.store.payload, {data: {syncTags: ['s1:1234', 's2:5678']}})
130
+ this.#updateCodeView(this.api.store.payload, {syncTags: ['s1:1234', 's2:5678']})
131
131
  break
132
132
  default:
133
133
  this.#updateCodeView(this.api.store.payload, this.documentCacheByFunction[selectedIndex])
@@ -0,0 +1,6 @@
1
+ /** A left-to-right reveal of the wordmark; the last frame is the finished line. */
2
+ export declare function introFrames(): string[];
3
+ /** Print the intro, animated when the terminal can show it and static when it cannot. */
4
+ export declare function playIntro({ json }?: {
5
+ json?: boolean;
6
+ }): Promise<void>;
@@ -0,0 +1,63 @@
1
+ import { env, stdout } from 'node:process';
2
+ import { onExit } from 'signal-exit';
3
+ import { isInteractive } from '../interactive.js';
4
+ import { styleText } from '../style-text.js';
5
+ const INDENT = ' ';
6
+ const WORDMARK = 'sanity';
7
+ const TAIL = ' with blueprints';
8
+ /** The leading edge of the reveal, densest first. */
9
+ const GRADIENT = ['▓', '▒', '░'];
10
+ const REVEAL_PER_FRAME = 1;
11
+ const FRAME_MS = 45;
12
+ /** Time the finished wordmark holds before the command's own output follows. */
13
+ const HOLD_MS = 250;
14
+ const HIDE_CURSOR = '\x1b[?25l';
15
+ const SHOW_CURSOR = '\x1b[?25h';
16
+ const CLEAR_LINE = '\x1b[2K';
17
+ function style(revealed, edge) {
18
+ const wordmark = revealed.slice(0, WORDMARK.length);
19
+ const tail = revealed.slice(WORDMARK.length);
20
+ return INDENT + styleText(['bold', 'red'], wordmark) + styleText('dim', tail + edge);
21
+ }
22
+ /** A left-to-right reveal of the wordmark; the last frame is the finished line. */
23
+ export function introFrames() {
24
+ const full = WORDMARK + TAIL;
25
+ const frames = [];
26
+ for (let revealed = REVEAL_PER_FRAME; revealed < full.length; revealed += REVEAL_PER_FRAME) {
27
+ const edge = GRADIENT.slice(0, Math.min(GRADIENT.length, full.length - revealed)).join('');
28
+ frames.push(style(full.slice(0, revealed), edge));
29
+ }
30
+ frames.push(style(full, ''));
31
+ return frames;
32
+ }
33
+ function animates() {
34
+ return isInteractive && env.TERM !== 'dumb' && !env.CI;
35
+ }
36
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
37
+ /** Print the intro, animated when the terminal can show it and static when it cannot. */
38
+ export async function playIntro({ json = false } = {}) {
39
+ if (json)
40
+ return;
41
+ const frames = introFrames();
42
+ const finished = frames.at(-1) ?? '';
43
+ if (!animates()) {
44
+ stdout.write(`\n${finished}\n\n`);
45
+ return;
46
+ }
47
+ stdout.write(`\n${HIDE_CURSOR}`);
48
+ const cancelExitHook = onExit(() => {
49
+ stdout.write(SHOW_CURSOR);
50
+ });
51
+ try {
52
+ for (const frame of frames) {
53
+ stdout.write(`\r${CLEAR_LINE}${frame}`);
54
+ await sleep(FRAME_MS);
55
+ }
56
+ await sleep(HOLD_MS);
57
+ stdout.write('\n\n');
58
+ }
59
+ finally {
60
+ stdout.write(SHOW_CURSOR);
61
+ cancelExitHook();
62
+ }
63
+ }
@@ -24,10 +24,13 @@ export declare function promptForOrganization({ token, knownOrganizationId, logg
24
24
  /**
25
25
  * Asks the user whether to scope a Blueprint to an organization or a project.
26
26
  */
27
- export declare function promptForScope({ allowBack, hasProjects, }?: {
27
+ export declare function promptForScope({ allowBack, hasProjects, message, }?: {
28
28
  allowBack?: boolean;
29
29
  hasProjects?: boolean;
30
+ message?: string;
30
31
  }): Promise<ScopeType | Back>;
32
+ /** Offers a recovery path after the selected scope cannot be read. */
33
+ export declare function promptForScopeRecovery(): Promise<'exit' | 'retry'>;
31
34
  /**
32
35
  * Prompts the user to pick a project from a pre-fetched list of projects.
33
36
  * Use after promptForOrganization when the user chose project scope.
@@ -121,7 +121,7 @@ export async function promptForOrganization({ token, knownOrganizationId, logger
121
121
  /**
122
122
  * Asks the user whether to scope a Blueprint to an organization or a project.
123
123
  */
124
- export async function promptForScope({ allowBack = false, hasProjects = true, } = {}) {
124
+ export async function promptForScope({ allowBack = false, hasProjects = true, message = 'How would you like to scope your Blueprint?', } = {}) {
125
125
  const choices = [
126
126
  {
127
127
  name: 'Organization',
@@ -136,12 +136,26 @@ export async function promptForScope({ allowBack = false, hasProjects = true, }
136
136
  },
137
137
  ];
138
138
  const answer = await select({
139
- message: 'How would you like to scope your Blueprint?',
139
+ message,
140
140
  choices: withBackChoice(choices, allowBack),
141
141
  default: 'organization',
142
142
  });
143
143
  return answer;
144
144
  }
145
+ /** Offers a recovery path after the selected scope cannot be read. */
146
+ export async function promptForScopeRecovery() {
147
+ return await select({
148
+ message: 'What would you like to do?',
149
+ choices: [
150
+ {
151
+ name: 'Choose another organization or project',
152
+ value: 'retry',
153
+ },
154
+ { name: 'Exit', value: 'exit' },
155
+ ],
156
+ default: 'retry',
157
+ });
158
+ }
145
159
  /**
146
160
  * Prompts the user to pick a project from a pre-fetched list of projects.
147
161
  * Use after promptForOrganization when the user chose project scope.
@@ -41,7 +41,7 @@ process.on('message', async (data) => {
41
41
 
42
42
  // Otherwise this is the initial start message. Support the legacy untyped
43
43
  // `{srcPath, payload}` shape as well as the new `{type: 'start', ...}`.
44
- const {srcPath, payload, isDurable} = jsonData
44
+ const {srcPath, payload, isDurable, noWait} = jsonData
45
45
  const {context, ...event} = payload
46
46
 
47
47
  let logs = ''
@@ -83,9 +83,10 @@ process.on('message', async (data) => {
83
83
  if (isDurable) {
84
84
  json = await runDurable(
85
85
  handler,
86
- {context, event, logger: console},
86
+ {context, event},
87
87
  {
88
88
  emit: (e) => process.send(JSON.stringify({type: 'step-event', event: e})),
89
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, noWait ? 0 : ms)),
89
90
  },
90
91
  )
91
92
  } else {
@@ -1,5 +1,5 @@
1
1
  import type {DurableContext, DurableHandler, DurableLogger, GenericEvent} from '@sanity/functions'
2
- import type {DurableStepEvent} from '../../types.js'
2
+ import type {DurableRuntimeEvent} from '../../types.js'
3
3
 
4
4
  type RunDurableArgs = {
5
5
  context: DurableContext
@@ -8,7 +8,7 @@ type RunDurableArgs = {
8
8
  }
9
9
 
10
10
  type DurableIO = {
11
- emit: (event: DurableStepEvent) => void
11
+ emit: (event: DurableRuntimeEvent) => void
12
12
  }
13
13
 
14
14
  export function runDurable(
@@ -1,17 +1,60 @@
1
1
  import {performance} from 'node:perf_hooks'
2
+ import {inspect} from 'node:util'
3
+
4
+ /**
5
+ *
6
+ * @param duration
7
+ * @return {number}
8
+ */
9
+ const durationToMs = (duration = {}) => {
10
+ const {days = 0, hours = 0, minutes = 0, seconds = 0} = duration
11
+ return (((days * 24 + hours) * 60 + minutes) * 60 + seconds) * 1000
12
+ }
13
+
14
+ /**
15
+ *
16
+ * @param args
17
+ */
18
+ const formatArgs = (args) =>
19
+ args.map((a) => (typeof a === 'string' ? a : inspect(a, {depth: null}))).join(' ')
2
20
 
3
21
  /**
4
22
  * Runs a durable function handler
5
23
  * @param handler - callback being run
6
24
  * @param context - data to send to the operation
7
25
  * @param event - the step being run
8
- * @param logger
9
26
  * @param io
10
27
  */
11
- export async function runDurable(handler, {context, event, logger}, io) {
28
+ export async function runDurable(handler, {context, event}, io) {
29
+ let currentStep = null
30
+ const streamLog = (level, args) => {
31
+ const date = new Date()
32
+ const timestamp = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
33
+
34
+ return io.emit({
35
+ timestamp,
36
+ kind: 'log',
37
+ phase: 'log',
38
+ step: currentStep,
39
+ level,
40
+ message: formatArgs(args),
41
+ })
42
+ }
43
+
44
+ // @todo: need to throw error for use of `console.log` in a durable function
45
+ const logger = {
46
+ log: (...a) => streamLog('info', a),
47
+ info: (...a) => streamLog('info', a),
48
+ warn: (...a) => streamLog('warn', a),
49
+ error: (...a) => streamLog('error', a),
50
+ }
51
+
12
52
  const track = async (name, kind, produce) => {
13
53
  io.emit({name, kind, phase: 'start'})
14
54
  const startedAt = performance.now()
55
+ const prev = currentStep
56
+ currentStep = name
57
+
15
58
  try {
16
59
  const value = await produce()
17
60
  io.emit({
@@ -35,11 +78,18 @@ export async function runDurable(handler, {context, event, logger}, io) {
35
78
  durationMs: performance.now() - startedAt,
36
79
  })
37
80
  throw err
81
+ } finally {
82
+ currentStep = prev
38
83
  }
39
84
  }
40
85
 
41
86
  const step = {
42
- run: ({name, handler: fn}) => track(name, 'run', () => fn({logger, attempt: 1})),
87
+ run: ({name, handler: fn}) => track(name || 'step-run', 'run', () => fn({logger, attempt: 1})),
88
+ wait: ({name, duration}) =>
89
+ track(name, 'wait', async () => {
90
+ await io.wait(durationToMs(duration))
91
+ return undefined
92
+ }),
43
93
  }
44
94
 
45
95
  return handler({context, event, step, logger})
@@ -0,0 +1,16 @@
1
+ import type { DurableLogEvent, DurableRuntimeEvent, DurableStepEvent } from '../types.js';
2
+ type DurableLogLine = Pick<DurableLogEvent, 'level' | 'message'>;
3
+ type DurableJsonStep = DurableStepEvent & {
4
+ logs: DurableLogLine[];
5
+ };
6
+ /**
7
+ * Records a durable log event
8
+ */
9
+ export declare const createDurableJsonCollector: () => {
10
+ record: (event: DurableRuntimeEvent) => void;
11
+ result: () => {
12
+ steps: DurableJsonStep[];
13
+ logs: DurableLogLine[];
14
+ };
15
+ };
16
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Records a durable log event
3
+ */
4
+ export const createDurableJsonCollector = () => {
5
+ const steps = [];
6
+ const logLines = [];
7
+ const activeSteps = new Map();
8
+ function record(event) {
9
+ if (event.kind === 'log') {
10
+ const logLine = {
11
+ level: event.level,
12
+ message: event.message,
13
+ };
14
+ if (event.step === null) {
15
+ logLines.push(logLine);
16
+ return;
17
+ }
18
+ const step = activeSteps.get(event.step);
19
+ if (step) {
20
+ step.logs.push(logLine);
21
+ return;
22
+ }
23
+ logLines.push(logLine);
24
+ return;
25
+ }
26
+ if (event.phase === 'start') {
27
+ const step = {
28
+ ...event,
29
+ logs: [],
30
+ };
31
+ steps.push(step);
32
+ activeSteps.set(event.name, step);
33
+ return;
34
+ }
35
+ const step = activeSteps.get(event.name);
36
+ if (!step)
37
+ return;
38
+ step.phase = event.phase;
39
+ step.durationMs = event.durationMs;
40
+ if (event.phase === 'complete') {
41
+ step.output = event.output;
42
+ }
43
+ if (event.phase === 'error') {
44
+ step.error = event.error;
45
+ }
46
+ activeSteps.delete(event.name);
47
+ }
48
+ return {
49
+ record,
50
+ result: () => ({ steps, logs: logLines }),
51
+ };
52
+ };