@sanity/runtime-cli 17.5.0 → 17.6.0-blueprinit.g77b7a64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +25 -25
  2. package/dist/actions/blueprinit/manifest.d.ts +15 -0
  3. package/dist/actions/blueprinit/manifest.js +82 -0
  4. package/dist/actions/blueprinit/studio.d.ts +6 -0
  5. package/dist/actions/blueprinit/studio.js +43 -0
  6. package/dist/actions/blueprinit/templates.d.ts +34 -0
  7. package/dist/actions/blueprinit/templates.js +83 -0
  8. package/dist/actions/blueprinit/workspace.d.ts +16 -0
  9. package/dist/actions/blueprinit/workspace.js +143 -0
  10. package/dist/actions/blueprints/blueprint.js +2 -1
  11. package/dist/actions/blueprints/resources.js +17 -1
  12. package/dist/actions/sanity/datasets.d.ts +15 -0
  13. package/dist/actions/sanity/datasets.js +26 -0
  14. package/dist/commands/init.d.ts +17 -0
  15. package/dist/commands/init.js +59 -0
  16. package/dist/constants.d.ts +5 -0
  17. package/dist/constants.js +6 -0
  18. package/dist/cores/blueprinit/adopt.d.ts +6 -0
  19. package/dist/cores/blueprinit/adopt.js +119 -0
  20. package/dist/cores/blueprinit/finish.d.ts +14 -0
  21. package/dist/cores/blueprinit/finish.js +64 -0
  22. package/dist/cores/blueprinit/fresh.d.ts +3 -0
  23. package/dist/cores/blueprinit/fresh.js +120 -0
  24. package/dist/cores/blueprinit/preview.d.ts +15 -0
  25. package/dist/cores/blueprinit/preview.js +14 -0
  26. package/dist/cores/blueprinit/prompts.d.ts +5 -0
  27. package/dist/cores/blueprinit/prompts.js +22 -0
  28. package/dist/cores/blueprinit/selection.d.ts +15 -0
  29. package/dist/cores/blueprinit/selection.js +90 -0
  30. package/dist/cores/blueprinit/types.d.ts +16 -0
  31. package/dist/cores/blueprinit/types.js +1 -0
  32. package/dist/cores/blueprinit.d.ts +7 -0
  33. package/dist/cores/blueprinit.js +48 -0
  34. package/dist/cores/blueprints/deploy.js +2 -0
  35. package/dist/cores/blueprints/plan.js +2 -0
  36. package/dist/cores/functions/add.js +10 -7
  37. package/dist/server/static/components/api-base.js +1 -0
  38. package/dist/server/static/components/filters.js +4 -2
  39. package/dist/server/static/components/payload-panel.js +3 -0
  40. package/dist/utils/blueprints/extract-durable-config.d.ts +8 -0
  41. package/dist/utils/blueprints/extract-durable-config.js +60 -0
  42. package/dist/utils/blueprints/validate-resources.js +5 -2
  43. package/dist/utils/display/blueprints-formatting.js +5 -1
  44. package/dist/utils/display/resources-formatting.js +2 -2
  45. package/dist/utils/types.d.ts +8 -2
  46. package/dist/utils/types.js +8 -2
  47. package/oclif.manifest.json +132 -1
  48. package/package.json +2 -2
  49. package/skills/init.md +155 -0
@@ -2,7 +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
+ import { MAP_EVENT_TO_FUNCTION_TYPE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_PUBSUB, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, } from '../../constants.js';
6
6
  import { styleText } from '../../utils/style-text.js';
7
7
  import { writeOrUpdateNodeDependency } from '../node.js';
8
8
  import { addResourceToBlueprint } from './blueprint.js';
@@ -36,6 +36,12 @@ export const handler = syncTagInvalidateEventHandler(async ({ context, event, do
36
36
  console.error('Error invoking Sanity invalidation done endpoint!', e)
37
37
  }
38
38
  })`;
39
+ const DEFAULT_PUBSUB_HELPER_FUNCTION_TEMPLATE = /*ts*/ `import { pubSubEventHandler } from '@sanity/functions'
40
+
41
+ export const handler = pubSubEventHandler(async ({ context, event }) => {
42
+ const time = new Date().toLocaleTimeString()
43
+ console.log(\`Your pubsub Sanity Function was called at \${time}\`)
44
+ })`;
39
45
  /**
40
46
  * Creates a new function resource file and adds it to the blueprint
41
47
  */
@@ -68,6 +74,9 @@ export async function createFunctionResource(options, logger) {
68
74
  let template = DEFAULT_FUNCTION_TEMPLATE;
69
75
  if (addHelpers) {
70
76
  switch (functionType) {
77
+ case SANITY_FUNCTION_PUBSUB:
78
+ template = DEFAULT_PUBSUB_HELPER_FUNCTION_TEMPLATE;
79
+ break;
71
80
  case SANITY_FUNCTION_SCHEDULED:
72
81
  template = DEFAULT_SCHEDULED_HELPER_FUNCTION_TEMPLATE;
73
82
  break;
@@ -119,6 +128,13 @@ export async function createFunctionResource(options, logger) {
119
128
  },
120
129
  };
121
130
  break;
131
+ case SANITY_FUNCTION_PUBSUB:
132
+ resourceJson = {
133
+ name,
134
+ src: `functions/${name}`,
135
+ type: SANITY_FUNCTION_PUBSUB,
136
+ };
137
+ break;
122
138
  case SANITY_FUNCTION_SCHEDULED:
123
139
  resourceJson = {
124
140
  name,
@@ -0,0 +1,15 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { ActionResponse } from '../../utils/types.js';
3
+ export interface Dataset {
4
+ name: string;
5
+ aclMode: string;
6
+ }
7
+ interface ListDatasetsResponse extends ActionResponse {
8
+ datasets: Dataset[];
9
+ }
10
+ export declare function listDatasets({ token, projectId, logger, }: {
11
+ token: string;
12
+ projectId: string;
13
+ logger: Logger;
14
+ }): Promise<ListDatasetsResponse>;
15
+ export {};
@@ -0,0 +1,26 @@
1
+ import { createTracedFetch } from '../../utils/traced-fetch.js';
2
+ import { projectsApiPath } from './projects.js';
3
+ export async function listDatasets({ token, projectId, logger, }) {
4
+ const fetchFn = createTracedFetch(logger);
5
+ const url = `${projectsApiPath}/${encodeURIComponent(projectId)}/datasets`;
6
+ const response = await fetchFn(url, {
7
+ method: 'GET',
8
+ headers: {
9
+ Authorization: `Bearer ${token}`,
10
+ 'Content-Type': 'application/json',
11
+ },
12
+ });
13
+ const body = await response.json();
14
+ if (!response.ok) {
15
+ return {
16
+ ok: false,
17
+ error: body.error?.message ?? response.statusText,
18
+ datasets: [],
19
+ };
20
+ }
21
+ return {
22
+ ok: true,
23
+ error: null,
24
+ datasets: body,
25
+ };
26
+ }
@@ -0,0 +1,17 @@
1
+ import { RuntimeCommand } from '../baseCommands.js';
2
+ export default class InitCommand extends RuntimeCommand<typeof InitCommand> {
3
+ static hidden: boolean;
4
+ static summary: string;
5
+ static description: string;
6
+ static examples: string[];
7
+ static flags: {
8
+ llms: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ 'organization-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ dataset: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'stack-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'studio-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<Record<string, unknown> | undefined>;
17
+ }
@@ -0,0 +1,59 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { llmsFlagConfig } from '@sanity-labs/oclif-plugin-skills-flag';
3
+ import { RuntimeCommand } from '../baseCommands.js';
4
+ import { blueprinitCore } from '../cores/blueprinit.js';
5
+ import { Logger } from '../utils/logger.js';
6
+ export default class InitCommand extends RuntimeCommand {
7
+ static hidden = true;
8
+ static summary = 'Experimental blueprints-first project init';
9
+ static description = `Scaffolds a Blueprint, Studio, and starter function around an existing Sanity project.
10
+
11
+ This hidden command is an experiment; its interface and generated files are not stable.`;
12
+ static examples = [
13
+ '<%= config.bin %> <%= command.id %>',
14
+ '<%= config.bin %> <%= command.id %> --yes',
15
+ '<%= config.bin %> <%= command.id %> --project-id <id> --dataset <name> --yes',
16
+ ];
17
+ static flags = {
18
+ llms: Flags.boolean(llmsFlagConfig),
19
+ yes: Flags.boolean({
20
+ char: 'y',
21
+ description: 'Skip confirmation prompts',
22
+ default: false,
23
+ }),
24
+ 'organization-id': Flags.string({
25
+ description: 'Organization to use when the account has several',
26
+ aliases: ['organization', 'organizationId', 'org'],
27
+ }),
28
+ 'project-id': Flags.string({
29
+ description: 'Project to use when the organization has several',
30
+ aliases: ['project', 'projectId'],
31
+ }),
32
+ dataset: Flags.string({
33
+ description: 'Dataset to attach',
34
+ }),
35
+ 'stack-name': Flags.string({
36
+ description: 'Name for the new organization-scoped Stack',
37
+ }),
38
+ 'studio-name': Flags.string({
39
+ description: 'Name and hosted slug for the Studio resource',
40
+ }),
41
+ };
42
+ async run() {
43
+ const result = await blueprinitCore({
44
+ bin: this.config.bin,
45
+ log: Logger(this.log.bind(this), this.flags),
46
+ flags: {
47
+ yes: this.flags.yes,
48
+ organizationId: this.flags['organization-id'],
49
+ projectId: this.flags['project-id'],
50
+ datasetName: this.flags.dataset,
51
+ stackName: this.flags['stack-name'],
52
+ studioName: this.flags['studio-name'],
53
+ },
54
+ });
55
+ if (!result.success)
56
+ this.coreError(result);
57
+ return result.json;
58
+ }
59
+ }
@@ -3,8 +3,10 @@ export declare const SANITY_ACCESS_ROBOT = "sanity.access.robot";
3
3
  export declare const SANITY_FUNCTION_PREFIX = "sanity.function.";
4
4
  export declare const SANITY_FUNCTION_DOCUMENT = "sanity.function.document";
5
5
  export declare const SANITY_FUNCTION_MEDIA_LIBRARY_ASSET = "sanity.function.media-library.asset";
6
+ export declare const SANITY_FUNCTION_PUBSUB = "sanity.function.pubsub";
6
7
  export declare const SANITY_FUNCTION_SCHEDULED = "sanity.function.cron";
7
8
  export declare const SANITY_FUNCTION_SYNC_TAG_INVALIDATE = "sanity.function.sync-tag-invalidate";
9
+ export declare const SANITY_FUNCTION_DURABLE = "sanity.function.durable";
8
10
  export declare const SANITY_PROJECT = "sanity.project";
9
11
  export declare const SANITY_PROJECT_CORS = "sanity.project.cors";
10
12
  export declare const SANITY_PROJECT_DATASET = "sanity.project.dataset";
@@ -17,6 +19,7 @@ export declare const EVENT_DOCUMENT_DELETE = "document-delete";
17
19
  export declare const EVENT_MEDIA_LIBRARY_ASSET_CREATE = "media-library-asset-create";
18
20
  export declare const EVENT_MEDIA_LIBRARY_ASSET_UPDATE = "media-library-asset-update";
19
21
  export declare const EVENT_MEDIA_LIBRARY_ASSET_DELETE = "media-library-asset-delete";
22
+ export declare const EVENT_PUBSUB = "pub-sub";
20
23
  export declare const EVENT_SCHEDULED = "scheduled-function";
21
24
  export declare const EVENT_SYNC_TAG_INVALIDATE = "sync-tag-invalidate";
22
25
  export declare const MAP_EVENT_TO_FUNCTION_TYPE: {
@@ -27,6 +30,7 @@ export declare const MAP_EVENT_TO_FUNCTION_TYPE: {
27
30
  readonly "media-library-asset-create": "sanity.function.media-library.asset";
28
31
  readonly "media-library-asset-delete": "sanity.function.media-library.asset";
29
32
  readonly "media-library-asset-update": "sanity.function.media-library.asset";
33
+ readonly "pub-sub": "sanity.function.pubsub";
30
34
  readonly "scheduled-function": "sanity.function.cron";
31
35
  readonly "sync-tag-invalidate": "sanity.function.sync-tag-invalidate";
32
36
  };
@@ -42,6 +46,7 @@ export declare const LABEL_DOCUMENT_DELETE = "Document Delete";
42
46
  export declare const LABEL_MEDIA_LIBRARY_ASSET_CREATE = "Media Library Asset Create";
43
47
  export declare const LABEL_MEDIA_LIBRARY_ASSET_UPDATE = "Media Library Asset Update";
44
48
  export declare const LABEL_MEDIA_LIBRARY_ASSET_DELETE = "Media Library Asset Delete";
49
+ export declare const LABEL_PUBSUB = "PubSub";
45
50
  export declare const LABEL_SCHEDULED = "Scheduled";
46
51
  export declare const LABEL_SYNC_TAG_INVALIDATE = "Sync Tag Invalidate";
47
52
  export declare const CI_SETUP_DOCS_URL = "https://www.sanity.io/docs/blueprints/deploy-blueprints-from-ci";
package/dist/constants.js CHANGED
@@ -3,8 +3,10 @@ export const SANITY_ACCESS_ROBOT = 'sanity.access.robot';
3
3
  export const SANITY_FUNCTION_PREFIX = 'sanity.function.';
4
4
  export const SANITY_FUNCTION_DOCUMENT = 'sanity.function.document';
5
5
  export const SANITY_FUNCTION_MEDIA_LIBRARY_ASSET = 'sanity.function.media-library.asset';
6
+ export const SANITY_FUNCTION_PUBSUB = 'sanity.function.pubsub';
6
7
  export const SANITY_FUNCTION_SCHEDULED = 'sanity.function.cron';
7
8
  export const SANITY_FUNCTION_SYNC_TAG_INVALIDATE = 'sanity.function.sync-tag-invalidate';
9
+ export const SANITY_FUNCTION_DURABLE = 'sanity.function.durable';
8
10
  export const SANITY_PROJECT = 'sanity.project';
9
11
  export const SANITY_PROJECT_CORS = 'sanity.project.cors';
10
12
  export const SANITY_PROJECT_DATASET = 'sanity.project.dataset';
@@ -17,6 +19,7 @@ export const EVENT_DOCUMENT_DELETE = 'document-delete';
17
19
  export const EVENT_MEDIA_LIBRARY_ASSET_CREATE = 'media-library-asset-create';
18
20
  export const EVENT_MEDIA_LIBRARY_ASSET_UPDATE = 'media-library-asset-update';
19
21
  export const EVENT_MEDIA_LIBRARY_ASSET_DELETE = 'media-library-asset-delete';
22
+ export const EVENT_PUBSUB = 'pub-sub';
20
23
  export const EVENT_SCHEDULED = 'scheduled-function';
21
24
  export const EVENT_SYNC_TAG_INVALIDATE = 'sync-tag-invalidate';
22
25
  export const MAP_EVENT_TO_FUNCTION_TYPE = {
@@ -27,6 +30,7 @@ export const MAP_EVENT_TO_FUNCTION_TYPE = {
27
30
  [EVENT_MEDIA_LIBRARY_ASSET_CREATE]: SANITY_FUNCTION_MEDIA_LIBRARY_ASSET,
28
31
  [EVENT_MEDIA_LIBRARY_ASSET_DELETE]: SANITY_FUNCTION_MEDIA_LIBRARY_ASSET,
29
32
  [EVENT_MEDIA_LIBRARY_ASSET_UPDATE]: SANITY_FUNCTION_MEDIA_LIBRARY_ASSET,
33
+ [EVENT_PUBSUB]: SANITY_FUNCTION_PUBSUB,
30
34
  [EVENT_SCHEDULED]: SANITY_FUNCTION_SCHEDULED,
31
35
  [EVENT_SYNC_TAG_INVALIDATE]: SANITY_FUNCTION_SYNC_TAG_INVALIDATE,
32
36
  };
@@ -35,6 +39,7 @@ export const PROJECT_SCOPED_FUNCTION_TYPES = new Set([
35
39
  SANITY_FUNCTION_DOCUMENT,
36
40
  SANITY_FUNCTION_MEDIA_LIBRARY_ASSET,
37
41
  SANITY_FUNCTION_SYNC_TAG_INVALIDATE,
42
+ SANITY_FUNCTION_DURABLE,
38
43
  ]);
39
44
  export const ORGANIZATION_SCOPED_FUNCTION_TYPES = new Set([
40
45
  SANITY_FUNCTION_SCHEDULED,
@@ -47,6 +52,7 @@ export const LABEL_DOCUMENT_DELETE = 'Document Delete';
47
52
  export const LABEL_MEDIA_LIBRARY_ASSET_CREATE = 'Media Library Asset Create';
48
53
  export const LABEL_MEDIA_LIBRARY_ASSET_UPDATE = 'Media Library Asset Update';
49
54
  export const LABEL_MEDIA_LIBRARY_ASSET_DELETE = 'Media Library Asset Delete';
55
+ export const LABEL_PUBSUB = 'PubSub';
50
56
  export const LABEL_SCHEDULED = 'Scheduled';
51
57
  export const LABEL_SYNC_TAG_INVALIDATE = 'Sync Tag Invalidate';
52
58
  export const CI_SETUP_DOCS_URL = 'https://www.sanity.io/docs/blueprints/deploy-blueprints-from-ci';
@@ -0,0 +1,6 @@
1
+ import type { DetectedStudio } from '../../actions/blueprinit/studio.js';
2
+ import type { CoreResult } from '../index.js';
3
+ import type { BlueprinitFlowOptions } from './types.js';
4
+ export declare function adoptFlow(options: BlueprinitFlowOptions & {
5
+ studio: DetectedStudio;
6
+ }): Promise<CoreResult>;
@@ -0,0 +1,119 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { env } from 'node:process';
4
+ import { hostedStudioOrigin, slugify } from '../../actions/blueprinit/manifest.js';
5
+ import { adoptWorkspace } from '../../actions/blueprinit/workspace.js';
6
+ import { listDatasets } from '../../actions/sanity/datasets.js';
7
+ import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
8
+ import { check } from '../../utils/display/presenters.js';
9
+ import { getLatestNpmVersion } from '../../utils/external/npmjs.js';
10
+ import { isInteractive } from '../../utils/interactive.js';
11
+ import { createStackAndFinish } from './finish.js';
12
+ import { printBlueprintPreview } from './preview.js';
13
+ import { confirmUnlessSkipped, promptForStudioName, resourceNamesCollide } from './prompts.js';
14
+ import { findProject } from './selection.js';
15
+ export async function adoptFlow(options) {
16
+ const { bin, dir, flags, log, studio, token } = options;
17
+ if (flags.projectId && studio.projectId && flags.projectId !== studio.projectId) {
18
+ return { success: false, error: `The Studio uses project "${studio.projectId}".` };
19
+ }
20
+ if (flags.datasetName && studio.dataset && flags.datasetName !== studio.dataset) {
21
+ return { success: false, error: `The Studio uses dataset "${studio.dataset}".` };
22
+ }
23
+ const projectId = studio.projectId ?? flags.projectId;
24
+ const datasetName = studio.dataset ?? flags.datasetName;
25
+ if (!projectId || !datasetName) {
26
+ return {
27
+ success: false,
28
+ error: `Found ${studio.configFile}, but could not read a projectId and dataset from it.`,
29
+ suggestions: ['Use literal values in the Studio config or pass --project-id and --dataset.'],
30
+ };
31
+ }
32
+ log(check(`Studio detected (${studio.configFile})`));
33
+ const grouped = await groupProjectsByOrganization({ token, logger: log });
34
+ if (!grouped.ok) {
35
+ return { success: false, error: `Could not list organizations: ${grouped.error}` };
36
+ }
37
+ const match = findProject(grouped.organizations, projectId);
38
+ if (!match) {
39
+ return {
40
+ success: false,
41
+ error: `Project "${projectId}" from ${studio.configFile} was not found in your account.`,
42
+ };
43
+ }
44
+ if (flags.organizationId && flags.organizationId !== match.organization.id) {
45
+ return {
46
+ success: false,
47
+ error: `Project "${studio.projectId}" belongs to organization "${match.organization.id}".`,
48
+ };
49
+ }
50
+ const datasets = await listDatasets({ token, projectId: match.project.id, logger: log });
51
+ if (!datasets.ok) {
52
+ return { success: false, error: `Could not list datasets: ${datasets.error}` };
53
+ }
54
+ if (!datasets.datasets.some(({ name }) => name === datasetName)) {
55
+ return {
56
+ success: false,
57
+ error: `Dataset "${datasetName}" was not found in project ${match.project.displayName}.`,
58
+ };
59
+ }
60
+ log(check(`Matches project ${match.project.displayName} (${match.project.id}), dataset ${datasetName}`));
61
+ log('');
62
+ const studioName = await promptForStudioName(flags, isInteractive);
63
+ const stackName = flags.stackName
64
+ ? slugify(flags.stackName) || match.project.id
65
+ : slugify(match.project.displayName) || match.project.id;
66
+ if (resourceNamesCollide(stackName, studioName)) {
67
+ return {
68
+ success: false,
69
+ error: `Stack and Studio names both resolve to "${stackName}". Blueprint resource names must be unique.`,
70
+ suggestions: ['Pass --stack-name or --studio-name with a different value.'],
71
+ };
72
+ }
73
+ const studioOrigin = hostedStudioOrigin(studioName, env.SANITY_INTERNAL_ENV);
74
+ const marker = (file) => (existsSync(join(dir, file)) ? '~' : '+');
75
+ printBlueprintPreview({
76
+ log,
77
+ organizationName: match.organization.name,
78
+ projectId: match.project.id,
79
+ projectName: match.project.displayName,
80
+ datasetName,
81
+ studioName,
82
+ studioOrigin,
83
+ studioSource: '.',
84
+ stackName,
85
+ });
86
+ log('Changes:');
87
+ log(" + sanity.blueprint.ts attach project + dataset; add CORS and Studio at '.'");
88
+ log(` + .sanity/blueprint.config.json org scope, stack ${stackName}`);
89
+ log(` ${marker('.gitignore')} .gitignore`);
90
+ log(` ${marker('package.json')} package.json`);
91
+ log('');
92
+ const confirmed = await confirmUnlessSkipped(flags, isInteractive, 'Adopt this Studio into the Blueprint?');
93
+ if (!confirmed)
94
+ return { success: false, error: 'Initialization cancelled.' };
95
+ const adoption = adoptWorkspace({
96
+ dir,
97
+ projectId: match.project.id,
98
+ displayName: match.project.displayName,
99
+ datasetName,
100
+ stackName,
101
+ studioName,
102
+ studioOrigin,
103
+ blueprintsVersion: await getLatestNpmVersion('@sanity/blueprints', log),
104
+ });
105
+ return await createStackAndFinish({
106
+ ...options,
107
+ organizationId: match.organization.id,
108
+ projectId: match.project.id,
109
+ datasetName,
110
+ stackName,
111
+ blueprintFilePath: adoption.blueprintFilePath,
112
+ files: adoption.files,
113
+ nextSteps: [
114
+ ` npx ${bin} blueprints plan`,
115
+ ` npx ${bin} blueprints deploy`,
116
+ ` npx ${bin} blueprints info`,
117
+ ],
118
+ });
119
+ }
@@ -0,0 +1,14 @@
1
+ import type { CoreResult } from '../index.js';
2
+ import type { BlueprinitFlowOptions } from './types.js';
3
+ interface FinishOptions extends BlueprinitFlowOptions {
4
+ organizationId: string;
5
+ projectId: string;
6
+ datasetName: string;
7
+ stackName: string;
8
+ blueprintFilePath: string;
9
+ files: string[];
10
+ nextSteps: string[];
11
+ }
12
+ export declare function createStackAndFinish(options: FinishOptions): Promise<CoreResult>;
13
+ export declare function alignSteps(steps: Array<[command: string, description?: string]>): string[];
14
+ export {};
@@ -0,0 +1,64 @@
1
+ import { writeConfigFile } from '../../actions/blueprints/config.js';
2
+ import { createEmptyStack } from '../../actions/blueprints/stacks.js';
3
+ import { check } from '../../utils/display/presenters.js';
4
+ export async function createStackAndFinish(options) {
5
+ let stack;
6
+ try {
7
+ stack = await createEmptyStack({
8
+ token: options.token,
9
+ scopeType: 'organization',
10
+ scopeId: options.organizationId,
11
+ name: options.stackName,
12
+ logger: options.log,
13
+ });
14
+ }
15
+ catch (error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ return {
18
+ success: false,
19
+ error: `Local files were written, but Stack creation failed: ${message}`,
20
+ suggestions: [
21
+ 'Resolve the Stack error, then finish setup with:',
22
+ `npx ${options.bin} blueprints init --organization-id ${options.organizationId} --stack-name ${options.stackName}`,
23
+ ],
24
+ };
25
+ }
26
+ try {
27
+ writeConfigFile(options.blueprintFilePath, {
28
+ organizationId: options.organizationId,
29
+ stackId: stack.id,
30
+ });
31
+ }
32
+ catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ return {
35
+ success: false,
36
+ error: `Stack ${stack.id} was created, but the local Blueprint config could not be written: ${message}`,
37
+ suggestions: [
38
+ 'Finish setup with:',
39
+ `npx ${options.bin} blueprints init --organization-id ${options.organizationId} --stack-id ${stack.id}`,
40
+ ],
41
+ };
42
+ }
43
+ options.log(check(`Stack ${stack.name} created (org scope)`));
44
+ options.log('The Stack is empty; Blueprint resources have not been deployed.');
45
+ options.log('');
46
+ options.log('Next steps:');
47
+ for (const step of options.nextSteps)
48
+ options.log(step);
49
+ return {
50
+ success: true,
51
+ json: {
52
+ organizationId: options.organizationId,
53
+ projectId: options.projectId,
54
+ dataset: options.datasetName,
55
+ stackId: stack.id,
56
+ stackName: stack.name,
57
+ files: [...options.files, '.sanity/blueprint.config.json'],
58
+ },
59
+ };
60
+ }
61
+ export function alignSteps(steps) {
62
+ const width = Math.max(...steps.map(([command]) => command.length));
63
+ return steps.map(([command, description]) => description ? ` ${command.padEnd(width + 2)}${description}` : ` ${command}`);
64
+ }
@@ -0,0 +1,3 @@
1
+ import type { CoreResult } from '../index.js';
2
+ import type { BlueprinitFlowOptions } from './types.js';
3
+ export declare function freshFlow(options: BlueprinitFlowOptions): Promise<CoreResult>;
@@ -0,0 +1,120 @@
1
+ import { env } from 'node:process';
2
+ import { hostedStudioOrigin, STARTER_FUNCTION_NAME, slugify, } from '../../actions/blueprinit/manifest.js';
3
+ import { scaffoldWorkspace } from '../../actions/blueprinit/workspace.js';
4
+ import { listDatasets } from '../../actions/sanity/datasets.js';
5
+ import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
6
+ import { check } from '../../utils/display/presenters.js';
7
+ import { getLatestNpmVersion } from '../../utils/external/npmjs.js';
8
+ import { isInteractive } from '../../utils/interactive.js';
9
+ import { alignSteps, createStackAndFinish } from './finish.js';
10
+ import { printBlueprintPreview } from './preview.js';
11
+ import { confirmUnlessSkipped, promptForStudioName, resourceNamesCollide } from './prompts.js';
12
+ import { chooseDataset, chooseOrganization, chooseProject } from './selection.js';
13
+ export async function freshFlow(options) {
14
+ const { bin, dir, flags, log, token } = options;
15
+ const grouped = await groupProjectsByOrganization({ token, logger: log });
16
+ if (!grouped.ok) {
17
+ return { success: false, error: `Could not list organizations: ${grouped.error}` };
18
+ }
19
+ const organizationResult = await chooseOrganization(grouped.organizations, flags.organizationId, isInteractive);
20
+ if (!organizationResult.ok) {
21
+ return {
22
+ success: false,
23
+ error: organizationResult.error.message,
24
+ suggestions: organizationResult.error.suggestions,
25
+ };
26
+ }
27
+ const { organization, projects = [] } = organizationResult.value;
28
+ const projectResult = await chooseProject(projects, flags.projectId, organization.name, isInteractive);
29
+ if (!projectResult.ok) {
30
+ return {
31
+ success: false,
32
+ error: projectResult.error.message,
33
+ suggestions: projectResult.error.suggestions,
34
+ };
35
+ }
36
+ const project = projectResult.value;
37
+ const datasets = await listDatasets({ token, projectId: project.id, logger: log });
38
+ if (!datasets.ok) {
39
+ return { success: false, error: `Could not list datasets: ${datasets.error}` };
40
+ }
41
+ const datasetResult = await chooseDataset(datasets.datasets, flags.datasetName, project.displayName, isInteractive);
42
+ if (!datasetResult.ok) {
43
+ return {
44
+ success: false,
45
+ error: datasetResult.error.message,
46
+ suggestions: datasetResult.error.suggestions,
47
+ };
48
+ }
49
+ const dataset = datasetResult.value;
50
+ log(check(`Org: ${organization.name} (${organization.id})`));
51
+ log(check(`Project: ${project.displayName} (${project.id}), dataset: ${dataset.name}`));
52
+ log('');
53
+ const stackName = flags.stackName
54
+ ? slugify(flags.stackName) || project.id
55
+ : slugify(project.displayName) || project.id;
56
+ const studioName = await promptForStudioName(flags, isInteractive);
57
+ if (resourceNamesCollide(stackName, studioName)) {
58
+ return {
59
+ success: false,
60
+ error: `Stack and Studio names both resolve to "${stackName}". Blueprint resource names must be unique.`,
61
+ suggestions: ['Pass --stack-name or --studio-name with a different value.'],
62
+ };
63
+ }
64
+ const studioOrigin = hostedStudioOrigin(studioName, env.SANITY_INTERNAL_ENV);
65
+ printBlueprintPreview({
66
+ log,
67
+ organizationName: organization.name,
68
+ projectId: project.id,
69
+ projectName: project.displayName,
70
+ datasetName: dataset.name,
71
+ studioName,
72
+ studioOrigin,
73
+ studioSource: `studios/${studioName}`,
74
+ stackName,
75
+ functionName: STARTER_FUNCTION_NAME,
76
+ });
77
+ log('Files to write:');
78
+ log(' + sanity.blueprint.ts project + dataset (attached), CORS, studio, function');
79
+ const studioDirectory = `studios/${studioName}/`;
80
+ log(` + ${studioDirectory.padEnd(Math.max(32, studioDirectory.length + 2))}Studio with a "post" type`);
81
+ log(` + ${`functions/${STARTER_FUNCTION_NAME}/`.padEnd(32)}sets a post slug from its title`);
82
+ log(` + .sanity/blueprint.config.json org scope, stack ${stackName}`);
83
+ log(' + .gitignore');
84
+ log(' + package.json');
85
+ log('');
86
+ const confirmed = await confirmUnlessSkipped(flags, isInteractive, `Create this Blueprint workspace for "${project.displayName}"?`);
87
+ if (!confirmed)
88
+ return { success: false, error: 'Initialization cancelled.' };
89
+ const scaffold = scaffoldWorkspace({
90
+ dir,
91
+ projectId: project.id,
92
+ displayName: project.displayName,
93
+ datasetName: dataset.name,
94
+ stackName,
95
+ studioName,
96
+ studioOrigin,
97
+ blueprintsVersion: await getLatestNpmVersion('@sanity/blueprints', log),
98
+ });
99
+ return await createStackAndFinish({
100
+ ...options,
101
+ organizationId: organization.id,
102
+ projectId: project.id,
103
+ datasetName: dataset.name,
104
+ stackName,
105
+ blueprintFilePath: scaffold.blueprintFilePath,
106
+ files: scaffold.files,
107
+ nextSteps: [
108
+ ...alignSteps([
109
+ ['npm install'],
110
+ [`npm run dev -w studios/${studioName}`, 'open the Studio and add a post'],
111
+ [`npx ${bin} blueprints plan`, 'preview the deployment'],
112
+ [`npx ${bin} blueprints deploy`, 'deploy the resources'],
113
+ ]),
114
+ '',
115
+ 'Test the function on that post:',
116
+ ` npx ${bin} functions test ${STARTER_FUNCTION_NAME} --document-id <id>` +
117
+ ` --project-id ${project.id} --dataset ${dataset.name} --with-user-token`,
118
+ ],
119
+ });
120
+ }
@@ -0,0 +1,15 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ interface BlueprintPreviewOptions {
3
+ log: Logger;
4
+ organizationName: string;
5
+ projectId: string;
6
+ projectName: string;
7
+ datasetName: string;
8
+ studioName: string;
9
+ studioOrigin: string;
10
+ studioSource: string;
11
+ stackName: string;
12
+ functionName?: string;
13
+ }
14
+ export declare function printBlueprintPreview(options: BlueprintPreviewOptions): void;
15
+ export {};
@@ -0,0 +1,14 @@
1
+ export function printBlueprintPreview(options) {
2
+ options.log('Blueprint plan:');
3
+ options.log(` attach Project ${options.projectName} (${options.projectId}) on deploy; retain on Stack removal`);
4
+ options.log(` attach Dataset ${options.datasetName} on deploy; retain on Stack removal`);
5
+ options.log(` create CORS Origin ${options.studioName}-cors for ${options.studioOrigin} on deploy`);
6
+ options.log(` create Studio ${options.studioName} from ${options.studioSource} at ${options.studioOrigin} on deploy`);
7
+ if (options.functionName) {
8
+ options.log(` create Function ${options.functionName} on deploy`);
9
+ }
10
+ options.log('');
11
+ options.log('Remote action now:');
12
+ options.log(` create Stack ${options.stackName} in ${options.organizationName} (empty, organization scope)`);
13
+ options.log('');
14
+ }
@@ -0,0 +1,5 @@
1
+ import type { BlueprinitFlags } from './types.js';
2
+ export declare const DEFAULT_STUDIO_NAME = "studio";
3
+ export declare function resourceNamesCollide(stackName: string, studioName: string): boolean;
4
+ export declare function confirmUnlessSkipped(flags: BlueprinitFlags, interactive: boolean, message: string): Promise<boolean>;
5
+ export declare function promptForStudioName(flags: BlueprinitFlags, interactive: boolean): Promise<string>;
@@ -0,0 +1,22 @@
1
+ import { confirm, input } from '@inquirer/prompts';
2
+ import { slugify } from '../../actions/blueprinit/manifest.js';
3
+ export const DEFAULT_STUDIO_NAME = 'studio';
4
+ export function resourceNamesCollide(stackName, studioName) {
5
+ return stackName === studioName;
6
+ }
7
+ export async function confirmUnlessSkipped(flags, interactive, message) {
8
+ if (flags.yes || !interactive)
9
+ return true;
10
+ return await confirm({ message, default: true });
11
+ }
12
+ export async function promptForStudioName(flags, interactive) {
13
+ if (flags.studioName)
14
+ return slugify(flags.studioName) || DEFAULT_STUDIO_NAME;
15
+ if (flags.yes || !interactive)
16
+ return DEFAULT_STUDIO_NAME;
17
+ const answer = await input({
18
+ message: 'Name your Studio and hosted slug?',
19
+ default: DEFAULT_STUDIO_NAME,
20
+ });
21
+ return slugify(answer) || DEFAULT_STUDIO_NAME;
22
+ }
@@ -0,0 +1,15 @@
1
+ import type { Dataset } from '../../actions/sanity/datasets.js';
2
+ import type { GroupedProjects, Project } from '../../actions/sanity/projects.js';
3
+ import type { Result } from '../../utils/types.js';
4
+ interface SelectionError {
5
+ message: string;
6
+ suggestions?: string[];
7
+ }
8
+ export declare function chooseOrganization(groups: GroupedProjects[], organizationId: string | undefined, interactive: boolean): Promise<Result<GroupedProjects, SelectionError>>;
9
+ export declare function chooseProject(projects: Project[], projectId: string | undefined, organizationName: string, interactive: boolean): Promise<Result<Project, SelectionError>>;
10
+ export declare function chooseDataset(datasets: Dataset[], datasetName: string | undefined, projectName: string, interactive: boolean): Promise<Result<Dataset, SelectionError>>;
11
+ export declare function findProject(groups: GroupedProjects[], projectId: string): {
12
+ organization: GroupedProjects['organization'];
13
+ project: Project;
14
+ } | undefined;
15
+ export {};