@sanity/runtime-cli 14.12.1 → 14.13.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.
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/14.12.1 linux-x64 node-v24.14.1
23
+ @sanity/runtime-cli/14.13.1 linux-x64 node-v24.14.1
24
24
  $ sanity-run --help [COMMAND]
25
25
  USAGE
26
26
  $ sanity-run COMMAND
@@ -50,7 +50,7 @@ USAGE
50
50
 
51
51
  ## `sanity-run blueprints add TYPE`
52
52
 
53
- Add a function resource to a Blueprint
53
+ [deprecated] Use "functions add" instead
54
54
 
55
55
  ```
56
56
  USAGE
@@ -82,12 +82,13 @@ FLAGS
82
82
  --[no-]validate-resources Validate resources
83
83
 
84
84
  DESCRIPTION
85
- Add a function resource to a Blueprint
85
+ [deprecated] Use "functions add" instead
86
86
 
87
- Scaffolds a new Sanity Function in your Blueprint. Functions are serverless handlers triggered by document events
88
- (create, update, delete, publish) or media library events.
87
+ This command is deprecated. Use "functions add" instead.
89
88
 
90
- After adding a function, use 'functions dev' to test locally, then 'blueprints deploy' to publish it.
89
+ Equivalent usage:
90
+ $ sanity-run functions add
91
+ $ sanity-run functions add --name my-function --type document-create
91
92
 
92
93
  EXAMPLES
93
94
  $ sanity-run blueprints add function
@@ -101,7 +102,7 @@ EXAMPLES
101
102
  $ sanity-run blueprints add function --name my-function --fn-type document-create --fn-type document-update --lang js
102
103
  ```
103
104
 
104
- _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/add.ts)_
105
+ _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/add.ts)_
105
106
 
106
107
  ## `sanity-run blueprints config`
107
108
 
@@ -140,7 +141,7 @@ EXAMPLES
140
141
  $ sanity-run blueprints config --edit --project-id <projectId> --stack <name-or-id>
141
142
  ```
142
143
 
143
- _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/config.ts)_
144
+ _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/config.ts)_
144
145
 
145
146
  ## `sanity-run blueprints deploy`
146
147
 
@@ -182,7 +183,7 @@ EXAMPLES
182
183
  $ sanity-run blueprints deploy --fn-installer npm
183
184
  ```
184
185
 
185
- _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/deploy.ts)_
186
+ _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/deploy.ts)_
186
187
 
187
188
  ## `sanity-run blueprints destroy`
188
189
 
@@ -218,7 +219,7 @@ EXAMPLES
218
219
  $ sanity-run blueprints destroy --stack <name-or-id> --project-id <projectId> --force --no-wait
219
220
  ```
220
221
 
221
- _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/destroy.ts)_
222
+ _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/destroy.ts)_
222
223
 
223
224
  ## `sanity-run blueprints doctor`
224
225
 
@@ -250,7 +251,7 @@ EXAMPLES
250
251
  $ sanity-run blueprints doctor --fix
251
252
  ```
252
253
 
253
- _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/doctor.ts)_
254
+ _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/doctor.ts)_
254
255
 
255
256
  ## `sanity-run blueprints info`
256
257
 
@@ -285,7 +286,7 @@ EXAMPLES
285
286
  $ sanity-run blueprints info --project-id <id> --stack <name-or-id>
286
287
  ```
287
288
 
288
- _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/info.ts)_
289
+ _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/info.ts)_
289
290
 
290
291
  ## `sanity-run blueprints init [DIR]`
291
292
 
@@ -338,7 +339,7 @@ EXAMPLES
338
339
  $ sanity-run blueprints init --blueprint-type <json|js|ts> --stack-name <stackName>
339
340
  ```
340
341
 
341
- _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/init.ts)_
342
+ _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/init.ts)_
342
343
 
343
344
  ## `sanity-run blueprints logs`
344
345
 
@@ -369,7 +370,7 @@ EXAMPLES
369
370
  $ sanity-run blueprints logs --watch
370
371
  ```
371
372
 
372
- _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/logs.ts)_
373
+ _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/logs.ts)_
373
374
 
374
375
  ## `sanity-run blueprints plan`
375
376
 
@@ -397,7 +398,7 @@ EXAMPLES
397
398
  $ sanity-run blueprints plan
398
399
  ```
399
400
 
400
- _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/plan.ts)_
401
+ _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/plan.ts)_
401
402
 
402
403
  ## `sanity-run blueprints stacks`
403
404
 
@@ -405,9 +406,11 @@ List remote Stack deployments for your project or organization
405
406
 
406
407
  ```
407
408
  USAGE
408
- $ sanity-run blueprints stacks [--json] [--validate-resources] [--project-id <value> | --organization-id <value>]
409
+ $ sanity-run blueprints stacks [--json] [--validate-resources] [--project-id <value> | --organization-id <value> |
410
+ --include-projects]
409
411
 
410
412
  FLAGS
413
+ --include-projects Include Stacks from all projects within the organization. Requires --organization-id.
411
414
  --json Format output as json
412
415
  --organization-id=<value> Sanity organization ID used to scope Blueprint and Stack
413
416
  --project-id=<value> Sanity project ID used to scope Blueprint and Stack
@@ -421,15 +424,19 @@ DESCRIPTION
421
424
  Use this to discover existing Stacks you can scope a local Blueprint to (using 'blueprints config --edit'), or to
422
425
  audit what's deployed across your project.
423
426
 
427
+ Use --include-projects with --organization-id to also list Stacks from all projects within the organization.
428
+
424
429
  EXAMPLES
425
430
  $ sanity-run blueprints stacks
426
431
 
427
432
  $ sanity-run blueprints stacks --project-id <projectId>
428
433
 
429
434
  $ sanity-run blueprints stacks --organization-id <organizationId>
435
+
436
+ $ sanity-run blueprints stacks --organization-id <organizationId> --include-projects
430
437
  ```
431
438
 
432
- _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/blueprints/stacks.ts)_
439
+ _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/blueprints/stacks.ts)_
433
440
 
434
441
  ## `sanity-run functions add`
435
442
 
@@ -482,7 +489,7 @@ EXAMPLES
482
489
  $ sanity-run functions add --name my-function --type document-create --type document-update --lang js
483
490
  ```
484
491
 
485
- _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/functions/add.ts)_
492
+ _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/functions/add.ts)_
486
493
 
487
494
  ## `sanity-run functions dev`
488
495
 
@@ -518,7 +525,7 @@ EXAMPLES
518
525
  $ sanity-run functions dev --timeout 60
519
526
  ```
520
527
 
521
- _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/functions/dev.ts)_
528
+ _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/functions/dev.ts)_
522
529
 
523
530
  ## `sanity-run functions env add NAME KEY VALUE`
524
531
 
@@ -549,7 +556,7 @@ EXAMPLES
549
556
  $ sanity-run functions env add MyFunction API_URL https://api.example.com/
550
557
  ```
551
558
 
552
- _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/functions/env/add.ts)_
559
+ _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/functions/env/add.ts)_
553
560
 
554
561
  ## `sanity-run functions env list NAME`
555
562
 
@@ -577,7 +584,7 @@ EXAMPLES
577
584
  $ sanity-run functions env list MyFunction
578
585
  ```
579
586
 
580
- _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/functions/env/list.ts)_
587
+ _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/functions/env/list.ts)_
581
588
 
582
589
  ## `sanity-run functions env remove NAME KEY`
583
590
 
@@ -607,7 +614,7 @@ EXAMPLES
607
614
  $ sanity-run functions env remove MyFunction API_URL
608
615
  ```
609
616
 
610
- _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/functions/env/remove.ts)_
617
+ _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/functions/env/remove.ts)_
611
618
 
612
619
  ## `sanity-run functions logs [NAME]`
613
620
 
@@ -649,7 +656,7 @@ EXAMPLES
649
656
  $ sanity-run functions logs <name> --delete
650
657
  ```
651
658
 
652
- _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/functions/logs.ts)_
659
+ _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/functions/logs.ts)_
653
660
 
654
661
  ## `sanity-run functions test [NAME]`
655
662
 
@@ -707,7 +714,7 @@ EXAMPLES
707
714
  $ sanity-run functions test <name> --event update --data-before '{ "title": "before" }' --data-after '{ "title": "after" }'
708
715
  ```
709
716
 
710
- _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v14.12.1/src/commands/functions/test.ts)_
717
+ _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v14.13.1/src/commands/functions/test.ts)_
711
718
 
712
719
  ## `sanity-run help [COMMAND]`
713
720
 
@@ -0,0 +1,20 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { AuthParams, BlueprintLog } from '../../utils/types.js';
3
+ export declare const LOG_POLL_INTERVAL_MS = 725;
4
+ export interface LogPollingConfig {
5
+ stackId: string;
6
+ operationId?: string;
7
+ knownIds?: string[];
8
+ auth: AuthParams;
9
+ showBanner?: boolean;
10
+ verbose?: boolean;
11
+ log: Logger;
12
+ onActivity?: () => void;
13
+ onLogEntry?: (log: BlueprintLog) => void;
14
+ }
15
+ /**
16
+ * Polls the logs list endpoint at a fixed interval and displays new entries.
17
+ * Deduplicates by log ID so no dependency on server timestamps.
18
+ * @returns A cleanup function that stops polling
19
+ */
20
+ export declare function setupLogPolling(config: LogPollingConfig): () => void;
@@ -0,0 +1,48 @@
1
+ import { formatLogEntry } from '../../utils/display/logs-formatting.js';
2
+ import { styleText } from '../../utils/style-text.js';
3
+ import { getLogs } from './logs.js';
4
+ export const LOG_POLL_INTERVAL_MS = 725;
5
+ /**
6
+ * Polls the logs list endpoint at a fixed interval and displays new entries.
7
+ * Deduplicates by log ID so no dependency on server timestamps.
8
+ * @returns A cleanup function that stops polling
9
+ */
10
+ export function setupLogPolling(config) {
11
+ const { stackId, operationId, auth, log, showBanner, verbose = false } = config;
12
+ const seenIds = new Set(config.knownIds);
13
+ let previousLog;
14
+ let stopped = false;
15
+ if (showBanner) {
16
+ log(`Watching logs... ${styleText('bold', 'ctrl+c')} to cancel`);
17
+ }
18
+ const fetchAndProcessLogs = async () => {
19
+ if (stopped)
20
+ return;
21
+ try {
22
+ const params = operationId ? { operationId } : { stackId };
23
+ const { ok, logs } = await getLogs(params, auth, log);
24
+ if (!ok || stopped)
25
+ return;
26
+ // API returns newest first; reverse so we display in chronological order
27
+ const newLogs = logs.filter((entry) => !seenIds.has(entry.id)).reverse();
28
+ for (const logEntry of newLogs) {
29
+ seenIds.add(logEntry.id);
30
+ config.onActivity?.();
31
+ config.onLogEntry?.(logEntry);
32
+ log(formatLogEntry(logEntry, verbose, previousLog));
33
+ previousLog = logEntry;
34
+ }
35
+ }
36
+ catch {
37
+ // Swallow fetch errors; the operation status poll in callers handles terminal failure.
38
+ // Logging noise here is not useful.
39
+ }
40
+ };
41
+ // Fire immediately, then on interval
42
+ fetchAndProcessLogs();
43
+ const intervalId = setInterval(fetchAndProcessLogs, LOG_POLL_INTERVAL_MS);
44
+ return () => {
45
+ stopped = true;
46
+ clearInterval(intervalId);
47
+ };
48
+ }
@@ -1,7 +1,13 @@
1
1
  import type { Logger } from '../../utils/logger.js';
2
2
  import type { ActionResponse, AuthParams, BlueprintLog } from '../../utils/types.js';
3
3
  export declare const logsUrl: string;
4
- export declare function getLogs(stackId: string, auth: AuthParams, logger: Logger): Promise<ActionResponse & {
4
+ export interface GetLogsParams {
5
+ stackId?: string;
6
+ operationId?: string;
7
+ limit?: number;
8
+ before?: string;
9
+ }
10
+ export declare function getLogs(params: GetLogsParams, auth: AuthParams, logger: Logger): Promise<ActionResponse & {
5
11
  logs: BlueprintLog[];
6
12
  }>;
7
13
  export declare function getRecentLogs(logs: BlueprintLog[], limit?: number): BlueprintLog[];
@@ -3,10 +3,17 @@ import getHeaders from '../../utils/get-headers.js';
3
3
  import { createTracedFetch } from '../../utils/traced-fetch.js';
4
4
  const { apiUrl } = config;
5
5
  export const logsUrl = `${apiUrl}vX/blueprints/logs`;
6
- export async function getLogs(stackId, auth, logger) {
6
+ export async function getLogs(params, auth, logger) {
7
7
  const fetchFn = createTracedFetch(logger);
8
8
  const url = new URL(logsUrl);
9
- url.searchParams.append('stackId', stackId);
9
+ if (params.stackId)
10
+ url.searchParams.append('stackId', params.stackId);
11
+ if (params.operationId)
12
+ url.searchParams.append('operationId', params.operationId);
13
+ if (params.limit)
14
+ url.searchParams.append('limit', String(params.limit));
15
+ if (params.before)
16
+ url.searchParams.append('before', params.before);
10
17
  const response = await fetchFn(url.toString(), {
11
18
  headers: getHeaders(auth),
12
19
  method: 'GET',
@@ -37,7 +37,9 @@ export declare function createEmptyStack({ token, scopeType, scopeId, name, logg
37
37
  logger: Logger;
38
38
  }): Promise<Stack>;
39
39
  interface UpdateStackResponse extends ActionResponse {
40
- stack: Stack;
40
+ stack: Stack & {
41
+ operationId?: string;
42
+ };
41
43
  }
42
44
  export declare function updateStack({ stackId, stackMutation, auth, logger, }: {
43
45
  stackId: string;
@@ -96,7 +98,9 @@ export declare function planStack({ stackId, document, auth, logger, }: {
96
98
  logger: Logger;
97
99
  }): Promise<PlanStackResponse>;
98
100
  interface DestroyStackResponse extends ActionResponse {
99
- stack: Stack;
101
+ stack: Stack & {
102
+ operationId?: string;
103
+ };
100
104
  }
101
105
  export declare function resolveStackIdByNameOrId(value: string, auth: AuthParams, logger: Logger): Promise<string>;
102
106
  interface PromoteStackResponse extends ActionResponse {
@@ -17,8 +17,9 @@ export interface Project {
17
17
  interface ListProjectsResponse extends ActionResponse {
18
18
  projects: Project[];
19
19
  }
20
- export declare function listProjects({ token, logger, }: {
20
+ export declare function listProjects({ token, organizationId, logger, }: {
21
21
  token: string;
22
+ organizationId?: string;
22
23
  logger: Logger;
23
24
  }): Promise<ListProjectsResponse>;
24
25
  interface GroupedProjectsByOrganizationResponse extends ActionResponse {
@@ -3,9 +3,12 @@ import { createTracedFetch } from '../../utils/traced-fetch.js';
3
3
  const { populusApiUrl } = config;
4
4
  export const projectsApiPath = `${populusApiUrl}v2021-06-07/projects`;
5
5
  export const orgsApiPath = `${populusApiUrl}v2021-06-07/organizations`;
6
- export async function listProjects({ token, logger, }) {
6
+ export async function listProjects({ token, organizationId, logger, }) {
7
7
  const fetchFn = createTracedFetch(logger);
8
- const projectsFetch = await fetchFn(projectsApiPath, {
8
+ const url = organizationId
9
+ ? `${projectsApiPath}?organizationId=${encodeURIComponent(organizationId)}`
10
+ : projectsApiPath;
11
+ const projectsFetch = await fetchFn(url, {
9
12
  method: 'GET',
10
13
  headers: {
11
14
  Authorization: `Bearer ${token}`,
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * @file
3
3
  * @deprecated Use `functions add` instead.
4
- * We're in the process of deprecating the `blueprints add` command.
5
4
  */
6
5
  import { ResolvedCommand } from '../../baseCommands.js';
7
6
  export default class AddCommand extends ResolvedCommand<typeof AddCommand> {
8
7
  static needs: readonly ["blueprint"];
8
+ static state: string;
9
+ static deprecationOptions: {
10
+ to: string;
11
+ };
9
12
  static summary: string;
10
13
  static description: string;
11
14
  static examples: string[];
@@ -1,25 +1,26 @@
1
1
  /**
2
2
  * @file
3
3
  * @deprecated Use `functions add` instead.
4
- * We're in the process of deprecating the `blueprints add` command.
5
4
  */
6
5
  import { Args, Flags } from '@oclif/core';
7
6
  import { ResolvedCommand } from '../../baseCommands.js';
8
7
  import { FUNCTION_TYPES } from '../../constants.js';
9
8
  import { functionAddCore } from '../../cores/functions/index.js';
9
+ import { warn } from '../../utils/display/presenters.js';
10
10
  import { Logger } from '../../utils/logger.js';
11
11
  import { INSTALLER_OPTIONS } from '../../utils/types.js';
12
- // import {warn} from '../../utils/display/presenters.js'
13
12
  export default class AddCommand extends ResolvedCommand {
14
13
  static needs = ['blueprint'];
15
- // static state = 'deprecated'
16
- // static deprecationOptions = {
17
- // message: '`blueprints add` is deprecated. Use `functions add` instead.',
18
- // }
19
- static summary = 'Add a function resource to a Blueprint';
20
- static description = `Scaffolds a new Sanity Function in your Blueprint. Functions are serverless handlers triggered by document events (create, update, delete, publish) or media library events.
14
+ static state = 'deprecated';
15
+ static deprecationOptions = {
16
+ to: 'functions add',
17
+ };
18
+ static summary = '[deprecated] Use "functions add" instead';
19
+ static description = `This command is deprecated. Use "functions add" instead.
21
20
 
22
- After adding a function, use 'functions dev' to test locally, then 'blueprints deploy' to publish it.`;
21
+ Equivalent usage:
22
+ $ <%= config.bin %> functions add
23
+ $ <%= config.bin %> functions add --name my-function --type document-create`;
23
24
  static examples = [
24
25
  '<%= config.bin %> <%= command.id %> function',
25
26
  '<%= config.bin %> <%= command.id %> function --helpers',
@@ -84,11 +85,7 @@ After adding a function, use 'functions dev' to test locally, then 'blueprints d
84
85
  if (resourceType !== 'function') {
85
86
  this.error(`Unsupported Resource type: ${resourceType}`);
86
87
  }
87
- // log(
88
- // warn(
89
- // `\`npx ${bin} blueprints add function\` is deprecated. Use \`npx ${bin} functions add\` instead.`,
90
- // ),
91
- // )
88
+ this.log(warn(`"${bin} blueprints add" is deprecated and will be removed in a future release. Use "${bin} functions add" instead.`));
92
89
  const result = await functionAddCore({
93
90
  bin,
94
91
  log: Logger(this.log.bind(this), this.flags),
@@ -7,6 +7,7 @@ export default class StacksCommand extends ResolvedCommand<typeof StacksCommand>
7
7
  static flags: {
8
8
  'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
9
  'organization-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'include-projects': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  };
11
12
  run(): Promise<Record<string, unknown> | undefined>;
12
13
  }
@@ -7,15 +7,25 @@ export default class StacksCommand extends ResolvedCommand {
7
7
  static summary = 'List remote Stack deployments for your project or organization';
8
8
  static description = `Shows all Stacks associated with a project or organization. By default, lists Stacks scoped to the local Blueprint.
9
9
 
10
- Use this to discover existing Stacks you can scope a local Blueprint to (using 'blueprints config --edit'), or to audit what's deployed across your project.`;
10
+ Use this to discover existing Stacks you can scope a local Blueprint to (using 'blueprints config --edit'), or to audit what's deployed across your project.
11
+
12
+ Use --include-projects with --organization-id to also list Stacks from all projects within the organization.`;
11
13
  static examples = [
12
14
  '<%= config.bin %> <%= command.id %>',
13
15
  '<%= config.bin %> <%= command.id %> --project-id <projectId>',
14
16
  '<%= config.bin %> <%= command.id %> --organization-id <organizationId>',
17
+ '<%= config.bin %> <%= command.id %> --organization-id <organizationId> --include-projects',
15
18
  ];
16
19
  static flags = {
17
- 'project-id': Flags.string({ ...projectIdFlagConfig, exclusive: ['organization-id'] }),
20
+ 'project-id': Flags.string({
21
+ ...projectIdFlagConfig,
22
+ exclusive: ['organization-id', 'include-projects'],
23
+ }),
18
24
  'organization-id': Flags.string({ ...organizationIdFlagConfig, exclusive: ['project-id'] }),
25
+ 'include-projects': Flags.boolean({
26
+ description: 'Include Stacks from all projects within the organization. Requires --organization-id.',
27
+ dependsOn: ['organization-id'],
28
+ }),
19
29
  };
20
30
  async run() {
21
31
  const result = await blueprintStacksCore({
@@ -1,6 +1,6 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
2
  import { stashAsset } from '../../actions/blueprints/assets.js';
3
- import { setupLogStreaming } from '../../actions/blueprints/logs-streaming.js';
3
+ import { setupLogPolling } from '../../actions/blueprints/logs-polling.js';
4
4
  import { getStack, updateStack } from '../../actions/blueprints/stacks.js';
5
5
  import { createLogHintCollector } from '../../utils/blueprints/log-hints.js';
6
6
  import { niceId } from '../../utils/display/presenters.js';
@@ -47,7 +47,6 @@ export async function blueprintDeployCore(options) {
47
47
  }
48
48
  }
49
49
  const spinner = log.ora('Deploying...').start();
50
- const isoNow = new Date().toISOString();
51
50
  const { ok: deployOk, stack, error: deployError, } = await updateStack({
52
51
  stackId,
53
52
  stackMutation: {
@@ -87,9 +86,9 @@ export async function blueprintDeployCore(options) {
87
86
  try {
88
87
  let lastLogAt = Date.now();
89
88
  let idleMessageShown = false;
90
- logStreamCleanup = await setupLogStreaming({
89
+ logStreamCleanup = setupLogPolling({
91
90
  stackId: stack.id,
92
- after: isoNow,
91
+ operationId: stack.operationId,
93
92
  auth,
94
93
  log,
95
94
  verbose,
@@ -1,6 +1,6 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
2
  import { confirm } from '@inquirer/prompts';
3
- import { setupLogStreaming } from '../../actions/blueprints/logs-streaming.js';
3
+ import { setupLogPolling } from '../../actions/blueprints/logs-polling.js';
4
4
  import { destroyStack, getStack, resolveStackIdByNameOrId } from '../../actions/blueprints/stacks.js';
5
5
  import { createLogHintCollector } from '../../utils/blueprints/log-hints.js';
6
6
  import { niceId } from '../../utils/display/presenters.js';
@@ -25,7 +25,6 @@ export async function blueprintDestroyCore(options) {
25
25
  }
26
26
  const auth = { token, scopeType, scopeId };
27
27
  const resolvedId = await resolveStackIdByNameOrId(flagStack, auth, log);
28
- const isoNow = new Date().toISOString();
29
28
  const { ok, error, stack } = await destroyStack({
30
29
  stackId: resolvedId,
31
30
  auth,
@@ -40,10 +39,10 @@ export async function blueprintDestroyCore(options) {
40
39
  return waitForDestruction({
41
40
  stackId: stack.id,
42
41
  stackName: stack.name,
42
+ operationId: stack.operationId,
43
43
  auth,
44
44
  log,
45
45
  bin,
46
- since: isoNow,
47
46
  });
48
47
  }
49
48
  const { scopeType, scopeId, stackId } = blueprint;
@@ -103,8 +102,11 @@ export async function blueprintDestroyCore(options) {
103
102
  else {
104
103
  destroySpinner.start();
105
104
  }
106
- const isoNow = new Date().toISOString();
107
- const { ok, error } = await destroyStack({ stackId: stack.id, auth, logger: log });
105
+ const { ok, error, stack: destroyedStack, } = await destroyStack({
106
+ stackId: stack.id,
107
+ auth,
108
+ logger: log,
109
+ });
108
110
  if (!ok) {
109
111
  destroySpinner.fail('Failed to destroy Stack deployment');
110
112
  return { success: false, error: error || 'Failed to destroy Stack deployment' };
@@ -117,10 +119,10 @@ export async function blueprintDestroyCore(options) {
117
119
  return waitForDestruction({
118
120
  stackId: stack.id,
119
121
  stackName: stack.name,
122
+ operationId: destroyedStack.operationId,
120
123
  auth,
121
124
  log,
122
125
  bin,
123
- since: isoNow,
124
126
  });
125
127
  }
126
128
  catch (error) {
@@ -129,16 +131,16 @@ export async function blueprintDestroyCore(options) {
129
131
  }
130
132
  }
131
133
  async function waitForDestruction(options) {
132
- const { stackId, stackName, auth, log, bin, since } = options;
134
+ const { stackId, stackName, operationId, auth, log, bin } = options;
133
135
  log(styleText('dim', 'Stack destruction progress:'));
134
136
  const logHints = createLogHintCollector(bin);
135
137
  let logStreamCleanup = null;
136
138
  try {
137
139
  let lastLogAt = Date.now();
138
140
  let idleMessageShown = false;
139
- logStreamCleanup = await setupLogStreaming({
141
+ logStreamCleanup = setupLogPolling({
140
142
  stackId,
141
- after: since,
143
+ operationId,
142
144
  auth,
143
145
  log,
144
146
  onActivity: () => {
@@ -1,5 +1,5 @@
1
1
  import { getLogs, getRecentLogs } from '../../actions/blueprints/logs.js';
2
- import { setupLogStreaming } from '../../actions/blueprints/logs-streaming.js';
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';
@@ -10,7 +10,7 @@ export async function blueprintLogsCore(options) {
10
10
  const spinner = log.ora(`Fetching recent logs for Stack deployment ${niceId(stackId)}`).start();
11
11
  try {
12
12
  if (watch) {
13
- const { ok, logs, error } = await getLogs(stackId, auth, log);
13
+ const { ok, logs, error } = await getLogs({ stackId }, auth, log);
14
14
  if (!ok) {
15
15
  spinner.fail(`${styleText('red', 'Failed')} to retrieve logs`);
16
16
  log.error(`Error: ${error || 'Unknown error'}`);
@@ -30,22 +30,23 @@ export async function blueprintLogsCore(options) {
30
30
  else {
31
31
  log(`No recent logs found for Stack deployment ${niceId(stackId)}`);
32
32
  }
33
- // Set up streaming log display
34
- await setupLogStreaming({
33
+ // Set up polling log display, seeding with already-displayed log IDs
34
+ setupLogPolling({
35
35
  stackId,
36
+ knownIds: logs.map((l) => l.id),
36
37
  auth,
37
38
  log,
38
39
  showBanner: true,
39
40
  verbose,
40
41
  });
41
- // Return a special key for streaming mode
42
+ // Return a special key for polling mode
42
43
  return {
43
44
  success: true,
44
45
  streaming: new Promise(() => { }),
45
46
  };
46
47
  }
47
48
  // Regular non-streaming logs
48
- const { ok, logs, error } = await getLogs(stackId, auth, log);
49
+ const { ok, logs, error } = await getLogs({ stackId }, auth, log);
49
50
  if (!ok) {
50
51
  spinner.fail(`${styleText('red', 'Failed')} to retrieve Stack deployment logs`);
51
52
  log.error(`Error: ${error || 'Unknown error'}`);
@@ -3,6 +3,7 @@ export interface BlueprintStacksOptions extends BlueprintConfig {
3
3
  flags: {
4
4
  'project-id'?: string;
5
5
  'organization-id'?: string;
6
+ 'include-projects'?: boolean;
6
7
  verbose?: boolean;
7
8
  };
8
9
  }
@@ -1,11 +1,14 @@
1
1
  import { listStacks } from '../../actions/blueprints/stacks.js';
2
+ import { listProjects } from '../../actions/sanity/projects.js';
2
3
  import { formatStacksListing } from '../../utils/display/blueprints-formatting.js';
3
- import { capitalize, niceId } from '../../utils/display/presenters.js';
4
+ import { capitalize, niceId, warn } from '../../utils/display/presenters.js';
4
5
  import { styleText } from '../../utils/style-text.js';
6
+ /** Max projects to query for stacks during --include-projects */
7
+ const PROJECT_LIMIT = 50;
5
8
  export async function blueprintStacksCore(options) {
6
9
  const { log, token, blueprint, flags } = options;
7
10
  const { scopeType: blueprintScopeType, scopeId: blueprintScopeId, stackId: blueprintStackId, } = blueprint;
8
- const { 'project-id': flagProjectId, 'organization-id': flagOrganizationId, verbose: _verbose = false, } = flags;
11
+ const { 'project-id': flagProjectId, 'organization-id': flagOrganizationId, 'include-projects': includeProjects = false, verbose: _verbose = false, } = flags;
9
12
  if (flagOrganizationId && flagProjectId) {
10
13
  log.error('Cannot specify both --organization-id and --project-id');
11
14
  return { success: false, error: 'Cannot specify both --organization-id and --project-id' };
@@ -27,6 +30,9 @@ export async function blueprintStacksCore(options) {
27
30
  suggestions: ['Run in a Blueprint directory or provide a project with --project-id.'],
28
31
  };
29
32
  }
33
+ if (includeProjects && scopeType === 'organization') {
34
+ return deepListStacks({ organizationId: scopeId, token, log, blueprintStackId });
35
+ }
30
36
  try {
31
37
  const spinner = log.ora('Fetching Stacks...').start();
32
38
  const { ok, stacks, error } = await listStacks({ token, scopeType, scopeId }, log);
@@ -57,3 +63,68 @@ export async function blueprintStacksCore(options) {
57
63
  return { success: false, error: errorMessage };
58
64
  }
59
65
  }
66
+ async function deepListStacks(options) {
67
+ const { organizationId, token, log, blueprintStackId } = options;
68
+ const allStacks = [];
69
+ const spinner = log.ora('Fetching Stacks...').start();
70
+ try {
71
+ // 1. Org-scoped stacks
72
+ const orgResult = await listStacks({ token, scopeType: 'organization', scopeId: organizationId }, log);
73
+ const orgStacks = orgResult.ok ? orgResult.stacks : [];
74
+ // 2. Get projects in the organization
75
+ const projectsResult = await listProjects({ token, organizationId, logger: log });
76
+ if (!projectsResult.ok) {
77
+ spinner.fail('Failed to list projects');
78
+ return { success: false, error: projectsResult.error || 'Failed to list projects' };
79
+ }
80
+ const projects = projectsResult.projects;
81
+ const totalProjects = projects.length;
82
+ const cappedProjects = projects.slice(0, PROJECT_LIMIT);
83
+ // 3. Fetch stacks for each project
84
+ const projectGroups = [];
85
+ for (let i = 0; i < cappedProjects.length; i++) {
86
+ const project = cappedProjects[i];
87
+ spinner.text = `Fetching Stacks... (project ${i + 1}/${cappedProjects.length})`;
88
+ const result = await listStacks({ token, scopeType: 'project', scopeId: project.id }, log);
89
+ if (result.ok && result.stacks.length > 0) {
90
+ projectGroups.push({
91
+ projectId: project.id,
92
+ displayName: project.displayName,
93
+ stacks: result.stacks,
94
+ });
95
+ allStacks.push(...result.stacks);
96
+ }
97
+ }
98
+ spinner.stop().clear();
99
+ // 4. Print results
100
+ if (orgStacks.length > 0) {
101
+ log(`${styleText('bold', 'Organization')} ${niceId(organizationId)} ${styleText('bold', 'Stacks')}:\n`);
102
+ log(formatStacksListing(orgStacks, blueprintStackId));
103
+ if (projectGroups.length > 0)
104
+ log(''); // blank line between sections
105
+ }
106
+ for (const group of projectGroups) {
107
+ log(`${styleText('bold', 'Project')} "${group.displayName}" ${niceId(group.projectId)} ${styleText('bold', 'Stacks')}:`);
108
+ log(formatStacksListing(group.stacks, blueprintStackId));
109
+ log('');
110
+ }
111
+ if (totalProjects > PROJECT_LIMIT) {
112
+ log(warn(`Organization has ${totalProjects} projects, only checked the first ${PROJECT_LIMIT}.`));
113
+ }
114
+ if (orgStacks.length === 0 && allStacks.length === 0) {
115
+ log('No stacks found');
116
+ return { success: true };
117
+ }
118
+ allStacks.push(...orgStacks);
119
+ return {
120
+ success: true,
121
+ json: { stacks: allStacks, scopeType: 'organization', scopeId: organizationId },
122
+ };
123
+ }
124
+ catch (error) {
125
+ spinner.stop().clear();
126
+ const errorMessage = error instanceof Error ? error.message : String(error);
127
+ log.error(`Error: ${errorMessage}`);
128
+ return { success: false, error: errorMessage };
129
+ }
130
+ }
@@ -28,11 +28,13 @@ function arrayifyEvent(event) {
28
28
  if ('projection' in event && event.projection) {
29
29
  details.push(formatLabeledValue('projection', event.projection));
30
30
  }
31
- // TODO: expose event.resource details once we are ready
32
- // https://linear.app/sanity/issue/RUN-1246/cli-should-show-eventresource-details-in-blueprints-infoplans
33
- // if (event.resource) {
34
- // details.push(formatLabeledValue('resource', JSON.stringify(event.resource)))
35
- // }
31
+ if (event.resource) {
32
+ details.push(formatLabel('resource'));
33
+ details.push([
34
+ formatLabeledValue('type', event.resource.type),
35
+ formatLabeledValue('id', event.resource.id),
36
+ ]);
37
+ }
36
38
  }
37
39
  return details;
38
40
  }
@@ -12,7 +12,10 @@
12
12
  "required": true
13
13
  }
14
14
  },
15
- "description": "Scaffolds a new Sanity Function in your Blueprint. Functions are serverless handlers triggered by document events (create, update, delete, publish) or media library events.\n\nAfter adding a function, use 'functions dev' to test locally, then 'blueprints deploy' to publish it.",
15
+ "deprecationOptions": {
16
+ "to": "functions add"
17
+ },
18
+ "description": "This command is deprecated. Use \"functions add\" instead.\n\nEquivalent usage:\n $ <%= config.bin %> functions add\n $ <%= config.bin %> functions add --name my-function --type document-create",
16
19
  "examples": [
17
20
  "<%= config.bin %> <%= command.id %> function",
18
21
  "<%= config.bin %> <%= command.id %> function --helpers",
@@ -216,8 +219,9 @@
216
219
  "pluginAlias": "@sanity/runtime-cli",
217
220
  "pluginName": "@sanity/runtime-cli",
218
221
  "pluginType": "core",
222
+ "state": "deprecated",
219
223
  "strict": true,
220
- "summary": "Add a function resource to a Blueprint",
224
+ "summary": "[deprecated] Use \"functions add\" instead",
221
225
  "enableJsonFlag": true,
222
226
  "needs": [
223
227
  "blueprint"
@@ -1289,11 +1293,12 @@
1289
1293
  "blueprints:stacks": {
1290
1294
  "aliases": [],
1291
1295
  "args": {},
1292
- "description": "Shows all Stacks associated with a project or organization. By default, lists Stacks scoped to the local Blueprint.\n\nUse this to discover existing Stacks you can scope a local Blueprint to (using 'blueprints config --edit'), or to audit what's deployed across your project.",
1296
+ "description": "Shows all Stacks associated with a project or organization. By default, lists Stacks scoped to the local Blueprint.\n\nUse this to discover existing Stacks you can scope a local Blueprint to (using 'blueprints config --edit'), or to audit what's deployed across your project.\n\nUse --include-projects with --organization-id to also list Stacks from all projects within the organization.",
1293
1297
  "examples": [
1294
1298
  "<%= config.bin %> <%= command.id %>",
1295
1299
  "<%= config.bin %> <%= command.id %> --project-id <projectId>",
1296
- "<%= config.bin %> <%= command.id %> --organization-id <organizationId>"
1300
+ "<%= config.bin %> <%= command.id %> --organization-id <organizationId>",
1301
+ "<%= config.bin %> <%= command.id %> --organization-id <organizationId> --include-projects"
1297
1302
  ],
1298
1303
  "flags": {
1299
1304
  "json": {
@@ -1350,7 +1355,8 @@
1350
1355
  ],
1351
1356
  "description": "Sanity project ID used to scope Blueprint and Stack",
1352
1357
  "exclusive": [
1353
- "organization-id"
1358
+ "organization-id",
1359
+ "include-projects"
1354
1360
  ],
1355
1361
  "name": "project-id",
1356
1362
  "hasDynamicHelp": false,
@@ -1371,6 +1377,15 @@
1371
1377
  "hasDynamicHelp": false,
1372
1378
  "multiple": false,
1373
1379
  "type": "option"
1380
+ },
1381
+ "include-projects": {
1382
+ "dependsOn": [
1383
+ "organization-id"
1384
+ ],
1385
+ "description": "Include Stacks from all projects within the organization. Requires --organization-id.",
1386
+ "name": "include-projects",
1387
+ "allowNo": false,
1388
+ "type": "boolean"
1374
1389
  }
1375
1390
  },
1376
1391
  "hasDynamicHelp": false,
@@ -2677,5 +2692,5 @@
2677
2692
  ]
2678
2693
  }
2679
2694
  },
2680
- "version": "14.12.1"
2695
+ "version": "14.13.1"
2681
2696
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanity/runtime-cli",
3
3
  "description": "Sanity's Runtime CLI for Blueprints and Functions",
4
- "version": "14.12.1",
4
+ "version": "14.13.1",
5
5
  "author": "Sanity Runtime Team",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -94,6 +94,7 @@
94
94
  "test:api": "cd test/api-types && npm it",
95
95
  "test:depmgmt": "vitest run --config ./test-depmgmt/vitest.config.ts",
96
96
  "test:vitest": "vitest run",
97
+ "test:coverage": "npm run test:vitest -- --coverage --coverage.include \"runtime-cli/src/**/*\"",
97
98
  "typecheck": "tsc --project tsconfig.typecheck.json",
98
99
  "watch": "tsc --watch"
99
100
  },
@@ -137,6 +138,7 @@
137
138
  "@types/node": "20",
138
139
  "@types/tar-stream": "^3.1.4",
139
140
  "@types/ws": "^8.18.1",
141
+ "@vitest/coverage-v8": "^4.1.0",
140
142
  "codemirror": "^6.0.2",
141
143
  "mentoss": "^0.13.0",
142
144
  "oclif": "^4.22.92",
@@ -1,30 +0,0 @@
1
- import type { Logger } from '../../utils/logger.js';
2
- import type { AuthParams, BlueprintLog } from '../../utils/types.js';
3
- export interface LogStreamingConfig {
4
- stackId: string;
5
- after?: string;
6
- auth: AuthParams;
7
- showBanner?: boolean;
8
- verbose?: boolean;
9
- log: Logger;
10
- onActivity?: () => void;
11
- onLogEntry?: (log: BlueprintLog) => void;
12
- }
13
- export interface StreamLogsOptions {
14
- stackId: string;
15
- after?: string;
16
- auth: AuthParams;
17
- onLog: (log: BlueprintLog) => void;
18
- onOpen: () => void;
19
- onError: (error: string) => void;
20
- logger: Logger;
21
- }
22
- export declare function streamLogs({ stackId, after, auth, onLog, onOpen, onError, logger, }: StreamLogsOptions): () => void;
23
- /** Check if a log is newer than a given timestamp */
24
- export declare function isNewerLog(log: BlueprintLog, timestamp: number): boolean;
25
- /**
26
- * Sets up log streaming for operations like deploy or destroy with spinner integration
27
- * @param config Configuration for log streaming
28
- * @returns A cleanup function for closing the log stream
29
- */
30
- export declare function setupLogStreaming(config: LogStreamingConfig): Promise<() => void>;
@@ -1,107 +0,0 @@
1
- import { EventSource } from 'eventsource';
2
- import { formatLogEntry } from '../../utils/display/logs-formatting.js';
3
- import getHeaders from '../../utils/get-headers.js';
4
- import { styleText } from '../../utils/style-text.js';
5
- import { createTracedFetch } from '../../utils/traced-fetch.js';
6
- import { logsUrl } from './logs.js';
7
- export function streamLogs({ stackId, after, auth, onLog, onOpen, onError, logger, }) {
8
- const fetchFn = createTracedFetch(logger);
9
- const url = new URL(`${logsUrl}/stream`);
10
- url.searchParams.append('stackId', stackId);
11
- if (after)
12
- url.searchParams.append('after', after);
13
- const headers = getHeaders(auth);
14
- const eventSource = new EventSource(url.toString(), {
15
- fetch: (input, init) => fetchFn(input, {
16
- ...init,
17
- headers: {
18
- ...init?.headers,
19
- ...headers,
20
- },
21
- }),
22
- });
23
- eventSource.onopen = onOpen;
24
- eventSource.onmessage = (event) => {
25
- try {
26
- const log = JSON.parse(event.data);
27
- onLog(log);
28
- }
29
- catch (err) {
30
- onError(`Failed to parse log data: ${err instanceof Error ? err.message : String(err)}`);
31
- }
32
- };
33
- eventSource.addEventListener('logs', (event) => {
34
- try {
35
- const logData = JSON.parse(event.data);
36
- // usually an array
37
- if (Array.isArray(logData)) {
38
- for (const log of logData)
39
- onLog(log);
40
- }
41
- else {
42
- onLog(logData);
43
- }
44
- }
45
- catch (err) {
46
- console.error('Error parsing logs event:', err);
47
- }
48
- });
49
- eventSource.onerror = () => {
50
- onError('Connection to log stream failed or was closed');
51
- if (eventSource.readyState === eventSource.CLOSED) {
52
- console.log('Connection is CLOSED');
53
- }
54
- else if (eventSource.readyState === eventSource.CONNECTING) {
55
- console.log('Connection is attempting to reconnect...');
56
- return; // Don't close if trying to reconnect
57
- }
58
- eventSource.close();
59
- };
60
- return () => {
61
- eventSource.close();
62
- };
63
- }
64
- /** Check if a log is newer than a given timestamp */
65
- export function isNewerLog(log, timestamp) {
66
- const logTimestamp = new Date(log.timestamp).getTime();
67
- return logTimestamp > timestamp;
68
- }
69
- /**
70
- * Sets up log streaming for operations like deploy or destroy with spinner integration
71
- * @param config Configuration for log streaming
72
- * @returns A cleanup function for closing the log stream
73
- */
74
- export async function setupLogStreaming(config) {
75
- const { stackId, auth, log, showBanner, verbose = false, after } = config;
76
- let newestTimestamp = Date.now();
77
- let previousLog;
78
- const onLogReceived = (logEntry) => {
79
- if (!isNewerLog(logEntry, newestTimestamp))
80
- return;
81
- newestTimestamp = new Date(logEntry.timestamp).getTime();
82
- config.onActivity?.();
83
- config.onLogEntry?.(logEntry);
84
- log(formatLogEntry(logEntry, verbose, previousLog));
85
- previousLog = logEntry;
86
- };
87
- let alreadyOpened = false;
88
- const onStreamOpen = () => {
89
- if (!alreadyOpened && showBanner)
90
- log(`Streaming logs... ${styleText('bold', 'ctrl+c')} to cancel`);
91
- if (alreadyOpened)
92
- log(`${styleText('green', 'Reconnected')}`);
93
- alreadyOpened = true;
94
- };
95
- const onStreamError = (error) => {
96
- log(`${styleText('red', 'Stream error:')} ${error}`);
97
- };
98
- return streamLogs({
99
- stackId,
100
- after,
101
- auth,
102
- onLog: onLogReceived,
103
- onOpen: onStreamOpen,
104
- onError: onStreamError,
105
- logger: log,
106
- });
107
- }