@sanity/runtime-cli 17.0.0 → 17.1.0-next.g3cb2d2e

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.
package/README.md CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @sanity/runtime-cli
20
20
  $ sanity-run COMMAND
21
21
  running command...
22
22
  $ sanity-run (--version)
23
- @sanity/runtime-cli/17.0.0 linux-x64 node-v24.16.0
23
+ @sanity/runtime-cli/17.1.0-next.g3cb2d2e linux-x64 node-v24.18.0
24
24
  $ sanity-run --help [COMMAND]
25
25
  USAGE
26
26
  $ sanity-run COMMAND
@@ -103,7 +103,7 @@ EXAMPLES
103
103
  $ sanity-run blueprints add function --name my-function --fn-type document-create --fn-type document-update --lang js
104
104
  ```
105
105
 
106
- _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/add.ts)_
106
+ _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/add.ts)_
107
107
 
108
108
  ## `sanity-run blueprints config`
109
109
 
@@ -140,7 +140,7 @@ EXAMPLES
140
140
  $ sanity-run blueprints config --edit --project-id <projectId> --stack <name-or-id>
141
141
  ```
142
142
 
143
- _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/config.ts)_
143
+ _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/config.ts)_
144
144
 
145
145
  ## `sanity-run blueprints deploy`
146
146
 
@@ -194,7 +194,7 @@ EXAMPLES
194
194
  $ sanity-run blueprints deploy --new-stack-name <new-name>
195
195
  ```
196
196
 
197
- _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/deploy.ts)_
197
+ _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/deploy.ts)_
198
198
 
199
199
  ## `sanity-run blueprints destroy`
200
200
 
@@ -232,7 +232,7 @@ EXAMPLES
232
232
  $ sanity-run blueprints destroy --stack <name-or-id> --project-id <projectId> --force --no-wait
233
233
  ```
234
234
 
235
- _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/destroy.ts)_
235
+ _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/destroy.ts)_
236
236
 
237
237
  ## `sanity-run blueprints doctor`
238
238
 
@@ -263,7 +263,7 @@ EXAMPLES
263
263
  $ sanity-run blueprints doctor --fix
264
264
  ```
265
265
 
266
- _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/doctor.ts)_
266
+ _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/doctor.ts)_
267
267
 
268
268
  ## `sanity-run blueprints info`
269
269
 
@@ -300,7 +300,7 @@ EXAMPLES
300
300
  $ sanity-run blueprints info --organization-id <orgId> --stack <name-or-id>
301
301
  ```
302
302
 
303
- _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/info.ts)_
303
+ _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/info.ts)_
304
304
 
305
305
  ## `sanity-run blueprints init [DIR]`
306
306
 
@@ -364,7 +364,7 @@ EXAMPLES
364
364
  $ sanity-run blueprints init old-stack --type <json|js|ts> --project-id <projectId> --stack-id <existingStackId>
365
365
  ```
366
366
 
367
- _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/init.ts)_
367
+ _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/init.ts)_
368
368
 
369
369
  ## `sanity-run blueprints logs`
370
370
 
@@ -410,7 +410,7 @@ EXAMPLES
410
410
  $ sanity-run blueprints logs --before 2026-05-01T00:00:00Z
411
411
  ```
412
412
 
413
- _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/logs.ts)_
413
+ _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/logs.ts)_
414
414
 
415
415
  ## `sanity-run blueprints mint-deploy-token`
416
416
 
@@ -455,7 +455,7 @@ EXAMPLES
455
455
  $ sanity-run blueprints mint-deploy-token --organization-id <orgId>
456
456
  ```
457
457
 
458
- _See code: [src/commands/blueprints/mint-deploy-token.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/mint-deploy-token.ts)_
458
+ _See code: [src/commands/blueprints/mint-deploy-token.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/mint-deploy-token.ts)_
459
459
 
460
460
  ## `sanity-run blueprints plan`
461
461
 
@@ -488,7 +488,7 @@ EXAMPLES
488
488
  $ sanity-run blueprints plan --organization-id <orgId> --stack <name-or-id>
489
489
  ```
490
490
 
491
- _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/plan.ts)_
491
+ _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/plan.ts)_
492
492
 
493
493
  ## `sanity-run blueprints promote`
494
494
 
@@ -524,7 +524,7 @@ EXAMPLES
524
524
  $ sanity-run blueprints promote --new-stack-name <new-name>
525
525
  ```
526
526
 
527
- _See code: [src/commands/blueprints/promote.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/promote.ts)_
527
+ _See code: [src/commands/blueprints/promote.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/promote.ts)_
528
528
 
529
529
  ## `sanity-run blueprints stacks`
530
530
 
@@ -560,7 +560,7 @@ EXAMPLES
560
560
  $ sanity-run blueprints stacks --organization-id <organizationId> --include-projects
561
561
  ```
562
562
 
563
- _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/blueprints/stacks.ts)_
563
+ _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/blueprints/stacks.ts)_
564
564
 
565
565
  ## `sanity-run functions add`
566
566
 
@@ -612,7 +612,7 @@ EXAMPLES
612
612
  $ sanity-run functions add --name my-function --type document-create --type document-update --lang js
613
613
  ```
614
614
 
615
- _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/functions/add.ts)_
615
+ _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/functions/add.ts)_
616
616
 
617
617
  ## `sanity-run functions dev`
618
618
 
@@ -647,7 +647,7 @@ EXAMPLES
647
647
  $ sanity-run functions dev --timeout 60
648
648
  ```
649
649
 
650
- _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/functions/dev.ts)_
650
+ _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/functions/dev.ts)_
651
651
 
652
652
  ## `sanity-run functions env add NAME KEY VALUE`
653
653
 
@@ -677,7 +677,7 @@ EXAMPLES
677
677
  $ sanity-run functions env add MyFunction API_URL https://api.example.com/
678
678
  ```
679
679
 
680
- _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/functions/env/add.ts)_
680
+ _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/functions/env/add.ts)_
681
681
 
682
682
  ## `sanity-run functions env list NAME`
683
683
 
@@ -704,7 +704,7 @@ EXAMPLES
704
704
  $ sanity-run functions env list MyFunction
705
705
  ```
706
706
 
707
- _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/functions/env/list.ts)_
707
+ _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/functions/env/list.ts)_
708
708
 
709
709
  ## `sanity-run functions env remove NAME KEY`
710
710
 
@@ -733,7 +733,7 @@ EXAMPLES
733
733
  $ sanity-run functions env remove MyFunction API_URL
734
734
  ```
735
735
 
736
- _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/functions/env/remove.ts)_
736
+ _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/functions/env/remove.ts)_
737
737
 
738
738
  ## `sanity-run functions logs [NAME]`
739
739
 
@@ -773,7 +773,7 @@ EXAMPLES
773
773
  $ sanity-run functions logs <name> --delete
774
774
  ```
775
775
 
776
- _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/functions/logs.ts)_
776
+ _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/functions/logs.ts)_
777
777
 
778
778
  ## `sanity-run functions test [NAME]`
779
779
 
@@ -830,7 +830,7 @@ EXAMPLES
830
830
  $ sanity-run functions test <name> --event update --data-before '{ "title": "before" }' --data-after '{ "title": "after" }'
831
831
  ```
832
832
 
833
- _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v17.0.0/src/commands/functions/test.ts)_
833
+ _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v17.1.0-next.g3cb2d2e/src/commands/functions/test.ts)_
834
834
 
835
835
  ## `sanity-run help [COMMAND]`
836
836
 
@@ -850,5 +850,5 @@ DESCRIPTION
850
850
  Display help for sanity-run.
851
851
  ```
852
852
 
853
- _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/6.2.50/src/commands/help.ts)_
853
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/6.2.53/src/commands/help.ts)_
854
854
  <!-- commandsstop -->
@@ -1,4 +1,4 @@
1
- import invoke from '../../utils/functions/invoke-local.js';
1
+ import invoke from '../../utils/functions/invoke/local.js';
2
2
  export async function testAction(resource, payload, context, options) {
3
3
  try {
4
4
  const { json, logs, error } = await invoke(resource, payload, context, options);
@@ -4,6 +4,8 @@ import { findBlueprintFile, loadBlueprintFile, parseBlueprintContent, } from './
4
4
  import { backfillProjectBasedStackId, readConfigFile, } from './actions/blueprints/config.js';
5
5
  import { resolveIds } from './actions/blueprints/resolve.js';
6
6
  import { getStack, resolveStackIdByNameOrId } from './actions/blueprints/stacks.js';
7
+ import config from './config.js';
8
+ import { CI_SETUP_DOCS_URL } from './constants.js';
7
9
  import { presentBlueprintParserErrors } from './utils/display/errors.js';
8
10
  import { Logger } from './utils/logger.js';
9
11
  import { styleText } from './utils/style-text.js';
@@ -319,10 +321,15 @@ export class ResolvedCommand extends RuntimeCommand {
319
321
  // 5. Scope
320
322
  if (needsScope) {
321
323
  if (!resolved.scopeType || !resolved.scopeId) {
322
- this.error('Missing scope: provide --project-id or --organization-id, or run `blueprints init` to create a Blueprint config (.sanity/blueprint.config.json).', {
324
+ // In CI environments, since .sanity/blueprint.config.json is not guaranteed to exist, we can provide a more specific hint
325
+ const errorMessage = config.isCi
326
+ ? 'set SANITY_PROJECT_ID or SANITY_ORGANIZATION_ID.'
327
+ : 'provide --project-id or --organization-id, or run `blueprints init` to create a Blueprint config (.sanity/blueprint.config.json).';
328
+ this.error(`Missing scope: ${errorMessage}`, {
323
329
  suggestions: [
324
330
  `Run \`${this.config.bin} blueprints doctor\` to check your configuration.`,
325
331
  ],
332
+ ref: CI_SETUP_DOCS_URL,
326
333
  });
327
334
  }
328
335
  this.scopeType = resolved.scopeType;
@@ -341,10 +348,14 @@ export class ResolvedCommand extends RuntimeCommand {
341
348
  stackId = await resolveStackIdByNameOrId(stackOverride, auth, log);
342
349
  }
343
350
  if (!stackId) {
344
- this.error(`Missing Stack: provide --stack, or set a Stack in your Blueprint config (\`${this.config.bin} blueprints config --edit\`).`, {
351
+ const errorMessage = config.isCi
352
+ ? "couldn't determine which Stack to target. Set SANITY_BLUEPRINT_STACK_ID."
353
+ : `provide --stack, or set a Stack in your Blueprint config (\`${this.config.bin} blueprints config --edit\`).`;
354
+ this.error(`Missing Stack: ${errorMessage}`, {
345
355
  suggestions: [
346
356
  `Run \`${this.config.bin} blueprints doctor\` to check your configuration.`,
347
357
  ],
358
+ ref: CI_SETUP_DOCS_URL,
348
359
  });
349
360
  }
350
361
  this.stackId = stackId;
package/dist/config.d.ts CHANGED
@@ -17,6 +17,7 @@ declare const _default: {
17
17
  isLive: boolean;
18
18
  apiUrl: string;
19
19
  populusApiUrl: string;
20
+ isCi: boolean;
20
21
  token: string | null;
21
22
  };
22
23
  export default _default;
package/dist/config.js CHANGED
@@ -3,6 +3,7 @@ import { env } from 'node:process';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import * as pkg from 'empathic/package';
5
5
  import getToken from './utils/get-token.js';
6
+ import isTrueish from './utils/isTrueish.js';
6
7
  export const BLUEPRINT_CONFIG_VERSION = 'v2025-05-08';
7
8
  // Default API version for Blueprints service calls.
8
9
  // Overridable globally via env, or per-endpoint via the `version` arg to `blueprintsUrl`.
@@ -29,7 +30,11 @@ const isTest = nodeEnv === 'test';
29
30
  const sanityEnv = env.SANITY_INTERNAL_ENV?.toLowerCase() ?? 'production';
30
31
  const sanityProd = sanityEnv === 'production';
31
32
  const isLive = sanityProd || sanityEnv === 'staging';
32
- const isPublishing = env?.PUBLISHING?.toLowerCase() === 'true';
33
+ const isPublishing = isTrueish(env?.PUBLISHING);
34
+ // https://github.com/sanity-io/cli/blob/adf90ea6691ee6c36849e426de9729533706f47f/packages/%40sanity/cli-core/src/util/isCi.ts
35
+ const isCi =
36
+ // Travis CI, CircleCI, Gitlab CI, Appveyor, CodeShip, Travis CI, Jenkins, TeamCity
37
+ isTrueish(env?.CI) || isTrueish(env?.CONTINUOUS_INTEGRATION) || isTrueish(env?.BUILD_NUMBER);
33
38
  const apiUrls = {
34
39
  production: 'https://api.sanity.io/',
35
40
  staging: 'https://api.sanity.work/',
@@ -74,6 +79,7 @@ export default {
74
79
  isLive,
75
80
  apiUrl,
76
81
  populusApiUrl,
82
+ isCi,
77
83
  get token() {
78
84
  return _token === undefined ? loadToken() : _token;
79
85
  },
@@ -43,3 +43,4 @@ export declare const LABEL_MEDIA_LIBRARY_ASSET_UPDATE = "Media Library Asset Upd
43
43
  export declare const LABEL_MEDIA_LIBRARY_ASSET_DELETE = "Media Library Asset Delete";
44
44
  export declare const LABEL_SCHEDULED = "Scheduled";
45
45
  export declare const LABEL_SYNC_TAG_INVALIDATE = "Sync Tag Invalidate";
46
+ export declare const CI_SETUP_DOCS_URL = "https://www.sanity.io/docs/blueprints/deploy-blueprints-from-ci";
package/dist/constants.js CHANGED
@@ -48,3 +48,4 @@ export const LABEL_MEDIA_LIBRARY_ASSET_UPDATE = 'Media Library Asset Update';
48
48
  export const LABEL_MEDIA_LIBRARY_ASSET_DELETE = 'Media Library Asset Delete';
49
49
  export const LABEL_SCHEDULED = 'Scheduled';
50
50
  export const LABEL_SYNC_TAG_INVALIDATE = 'Sync Tag Invalidate';
51
+ export const CI_SETUP_DOCS_URL = 'https://www.sanity.io/docs/blueprints/deploy-blueprints-from-ci';
@@ -8,6 +8,7 @@ import { resolveDeployedResources } from '../../utils/blueprints/resolve-deploye
8
8
  import buildPayload from '../../utils/functions/build-payload.js';
9
9
  import { fetchAsset, fetchDocument } from '../../utils/functions/fetch-document.js';
10
10
  import { findFunctionInResources, getFunctionNames } from '../../utils/functions/find.js';
11
+ import { runFunction } from '../../utils/functions/invoke/nested.js';
11
12
  import { parseJsonObject } from '../../utils/parse-json-object.js';
12
13
  import { isEventType, } from '../../utils/types.js';
13
14
  export async function functionTestCore(options) {
@@ -146,12 +147,54 @@ export async function functionTestCore(options) {
146
147
  : {
147
148
  event: 'scheduled',
148
149
  };
150
+ // Records each nested context.invoke() so they can be surfaced in order after the spinner resolves.
151
+ const nestedRuns = [];
152
+ let nestedOrder = 0;
153
+ /**
154
+ * Re-entrant callback that resolves a nested context.invoke(name, payload):
155
+ * it finds the named function in the same blueprint, reuses the already
156
+ * resolved resources, and runs it via the shared invoke-chain helper (which
157
+ * applies the depth/cycle guard and extends the call stack). Passing itself
158
+ * as onNestedInvoke keeps it recursable. Every run is recorded so it can be
159
+ * surfaced after the spinner resolves, including failed ones — a downstream
160
+ * error is fire-and-forget (it doesn't reject the caller) but is still shown.
161
+ */
162
+ const runNested = async (nestedName, nestedPayload, ctx, depth, callStack) => {
163
+ const order = ++nestedOrder;
164
+ const nestedResource = findFunctionInResources(blueprint.resources, nestedName);
165
+ const result = await runFunction(nestedResource, { payload: { data: nestedPayload }, event: 'nested' }, { ...ctx, resources: resolvedResources }, { depth, callStack, onNestedInvoke: runNested });
166
+ nestedRuns.push({ order, name: nestedName, logs: result.logs, error: result.error });
167
+ return result;
168
+ };
169
+ const logNestedRuns = () => {
170
+ for (const run of [...nestedRuns].sort((a, b) => a.order - b.order)) {
171
+ log(`↳ BEGIN: invoke('${run.name}')`);
172
+ // Show any logs received
173
+ if (run.logs?.trim()) {
174
+ log(run.logs.trim());
175
+ }
176
+ // Before any error occurs
177
+ if (run.error) {
178
+ log(run.error.stack || run.error.message || run.error.name);
179
+ }
180
+ log(`↳ END: invoke('${run.name}')`);
181
+ }
182
+ };
149
183
  const spinner = log.ora('Executing function...').start();
150
184
  const { json: response, logs, error, } = await testAction(resource, invokeOptions, contextOptions, {
151
185
  timeout: timeout ? timeout : resource.timeout,
186
+ onNestedInvoke: runNested,
187
+ depth: 0,
188
+ callStack: [fnName],
152
189
  });
153
190
  if (error) {
154
191
  spinner.fail('Function execution failed.');
192
+ // Surface the function's own logs even on failure
193
+ if (logs?.trim()) {
194
+ log('Logs:');
195
+ log(logs);
196
+ }
197
+ logNestedRuns();
155
198
  const errorMessage = error.stack || error.message || error.name;
156
199
  const isTimeout = error.message?.includes('Timeout');
157
200
  return {
@@ -166,6 +209,7 @@ export async function functionTestCore(options) {
166
209
  spinner.succeed('Function execution succeeded.');
167
210
  log('Logs:');
168
211
  log(logs || '');
212
+ logNestedRuns();
169
213
  if (response) {
170
214
  log('Response:');
171
215
  log(JSON.stringify(response, null, 2));
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, watchFile } from 'node:fs';
2
2
  import * as http from 'node:http';
3
3
  import { default as mime } from 'mime-types';
4
- import { WebSocketServer } from 'ws';
4
+ import { WebSocket, WebSocketServer } from 'ws';
5
5
  import { findBlueprintFile, loadAndParseBlueprint } from '../actions/blueprints/blueprint.js';
6
6
  import { readConfigFile } from '../actions/blueprints/config.js';
7
7
  import { resolveIds } from '../actions/blueprints/resolve.js';
@@ -10,6 +10,35 @@ import { isRecord } from '../utils/is-record.js';
10
10
  import { isEventType, isGroqContextOptions, } from '../utils/types.js';
11
11
  import { handleInvokeRequest } from './handlers/invoke.js';
12
12
  const app = (host, port, validateResources, executionOptions) => {
13
+ const wss = new WebSocketServer({ port: 8974 });
14
+ // Send a JSON envelope to every connected UI client.
15
+ const broadcast = (envelope) => {
16
+ const message = JSON.stringify(envelope);
17
+ for (const client of wss.clients) {
18
+ if (client.readyState === WebSocket.OPEN)
19
+ client.send(message);
20
+ }
21
+ };
22
+ let nestedOrder = 0;
23
+ /**
24
+ * Re-entrant callback that resolves a nested `context.invoke(name, payload)` by
25
+ * re-entering the invoke pipeline (which spawns its own child) and streams the
26
+ * nested run to the UI. Passing `nestedInvoke` to itself keeps it recursable.
27
+ */
28
+ const nestedInvoke = async (name, payload, ctx, depth, callStack) => {
29
+ const order = ++nestedOrder;
30
+ const result = await handleInvokeRequest(name, { data: payload }, { event: 'nested' }, ctx, validateResources, executionOptions, depth, callStack, nestedInvoke);
31
+ broadcast({
32
+ type: 'nested-invoke',
33
+ name,
34
+ order,
35
+ logs: result.logs,
36
+ json: result.json,
37
+ error: result.error,
38
+ timings: result.timings,
39
+ });
40
+ return result;
41
+ };
13
42
  const requestListener = async (req, res) => {
14
43
  res.setHeader('Content-Type', 'application/json');
15
44
  switch (true) {
@@ -50,7 +79,7 @@ const app = (host, port, validateResources, executionOptions) => {
50
79
  delete context.clientOptions.token;
51
80
  }
52
81
  }
53
- const result = await handleInvokeRequest(functionName, event, metadata, context, validateResources, executionOptions);
82
+ const result = await handleInvokeRequest(functionName, event, metadata, context, validateResources, executionOptions, 0, [], nestedInvoke);
54
83
  // Add Server-Timing header
55
84
  const timingHeaders = [];
56
85
  for (const [key, value] of Object.entries(result.timings)) {
@@ -235,7 +264,6 @@ const app = (host, port, validateResources, executionOptions) => {
235
264
  };
236
265
  const server = http.createServer(requestListener);
237
266
  server.listen(port, host, () => { });
238
- const wss = new WebSocketServer({ port: 8974 });
239
267
  wss.on('connection', function connection(ws) {
240
268
  ws.on('error', console.error);
241
269
  const fileInfo = findBlueprintFile();
@@ -1,4 +1,4 @@
1
- import type { InvocationResponse, InvokeContextOptions, InvokeExecutionOptions, InvokePayloadMetadata } from '../../utils/types.js';
2
- export declare function handleInvokeRequest(functionName: string, event: Record<string, unknown>, metadata: InvokePayloadMetadata, context: InvokeContextOptions, validateResources: boolean, executionOptions?: Partial<InvokeExecutionOptions>): Promise<InvocationResponse & {
1
+ import type { InvocationResponse, InvokeContextOptions, InvokeExecutionOptions, InvokePayloadMetadata, NestedInvoke } from '../../utils/types.js';
2
+ export declare function handleInvokeRequest(functionName: string, event: Record<string, unknown>, metadata: InvokePayloadMetadata, context: InvokeContextOptions, validateResources: boolean, executionOptions?: Partial<InvokeExecutionOptions>, depth?: number, callStack?: string[], onNestedInvoke?: NestedInvoke): Promise<InvocationResponse & {
3
3
  timings: Record<string, number>;
4
4
  }>;
@@ -2,10 +2,10 @@ import { readLocalBlueprint } from '../../actions/blueprints/blueprint.js';
2
2
  import config from '../../config.js';
3
3
  import { resolveDeployedResources } from '../../utils/blueprints/resolve-deployed-resources.js';
4
4
  import { findFunctionInResources } from '../../utils/functions/find.js';
5
- import invoke from '../../utils/functions/invoke-local.js';
5
+ import { runFunction } from '../../utils/functions/invoke/nested.js';
6
6
  import { Logger } from '../../utils/logger.js';
7
7
  const silentLogger = Logger(() => { });
8
- export async function handleInvokeRequest(functionName, event, metadata, context, validateResources, executionOptions) {
8
+ export async function handleInvokeRequest(functionName, event, metadata, context, validateResources, executionOptions, depth = 0, callStack = [], onNestedInvoke) {
9
9
  const start = performance.now();
10
10
  const { resources, stackId, scopeType, scopeId } = await readLocalBlueprint(silentLogger, {
11
11
  resources: validateResources,
@@ -22,9 +22,12 @@ export async function handleInvokeRequest(functionName, event, metadata, context
22
22
  payload: event,
23
23
  ...metadata,
24
24
  };
25
- const response = await invoke(resource, payload, { ...context, resources: resolvedResources }, {
25
+ const response = await runFunction(resource, payload, { ...context, resources: resolvedResources }, {
26
26
  forceColor: executionOptions?.forceColor ?? false,
27
- timeout: executionOptions?.timeout ?? resource.timeout,
27
+ timeout: executionOptions?.timeout,
28
+ depth,
29
+ callStack,
30
+ onNestedInvoke,
28
31
  });
29
32
  const timings = { ...response.timings, 'blueprint:read': readBlueprintTime };
30
33
  return { ...response, timings };
@@ -1,4 +1,4 @@
1
- /* globals customElements document */
1
+ /* globals customElements document window */
2
2
  import {ApiBaseElement} from './api-base.js'
3
3
  import {getSharedStyleSheets} from './shared-styles.js'
4
4
 
@@ -36,6 +36,16 @@ template.innerHTML = `
36
36
  `
37
37
 
38
38
  class ConsolePanel extends ApiBaseElement {
39
+ /**
40
+ * Nested context.invoke() output for the current run, kept as a list so the
41
+ * final render can order it by call order. Nested runs finish innermost-first
42
+ * (A → B → C resolves C then B), so each carries an `order` stamped when it was
43
+ * initiated; the final render sorts by it. Buffered separately so it survives
44
+ * the result render, which would otherwise overwrite the console when Preserve
45
+ * Log is off.
46
+ */
47
+ nestedEntries = []
48
+
39
49
  constructor() {
40
50
  super()
41
51
  this.attachShadow({mode: 'open'}).appendChild(template.content.cloneNode(true))
@@ -45,25 +55,59 @@ class ConsolePanel extends ApiBaseElement {
45
55
  if (!this.consoleOutput || !result) return
46
56
 
47
57
  const {error, logs} = result
48
- let update = ''
58
+ // Always show the logs. Then append the error, rather than replacing the logs with it.
59
+ let update = logs ?? ''
49
60
  if (error) {
50
- // Display error details in the console
51
- update = (error.stack || error.message || error.name) ?? 'An error occurred.'
52
- } else {
53
- // Display regular logs
54
- update = logs ?? '' // Handle case where logs might be null/undefined
61
+ const errText = (error.stack || error.message || error.name) ?? 'An error occurred.'
62
+ update = update.trim() ? `${update.trimEnd()}\n${errText}` : errText
55
63
  }
56
64
 
57
65
  if (this.api.store.preserveLog) {
66
+ // Nested output was already appended live during the run; keep it in place.
58
67
  this.consoleOutput.innerText = this.consoleOutput.innerText + update
59
68
  } else {
60
- this.consoleOutput.innerText = update
69
+ // Re-render so the nested output is shown in call order rather than the
70
+ // completion order it streamed in (and so it survives this overwrite).
71
+ this.consoleOutput.innerText = update + this.renderNestedEntries()
72
+ }
73
+ }
74
+
75
+ // Join the nested runs in call order (the order they were initiated).
76
+ renderNestedEntries = () =>
77
+ [...this.nestedEntries]
78
+ .sort((a, b) => a.order - b.order)
79
+ .map((e) => e.text)
80
+ .join('')
81
+
82
+ updateNested = (event) => {
83
+ const nestedInvoke = event.detail
84
+ if (!this.consoleOutput || !nestedInvoke) return
85
+
86
+ const {name, order, logs, json, error} = nestedInvoke
87
+ // Always show the logs. Then append the error, rather than replacing the logs with it.
88
+ let body = logs ?? ''
89
+ if (error) {
90
+ const errText = (error.stack || error.message || error.name) ?? 'An error occurred.'
91
+ body = body.trim() ? `${body.trimEnd()}\n${errText}` : errText
92
+ } else if (typeof json !== 'undefined') {
93
+ body += `${body && !body.endsWith('\n') ? '\n' : ''}→ returned ${JSON.stringify(json)}\n`
61
94
  }
95
+
96
+ const text = `↳ BEGIN: invoke('${name}')\n${body.trim()}\n↳ END: invoke('${name}')\n`
97
+ this.nestedEntries.push({order, text})
98
+ // Append live for immediate feedback; the final result render reorders.
99
+ this.consoleOutput.innerText = this.consoleOutput.innerText + text
100
+ }
101
+
102
+ // A new invocation is starting; drop the previous run's nested output.
103
+ resetNested = ({inprogress}) => {
104
+ if (inprogress) this.nestedEntries = []
62
105
  }
63
106
 
64
107
  clear = () => {
65
108
  const backUp = this.api.store.preserveLog
66
109
  this.api.store.result = {logs: undefined, error: undefined}
110
+ this.nestedEntries = []
67
111
  this.consoleOutput.innerText = ''
68
112
  this.api.store.preserveLog = backUp
69
113
  }
@@ -75,9 +119,14 @@ class ConsolePanel extends ApiBaseElement {
75
119
  this.consoleOutput = this.shadowRoot.querySelector('#console-output')
76
120
  this.addEventListener('clear-console', this.clear)
77
121
 
122
+ // Nested context.invoke() runs arrive as a stream of discrete DOM events
123
+ // (not state) so none are dropped when several land in one frame.
124
+ window.addEventListener('nested-invoke', this.updateNested)
125
+
78
126
  // Subscribe to changes in the result state to update the console
79
127
  if (this.api) {
80
128
  this.api.subscribe(this.updateConsole, ['result'])
129
+ this.api.subscribe(this.resetNested, ['inprogress'])
81
130
  // Initial update in case result is already populated
82
131
  if (this.api.store.result) {
83
132
  this.updateConsole({result: this.api.store.result})
@@ -90,9 +139,11 @@ class ConsolePanel extends ApiBaseElement {
90
139
 
91
140
  disconnectedCallback() {
92
141
  this.removeEventListener('clear-console', this.clear)
142
+ window.removeEventListener('nested-invoke', this.updateNested)
93
143
  // Unsubscribe when the element is removed from the DOM
94
144
  if (this.api) {
95
145
  this.api.unsubscribe(this.updateConsole)
146
+ this.api.unsubscribe(this.resetNested)
96
147
  }
97
148
  }
98
149
  }
@@ -43,14 +43,17 @@ class RulePanel extends ApiBaseElement {
43
43
  break
44
44
  }
45
45
 
46
- const transaction = rule.state.update({
47
- changes: {
48
- from: 0,
49
- insert: func.event ? JSON.stringify(func.event, null, 2) : '',
50
- to: rule.state.doc.length,
51
- },
52
- })
53
- rule.dispatch(transaction)
46
+ // Guard against the sate not being setup
47
+ if (rule?.state) {
48
+ const transaction = rule.state.update({
49
+ changes: {
50
+ from: 0,
51
+ insert: func.event ? JSON.stringify(func.event, null, 2) : '',
52
+ to: rule.state.doc.length,
53
+ },
54
+ })
55
+ rule.dispatch(transaction)
56
+ }
54
57
  }
55
58
 
56
59
  async connectedCallback() {
@@ -1,31 +1,34 @@
1
1
  /* globals fetch */
2
2
 
3
- let cachedStyleSheets = null
3
+ let sharedStyleSheetsPromise = null
4
4
 
5
- export async function getSharedStyleSheets() {
6
- if (cachedStyleSheets) {
7
- return cachedStyleSheets
8
- }
5
+ async function loadSharedStyleSheets() {
6
+ // Fetch both stylesheets
7
+ const [mCssResponse, appCssResponse] = await Promise.all([
8
+ fetch('./vendor/m-.css'),
9
+ fetch('./components/app.css'),
10
+ ])
9
11
 
10
- try {
11
- // Fetch both stylesheets
12
- const [mCssResponse, appCssResponse] = await Promise.all([
13
- fetch('./vendor/m-.css'),
14
- fetch('./components/app.css'),
15
- ])
12
+ const [mCssText, appCssText] = await Promise.all([mCssResponse.text(), appCssResponse.text()])
16
13
 
17
- const [mCssText, appCssText] = await Promise.all([mCssResponse.text(), appCssResponse.text()])
14
+ // Create CSSStyleSheet objects
15
+ const mSheet = new CSSStyleSheet()
16
+ const appSheet = new CSSStyleSheet()
18
17
 
19
- // Create CSSStyleSheet objects
20
- const mSheet = new CSSStyleSheet()
21
- const appSheet = new CSSStyleSheet()
18
+ await Promise.all([mSheet.replace(mCssText), appSheet.replace(appCssText)])
22
19
 
23
- await Promise.all([mSheet.replace(mCssText), appSheet.replace(appCssText)])
20
+ return [mSheet, appSheet]
21
+ }
24
22
 
25
- cachedStyleSheets = [mSheet, appSheet]
26
- return cachedStyleSheets
27
- } catch (error) {
28
- console.error('Failed to load shared stylesheets:', error)
29
- return []
23
+ export function getSharedStyleSheets() {
24
+ if (!sharedStyleSheetsPromise) {
25
+ sharedStyleSheetsPromise = loadSharedStyleSheets().catch((error) => {
26
+ console.error('Failed to load shared stylesheets:', error)
27
+ // Reset so a later caller can retry instead of being stuck with [].
28
+ sharedStyleSheetsPromise = null
29
+ return []
30
+ })
30
31
  }
32
+
33
+ return sharedStyleSheetsPromise
31
34
  }