@sanity/runtime-cli 14.10.1 → 14.12.0

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.
@@ -0,0 +1,46 @@
1
+ import type { ScopeType } from '../../utils/types.js';
2
+ /**
3
+ * Source of a resolved config value.
4
+ * - flags: from CLI flags like `--project-id`
5
+ * - env: from the environment - usually CLI var like `SANITY_ORGANIZATION_ID`
6
+ * - module: from the blueprint module - undocumented escape hatch
7
+ * - config: from the config file - .sanity/blueprint.config.json most common
8
+ * - inferred: legacy `ST-<projectId>` stacks from launch
9
+ */
10
+ export type ConfigSource = 'flags' | 'env' | 'module' | 'config' | 'inferred';
11
+ /** A set of optional IDs from a single source. */
12
+ export interface IdValues {
13
+ organizationId?: string;
14
+ projectId?: string;
15
+ stackId?: string;
16
+ }
17
+ /**
18
+ * Sources to resolve IDs from, in descending priority.
19
+ * Each key maps to a source; the first non-empty value wins.
20
+ */
21
+ export interface IdSources {
22
+ flags?: IdValues;
23
+ env?: IdValues;
24
+ module?: IdValues;
25
+ config?: IdValues;
26
+ }
27
+ export interface ResolvedIds {
28
+ organizationId?: string;
29
+ projectId?: string;
30
+ stackId?: string;
31
+ scopeType?: ScopeType;
32
+ scopeId?: string;
33
+ sources: {
34
+ organizationId?: ConfigSource;
35
+ projectId?: ConfigSource;
36
+ stackId?: ConfigSource;
37
+ };
38
+ }
39
+ /**
40
+ * Resolve organization, project, and stack IDs from a prioritized set of sources.
41
+ * Precedence: flags > env > module > config.
42
+ * Derives scopeType/scopeId from the resolved IDs (project > organization).
43
+ *
44
+ * This is a pure, synchronous function -- no I/O, no side effects.
45
+ */
46
+ export declare function resolveIds(sources: IdSources): ResolvedIds;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Resolve organization, project, and stack IDs from a prioritized set of sources.
3
+ * Precedence: flags > env > module > config.
4
+ * Derives scopeType/scopeId from the resolved IDs (project > organization).
5
+ *
6
+ * This is a pure, synchronous function -- no I/O, no side effects.
7
+ */
8
+ export function resolveIds(sources) {
9
+ const ordered = [
10
+ { source: 'flags', values: sources.flags },
11
+ { source: 'env', values: sources.env },
12
+ { source: 'module', values: sources.module },
13
+ { source: 'config', values: sources.config },
14
+ ];
15
+ const result = { sources: {} };
16
+ for (const { source, values } of ordered) {
17
+ if (!values)
18
+ continue;
19
+ if (!result.organizationId && values.organizationId) {
20
+ result.organizationId = values.organizationId;
21
+ result.sources.organizationId = source;
22
+ }
23
+ if (!result.projectId && values.projectId) {
24
+ result.projectId = values.projectId;
25
+ result.sources.projectId = source;
26
+ }
27
+ if (!result.stackId && values.stackId) {
28
+ result.stackId = values.stackId;
29
+ result.sources.stackId = source;
30
+ }
31
+ }
32
+ // Scope is as specific as possible; project > organization
33
+ if (result.projectId) {
34
+ result.scopeType = 'project';
35
+ result.scopeId = result.projectId;
36
+ }
37
+ else if (result.organizationId) {
38
+ result.scopeType = 'organization';
39
+ result.scopeId = result.organizationId;
40
+ }
41
+ return result;
42
+ }
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { cwd } from 'node:process';
5
+ import { MAP_EVENT_TO_FUNCTION_TYPE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, } from '../../constants.js';
5
6
  import { styleText } from '../../utils/style-text.js';
6
7
  import { writeOrUpdateNodeDependency } from '../node.js';
7
8
  import { addResourceToBlueprint } from './blueprint.js';
@@ -21,6 +22,20 @@ export const handler = scheduledEventHandler(async ({ context }) => {
21
22
  const time = new Date().toLocaleTimeString()
22
23
  console.log(\`Your scheduled Sanity Function was called at \${time}\`)
23
24
  })`;
25
+ const DEFAULT_SYNC_TAG_HELPER_FUNCTION_TEMPLATE = /*ts*/ `import { syncTagInvalidateEventHandler } from '@sanity/functions'
26
+
27
+ export const handler = syncTagInvalidateEventHandler(async ({ context, event, done }) => {
28
+ const time = new Date().toLocaleTimeString()
29
+ console.log(\`Your sync tag invalidate Sanity Function was called at \${time}\`)
30
+ // TODO: add code to do something with the invalidated sync tags provided to you in \`event.data.syncTags\`
31
+ try {
32
+ // notify Sanity that you have completed invalidation
33
+ const response = await done(event.data.syncTags)
34
+ console.log('Invalidation complete, Sanity responded with an HTTP', response.status)
35
+ } catch (e) {
36
+ console.error('Error invoking Sanity invalidation done endpoint!', e)
37
+ }
38
+ })`;
24
39
  /**
25
40
  * Creates a new function resource file and adds it to the blueprint
26
41
  */
@@ -47,16 +62,18 @@ export async function createFunctionResource(options, logger) {
47
62
  throw Error(`Unsupported language: ${lang}`);
48
63
  // type looks like 'document-publish', 'media-library-asset-delete' or 'scheduled-function'
49
64
  // and we are guaranteed to have the same leading words (typeName below) for all provided type strings (via guards in the call site for this method).
50
- // TODO: this substring convention is brittle - consider mapping EVENT_* constants to typeName values explicitly instead of relying on string splitting.
51
- const typeName = type[0].substring(0, type[0].lastIndexOf('-'));
65
+ const functionType = MAP_EVENT_TO_FUNCTION_TYPE[type[0]];
52
66
  // Create index.<lang> with default template
53
67
  const indexPath = join(functionDir, `index.${lang}`);
54
68
  let template = DEFAULT_FUNCTION_TEMPLATE;
55
69
  if (addHelpers) {
56
- switch (typeName) {
57
- case 'scheduled':
70
+ switch (functionType) {
71
+ case SANITY_FUNCTION_SCHEDULED:
58
72
  template = DEFAULT_SCHEDULED_HELPER_FUNCTION_TEMPLATE;
59
73
  break;
74
+ case SANITY_FUNCTION_SYNC_TAG_INVALIDATE:
75
+ template = DEFAULT_SYNC_TAG_HELPER_FUNCTION_TEMPLATE;
76
+ break;
60
77
  default:
61
78
  template = DEFAULT_HELPER_FUNCTION_TEMPLATE;
62
79
  break;
@@ -80,39 +97,45 @@ export async function createFunctionResource(options, logger) {
80
97
  const eventsOn = type.map((t) => t.substring(t.lastIndexOf('-') + 1));
81
98
  // Create resource definition
82
99
  let resourceJson;
83
- switch (typeName) {
84
- case 'document':
100
+ switch (functionType) {
101
+ case SANITY_FUNCTION_DOCUMENT:
85
102
  resourceJson = {
86
103
  name,
87
104
  src: `functions/${name}`,
88
- type: 'sanity.function.document',
105
+ type: SANITY_FUNCTION_DOCUMENT,
89
106
  event: {
90
107
  on: eventsOn,
91
108
  },
92
109
  };
93
110
  break;
94
- case 'media-library-asset':
111
+ case SANITY_FUNCTION_MEDIA_LIBRARY_ASSET:
95
112
  resourceJson = {
96
113
  name,
97
114
  src: `functions/${name}`,
98
- type: 'sanity.function.media-library.asset',
115
+ type: SANITY_FUNCTION_MEDIA_LIBRARY_ASSET,
99
116
  event: {
100
117
  on: eventsOn,
101
118
  resource: { type: 'media-library', id: 'my-media-library-id' },
102
119
  },
103
120
  };
104
121
  break;
105
- case 'scheduled':
122
+ case SANITY_FUNCTION_SCHEDULED:
106
123
  resourceJson = {
107
124
  name,
108
125
  src: `functions/${name}`,
109
- type: 'sanity.function.cron',
126
+ type: SANITY_FUNCTION_SCHEDULED,
110
127
  event: {
111
128
  expression: '0 0 * * *',
112
129
  },
113
130
  };
114
131
  break;
115
- // TODO: add sync tag invalidate funx
132
+ case SANITY_FUNCTION_SYNC_TAG_INVALIDATE:
133
+ resourceJson = {
134
+ name,
135
+ src: `functions/${name}`,
136
+ type: SANITY_FUNCTION_SYNC_TAG_INVALIDATE,
137
+ };
138
+ break;
116
139
  }
117
140
  if (!resourceJson) {
118
141
  throw new Error('Could not create function resource based on selections');
@@ -1,6 +1,6 @@
1
1
  import type { Interfaces } from '@oclif/core';
2
2
  import { Command } from '@oclif/core';
3
- import type { ReadBlueprintResult } from './actions/blueprints/blueprint.js';
3
+ import { type ReadBlueprintResult } from './actions/blueprints/blueprint.js';
4
4
  import type { CoreResult } from './cores/index.js';
5
5
  import type { AuthParams, ScopeType, Stack } from './utils/types.js';
6
6
  export type Flags<T extends typeof Command> = Interfaces.InferredFlags<(typeof RuntimeCommand)['baseFlags'] & T['flags']>;
@@ -69,40 +69,70 @@ export declare abstract class RuntimeCommand<T extends typeof Command> extends C
69
69
  }): Promise<unknown>;
70
70
  }
71
71
  /**
72
- * Guarantees flags, args, sanityToken, and blueprint.
73
- * Blueprint parser errors are logged and the command exits with an error
72
+ * Context a command can declare it needs.
73
+ * The base class resolves each from the best available source during init().
74
+ * Dependencies are resolved implicitly -- declaring 'deployedStack' implies
75
+ * token, scope, and stackId without listing them.
76
+ *
77
+ * - token: Authenticated API token
78
+ * - blueprint: Parsed local blueprint file (filesystem)
79
+ * - scope: scopeType + scopeId (from flags, env, config file, or blueprint)
80
+ * - stackId: stackId (from flags, env, config file, blueprint, or inferred)
81
+ * - deployedStack: Remote Stack object fetched from the API (implies token, scope, stackId)
82
+ */
83
+ export type Need = 'token' | 'blueprint' | 'scope' | 'stackId' | 'deployedStack';
84
+ /**
85
+ * Base command that resolves context declaratively from a `static needs` array.
86
+ * Commands declare what they *directly use*; the base class resolves transitive
87
+ * dependencies automatically (e.g. 'deployedStack' implies token, scope, stackId).
88
+ *
89
+ * When a command needs only remote context (e.g. `['deployedStack']`),
90
+ * no local blueprint file is required -- flags and config are sufficient.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * class InfoCommand extends ResolvedCommand<typeof InfoCommand> {
95
+ * static needs = ['deployedStack'] as const
96
+ * // token, scope, stackId resolved automatically
97
+ * }
98
+ * ```
99
+ *
74
100
  * @extends RuntimeCommand
75
101
  */
76
- export declare abstract class LocalBlueprintCommand<T extends typeof Command> extends RuntimeCommand<T> {
77
- protected sanityToken: string;
78
- protected blueprint: ReadBlueprintResult;
102
+ export declare abstract class ResolvedCommand<T extends typeof Command> extends RuntimeCommand<T> {
103
+ static needs: readonly Need[];
79
104
  static baseFlags: {
105
+ stack: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
106
+ 'project-id': Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
107
+ 'organization-id': Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
80
108
  json: Interfaces.BooleanFlag<boolean>;
81
109
  path: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
82
110
  trace: Interfaces.BooleanFlag<boolean>;
83
111
  'validate-resources': Interfaces.BooleanFlag<boolean>;
84
112
  verbose: Interfaces.BooleanFlag<boolean>;
85
113
  };
114
+ protected token: string;
115
+ protected blueprint: ReadBlueprintResult;
116
+ protected scopeType: ScopeType;
117
+ protected scopeId: string;
118
+ protected stackId: string;
119
+ protected auth: AuthParams;
120
+ protected deployedStack: Stack;
86
121
  init(): Promise<void>;
87
122
  }
88
123
  /**
89
- * Guarantees flags, args, sanityToken, blueprint, scopeType, scopeId, stackId, auth, and deployedStack.
90
- * If scope or stack is missing, the command exits with an error
124
+ * Guarantees token and blueprint.
125
+ * @extends ResolvedCommand
126
+ */
127
+ export declare abstract class LocalBlueprintCommand<T extends typeof Command> extends ResolvedCommand<T> {
128
+ static needs: Need[];
129
+ /** @deprecated Use `this.token` instead. */
130
+ protected get sanityToken(): string;
131
+ }
132
+ /**
133
+ * Guarantees token, scope, stackId, deployedStack, and blueprint.
91
134
  * @extends LocalBlueprintCommand
92
135
  */
93
136
  export declare abstract class DeployedStackCommand<T extends typeof Command> extends LocalBlueprintCommand<T> {
94
- protected auth: AuthParams;
95
- protected deployedStack: Stack;
96
- protected scopeType: ScopeType;
97
- protected scopeId: string;
98
- protected stackId: string;
99
- static baseFlags: {
100
- stack: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
101
- json: Interfaces.BooleanFlag<boolean>;
102
- path: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
103
- trace: Interfaces.BooleanFlag<boolean>;
104
- 'validate-resources': Interfaces.BooleanFlag<boolean>;
105
- verbose: Interfaces.BooleanFlag<boolean>;
106
- };
107
- init(): Promise<void>;
137
+ static needs: Need[];
108
138
  }
@@ -1,7 +1,13 @@
1
1
  // * https://oclif.io/docs/base_class
2
+ import { env } from 'node:process';
2
3
  import { Command, CommandHelp, Flags as OclifFlags } from '@oclif/core';
3
- import { initBlueprintConfig, initDeployedBlueprintConfig } from './cores/index.js';
4
+ import { findBlueprintFile, loadBlueprintFile, parseBlueprintContent, } from './actions/blueprints/blueprint.js';
5
+ import { backfillProjectBasedStackId, readConfigFile } from './actions/blueprints/config.js';
6
+ import { resolveIds } from './actions/blueprints/resolve.js';
7
+ import { getStack, resolveStackIdByNameOrId } from './actions/blueprints/stacks.js';
8
+ import { presentBlueprintParserErrors } from './utils/display/errors.js';
4
9
  import { Logger } from './utils/logger.js';
10
+ import { validTokenOrErrorMessage } from './utils/validated-token.js';
5
11
  /**
6
12
  * Fallback error-to-hint patterns for RuntimeCommand.catch().
7
13
  * Each entry maps a regex to a function that returns suggestion strings.
@@ -155,69 +161,227 @@ export class RuntimeCommand extends Command {
155
161
  }
156
162
  }
157
163
  /**
158
- * Guarantees flags, args, sanityToken, and blueprint.
159
- * Blueprint parser errors are logged and the command exits with an error
164
+ * Base command that resolves context declaratively from a `static needs` array.
165
+ * Commands declare what they *directly use*; the base class resolves transitive
166
+ * dependencies automatically (e.g. 'deployedStack' implies token, scope, stackId).
167
+ *
168
+ * When a command needs only remote context (e.g. `['deployedStack']`),
169
+ * no local blueprint file is required -- flags and config are sufficient.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * class InfoCommand extends ResolvedCommand<typeof InfoCommand> {
174
+ * static needs = ['deployedStack'] as const
175
+ * // token, scope, stackId resolved automatically
176
+ * }
177
+ * ```
178
+ *
160
179
  * @extends RuntimeCommand
161
180
  */
162
- export class LocalBlueprintCommand extends RuntimeCommand {
163
- sanityToken;
181
+ export class ResolvedCommand extends RuntimeCommand {
182
+ static needs = [];
183
+ static baseFlags = {
184
+ ...baseFlags,
185
+ stack: stackFlag,
186
+ 'project-id': OclifFlags.string({ ...projectIdFlagConfig, hidden: true }),
187
+ 'organization-id': OclifFlags.string({ ...organizationIdFlagConfig, hidden: true }),
188
+ };
189
+ // Populated by init() according to `needs`
190
+ token;
164
191
  blueprint;
165
- static baseFlags = baseFlags;
192
+ scopeType;
193
+ scopeId;
194
+ stackId;
195
+ auth;
196
+ deployedStack;
166
197
  async init() {
167
- await super.init();
168
- const result = await initBlueprintConfig({
169
- bin: this.config.bin,
170
- log: Logger(this.log.bind(this), this.flags),
171
- validateResources: this.flags['validate-resources'],
172
- blueprintPath: this.flags.path,
198
+ await super.init(); // RuntimeCommand: parse flags + args
199
+ const needs = new Set(this.constructor.needs);
200
+ if (needs.size === 0)
201
+ return;
202
+ const log = Logger(this.log.bind(this), this.flags);
203
+ const needsToken = needs.has('token') || needs.has('scope') || needs.has('stackId') || needs.has('deployedStack');
204
+ const needsScope = needs.has('scope') || needs.has('deployedStack');
205
+ const needsStack = needs.has('stackId') || needs.has('deployedStack');
206
+ const needsBlueprint = needs.has('blueprint');
207
+ const needsDeployedStack = needs.has('deployedStack');
208
+ // 1. Token
209
+ if (needsToken) {
210
+ const check = await validTokenOrErrorMessage(log);
211
+ if (!check.ok) {
212
+ this.error(check.error.message, {
213
+ suggestions: ['Run `npx @sanity/cli login` to authenticate.'],
214
+ });
215
+ }
216
+ this.token = check.value;
217
+ }
218
+ // 2. Gather ID sources
219
+ const flagIds = {
220
+ projectId: this.flags['project-id'],
221
+ organizationId: this.flags['organization-id'],
222
+ };
223
+ const envIds = {
224
+ organizationId: env.SANITY_ORGANIZATION_ID,
225
+ projectId: env.SANITY_PROJECT_ID,
226
+ stackId: env.SANITY_BLUEPRINT_STACK_ID,
227
+ };
228
+ // Try to read the config file (cheap single-file JSON read).
229
+ // Anchor to --path flag or the blueprint file location if found, else cwd.
230
+ const blueprintFileInfo = findBlueprintFile(this.flags.path);
231
+ const blueprintConfig = readConfigFile(blueprintFileInfo?.blueprintFilePath);
232
+ const configIds = blueprintConfig
233
+ ? {
234
+ organizationId: blueprintConfig.organizationId,
235
+ projectId: blueprintConfig.projectId,
236
+ stackId: blueprintConfig.stackId,
237
+ }
238
+ : undefined;
239
+ // 3. Blueprint (if needed)
240
+ let moduleIds;
241
+ if (needsBlueprint) {
242
+ if (!blueprintFileInfo) {
243
+ this.error('Could not find Blueprint file! Use the `blueprints init` command.', {
244
+ suggestions: [
245
+ `Run \`${this.config.bin} blueprints init\` to create one.`,
246
+ `Run \`${this.config.bin} blueprints doctor\` to check your configuration.`,
247
+ ],
248
+ });
249
+ }
250
+ const loaded = await loadBlueprintFile(blueprintFileInfo);
251
+ const parsed = parseBlueprintContent(loaded.rawBlueprint, {
252
+ validateResources: this.flags['validate-resources'],
253
+ });
254
+ if (parsed.errors.length > 0) {
255
+ log(presentBlueprintParserErrors(parsed.errors));
256
+ this.error('Blueprint file contains errors.', {
257
+ suggestions: [
258
+ 'Fix the Blueprint errors listed above.',
259
+ `Run \`${this.config.bin} blueprints doctor\` to check your configuration.`,
260
+ ],
261
+ });
262
+ }
263
+ if (loaded.module) {
264
+ moduleIds = {
265
+ organizationId: loaded.module.organizationId,
266
+ projectId: loaded.module.projectId,
267
+ stackId: loaded.module.stackId,
268
+ };
269
+ }
270
+ // Assemble the full ReadBlueprintResult -- will be completed with resolved IDs below
271
+ this.blueprint = {
272
+ fileInfo: blueprintFileInfo,
273
+ blueprintConfig,
274
+ rawBlueprint: loaded.rawBlueprint,
275
+ ...parsed,
276
+ };
277
+ }
278
+ // 4. Resolve IDs
279
+ const resolved = resolveIds({
280
+ flags: flagIds,
281
+ env: envIds,
282
+ module: moduleIds,
283
+ config: configIds,
173
284
  });
174
- if (!result.ok) {
175
- const suggestions = [];
176
- if (/token|login/i.test(result.error)) {
177
- suggestions.push('Run `npx @sanity/cli login` to authenticate.');
285
+ // Legacy stack ID inference (only when we have a blueprint file path for the write side-effect)
286
+ if (!resolved.stackId && resolved.projectId && blueprintFileInfo) {
287
+ try {
288
+ const inferred = await backfillProjectBasedStackId({
289
+ blueprintFilePath: blueprintFileInfo.blueprintFilePath,
290
+ projectId: resolved.projectId,
291
+ logger: log,
292
+ });
293
+ if (inferred) {
294
+ resolved.stackId = inferred;
295
+ resolved.sources.stackId = 'inferred';
296
+ }
297
+ }
298
+ catch {
299
+ // assumption was wrong; leave stackId undefined
178
300
  }
179
- else if (/blueprint/i.test(result.error) && /error/i.test(result.error)) {
180
- suggestions.push('Fix the Blueprint errors listed above.');
301
+ }
302
+ // Backfill resolved IDs onto the blueprint result if it was loaded
303
+ if (this.blueprint) {
304
+ Object.assign(this.blueprint, {
305
+ organizationId: resolved.organizationId,
306
+ projectId: resolved.projectId,
307
+ stackId: resolved.stackId,
308
+ scopeType: resolved.scopeType,
309
+ scopeId: resolved.scopeId,
310
+ sources: resolved.sources,
311
+ });
312
+ }
313
+ // 5. Scope
314
+ if (needsScope) {
315
+ if (!resolved.scopeType || !resolved.scopeId) {
316
+ this.error('Missing scope: provide --project-id or --organization-id, or configure a Blueprint.', {
317
+ suggestions: [
318
+ `Run \`${this.config.bin} blueprints doctor\` to check your configuration.`,
319
+ ],
320
+ });
181
321
  }
182
- suggestions.push(`Run \`${this.config.bin} blueprints doctor\` to check your configuration.`);
183
- this.error(result.error, { suggestions });
322
+ this.scopeType = resolved.scopeType;
323
+ this.scopeId = resolved.scopeId;
324
+ }
325
+ // 6. Stack
326
+ if (needsStack) {
327
+ let { stackId } = resolved;
328
+ const stackOverride = this.flags.stack;
329
+ if (stackOverride) {
330
+ const auth = {
331
+ token: this.token,
332
+ scopeType: this.scopeType,
333
+ scopeId: this.scopeId,
334
+ };
335
+ stackId = await resolveStackIdByNameOrId(stackOverride, auth, log);
336
+ }
337
+ if (!stackId) {
338
+ this.error('Missing Stack: provide --stack, or configure a Blueprint with a Stack.', {
339
+ suggestions: [
340
+ `Run \`${this.config.bin} blueprints doctor\` to check your configuration.`,
341
+ ],
342
+ });
343
+ }
344
+ this.stackId = stackId;
345
+ }
346
+ // 7. Auth
347
+ if (needsScope) {
348
+ this.auth = { token: this.token, scopeType: this.scopeType, scopeId: this.scopeId };
349
+ }
350
+ // 8. Deployed Stack
351
+ if (needsDeployedStack) {
352
+ const spinner = log.ora('Loading Stack deployment...').start();
353
+ const response = await getStack({ stackId: this.stackId, auth: this.auth, logger: log });
354
+ if (!response.ok) {
355
+ spinner.fail('Could not load Stack deployment');
356
+ this.error('Missing Stack deployment', {
357
+ suggestions: [
358
+ `Run \`${this.config.bin} blueprints doctor\` to check your configuration.`,
359
+ ],
360
+ });
361
+ }
362
+ spinner.stop().clear();
363
+ this.deployedStack = response.stack;
184
364
  }
185
- this.sanityToken = result.value.token;
186
- this.blueprint = result.value.blueprint;
187
365
  }
188
366
  }
367
+ // ---------------------------------------------------------------------------
368
+ // Legacy base classes -- thin wrappers around ResolvedCommand
369
+ // ---------------------------------------------------------------------------
189
370
  /**
190
- * Guarantees flags, args, sanityToken, blueprint, scopeType, scopeId, stackId, auth, and deployedStack.
191
- * If scope or stack is missing, the command exits with an error
371
+ * Guarantees token and blueprint.
372
+ * @extends ResolvedCommand
373
+ */
374
+ export class LocalBlueprintCommand extends ResolvedCommand {
375
+ static needs = ['token', 'blueprint'];
376
+ /** @deprecated Use `this.token` instead. */
377
+ get sanityToken() {
378
+ return this.token;
379
+ }
380
+ }
381
+ /**
382
+ * Guarantees token, scope, stackId, deployedStack, and blueprint.
192
383
  * @extends LocalBlueprintCommand
193
384
  */
194
385
  export class DeployedStackCommand extends LocalBlueprintCommand {
195
- auth;
196
- deployedStack;
197
- scopeType;
198
- scopeId;
199
- stackId;
200
- static baseFlags = { ...baseFlags, stack: stackFlag };
201
- async init() {
202
- await super.init();
203
- const result = await initDeployedBlueprintConfig({
204
- bin: this.config.bin,
205
- blueprint: this.blueprint,
206
- log: Logger(this.log.bind(this), this.flags),
207
- token: this.sanityToken,
208
- validateToken: false,
209
- validateResources: this.flags['validate-resources'],
210
- stackOverride: this.flags.stack,
211
- });
212
- if (!result.ok) {
213
- this.error(result.error, {
214
- suggestions: [`Run \`${this.config.bin} blueprints doctor\` to check your configuration.`],
215
- });
216
- }
217
- this.scopeType = result.value.scopeType;
218
- this.scopeId = result.value.scopeId;
219
- this.stackId = result.value.stackId;
220
- this.auth = result.value.auth;
221
- this.deployedStack = result.value.deployedStack;
222
- }
386
+ static needs = ['deployedStack', 'blueprint'];
223
387
  }
@@ -1,10 +1,12 @@
1
- import { DeployedStackCommand } from '../../baseCommands.js';
2
- export default class InfoCommand extends DeployedStackCommand<typeof InfoCommand> {
1
+ import { ResolvedCommand } from '../../baseCommands.js';
2
+ export default class InfoCommand extends ResolvedCommand<typeof InfoCommand> {
3
+ static needs: readonly ["deployedStack"];
3
4
  static summary: string;
4
5
  static description: string;
5
6
  static examples: string[];
6
7
  static flags: {
7
8
  stack: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
10
  };
9
11
  run(): Promise<Record<string, unknown> | undefined>;
10
12
  }
@@ -1,8 +1,9 @@
1
1
  import { Flags } from '@oclif/core';
2
- import { DeployedStackCommand } from '../../baseCommands.js';
2
+ import { projectIdFlagConfig, ResolvedCommand } from '../../baseCommands.js';
3
3
  import { blueprintInfoCore } from '../../cores/blueprints/info.js';
4
4
  import { Logger } from '../../utils/logger.js';
5
- export default class InfoCommand extends DeployedStackCommand {
5
+ export default class InfoCommand extends ResolvedCommand {
6
+ static needs = ['deployedStack'];
6
7
  static summary = 'Display the status and resources of the remote Stack deployment';
7
8
  static description = `Displays the current state and metadata of your remote Stack deployment, including deployed resources, status, and configuration.
8
9
 
@@ -12,12 +13,14 @@ Run 'blueprints stacks' to see all available Stacks in your project or organizat
12
13
  static examples = [
13
14
  '<%= config.bin %> <%= command.id %>',
14
15
  '<%= config.bin %> <%= command.id %> --stack <name-or-id>',
16
+ '<%= config.bin %> <%= command.id %> --project-id <id> --stack <name-or-id>',
15
17
  ];
16
18
  static flags = {
17
19
  stack: Flags.string({
18
- description: 'Stack name or ID to use instead of the locally configured Stack',
20
+ description: 'Stack name or ID',
19
21
  aliases: ['id'],
20
22
  }),
23
+ 'project-id': Flags.string({ ...projectIdFlagConfig }),
21
24
  };
22
25
  async run() {
23
26
  const result = await blueprintInfoCore({
@@ -8,7 +8,7 @@ export default class AddCommand extends LocalBlueprintCommand {
8
8
  static summary = 'Add a Function to your Blueprint';
9
9
  static description = `Scaffolds a new Function in the functions/ folder and templates a resource for your Blueprint manifest.
10
10
 
11
- Functions are serverless handlers triggered by document events (create, update, delete, publish) or media library events.
11
+ Functions are serverless handlers triggered by document, live content or media-library events (create, update, delete, publish).
12
12
 
13
13
  After adding, use 'functions dev' to test locally, then 'blueprints deploy' to publish.`;
14
14
  static examples = [