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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +21 -20
  2. package/dist/actions/blueprinit/manifest.d.ts +46 -0
  3. package/dist/actions/blueprinit/manifest.js +121 -0
  4. package/dist/actions/blueprinit/quote.d.ts +6 -0
  5. package/dist/actions/blueprinit/quote.js +8 -0
  6. package/dist/actions/blueprinit/studio.d.ts +6 -0
  7. package/dist/actions/blueprinit/studio.js +43 -0
  8. package/dist/actions/blueprinit/templates.d.ts +73 -0
  9. package/dist/actions/blueprinit/templates.js +138 -0
  10. package/dist/actions/blueprinit/versions.d.ts +8 -0
  11. package/dist/actions/blueprinit/versions.js +11 -0
  12. package/dist/actions/blueprinit/workspace.d.ts +27 -0
  13. package/dist/actions/blueprinit/workspace.js +145 -0
  14. package/dist/actions/blueprints/pre-deploy.js +2 -0
  15. package/dist/actions/blueprints/stacks.d.ts +1 -0
  16. package/dist/actions/sanity/create-project.d.ts +19 -0
  17. package/dist/actions/sanity/create-project.js +32 -0
  18. package/dist/actions/sanity/datasets.d.ts +15 -0
  19. package/dist/actions/sanity/datasets.js +26 -0
  20. package/dist/commands/functions/test.d.ts +1 -0
  21. package/dist/commands/functions/test.js +5 -0
  22. package/dist/commands/init.d.ts +21 -0
  23. package/dist/commands/init.js +82 -0
  24. package/dist/cores/blueprinit/adopt.d.ts +6 -0
  25. package/dist/cores/blueprinit/adopt.js +137 -0
  26. package/dist/cores/blueprinit/columns.d.ts +26 -0
  27. package/dist/cores/blueprinit/columns.js +27 -0
  28. package/dist/cores/blueprinit/finish.d.ts +15 -0
  29. package/dist/cores/blueprinit/finish.js +72 -0
  30. package/dist/cores/blueprinit/fresh.d.ts +3 -0
  31. package/dist/cores/blueprinit/fresh.js +219 -0
  32. package/dist/cores/blueprinit/naming.d.ts +27 -0
  33. package/dist/cores/blueprinit/naming.js +43 -0
  34. package/dist/cores/blueprinit/preview.d.ts +34 -0
  35. package/dist/cores/blueprinit/preview.js +110 -0
  36. package/dist/cores/blueprinit/project-choice.d.ts +28 -0
  37. package/dist/cores/blueprinit/project-choice.js +82 -0
  38. package/dist/cores/blueprinit/prompts.d.ts +13 -0
  39. package/dist/cores/blueprinit/prompts.js +42 -0
  40. package/dist/cores/blueprinit/results.d.ts +8 -0
  41. package/dist/cores/blueprinit/results.js +4 -0
  42. package/dist/cores/blueprinit/selection.d.ts +15 -0
  43. package/dist/cores/blueprinit/selection.js +90 -0
  44. package/dist/cores/blueprinit/stack-name.d.ts +39 -0
  45. package/dist/cores/blueprinit/stack-name.js +56 -0
  46. package/dist/cores/blueprinit/target.d.ts +33 -0
  47. package/dist/cores/blueprinit/target.js +80 -0
  48. package/dist/cores/blueprinit/types.d.ts +20 -0
  49. package/dist/cores/blueprinit/types.js +1 -0
  50. package/dist/cores/blueprinit.d.ts +7 -0
  51. package/dist/cores/blueprinit.js +64 -0
  52. package/dist/cores/functions/test.js +41 -26
  53. package/dist/utils/display/intro.d.ts +6 -0
  54. package/dist/utils/display/intro.js +63 -0
  55. package/dist/utils/functions/child-process-wrapper.js +3 -2
  56. package/dist/utils/functions/child-runtime/durable.d.ts +2 -2
  57. package/dist/utils/functions/child-runtime/durable.js +52 -2
  58. package/dist/utils/functions/durable-logs.d.ts +16 -0
  59. package/dist/utils/functions/durable-logs.js +52 -0
  60. package/dist/utils/functions/invoke/local.js +2 -1
  61. package/dist/utils/types.d.ts +18 -4
  62. package/oclif.manifest.json +159 -1
  63. package/package.json +1 -1
  64. package/skills/init.md +199 -0
@@ -0,0 +1,72 @@
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
+ import { styleText } from '../../utils/style-text.js';
5
+ import { renderRows } from './columns.js';
6
+ export async function createStackAndFinish(options) {
7
+ let stack;
8
+ try {
9
+ stack = await createEmptyStack({
10
+ token: options.token,
11
+ scopeType: 'organization',
12
+ scopeId: options.organizationId,
13
+ name: options.stackName,
14
+ logger: options.log,
15
+ });
16
+ }
17
+ catch (error) {
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ return {
20
+ success: false,
21
+ error: `Local files were written, but Stack creation failed: ${message}`,
22
+ suggestions: [
23
+ 'Resolve the Stack error, then finish setup with:',
24
+ `npx ${options.bin} blueprints init --organization-id ${options.organizationId} --stack-name ${options.stackName}`,
25
+ ],
26
+ };
27
+ }
28
+ try {
29
+ writeConfigFile(options.blueprintFilePath, {
30
+ organizationId: options.organizationId,
31
+ stackId: stack.id,
32
+ });
33
+ }
34
+ catch (error) {
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ return {
37
+ success: false,
38
+ error: `Stack ${stack.id} was created, but the local Blueprint config could not be written: ${message}`,
39
+ suggestions: [
40
+ 'Finish setup with:',
41
+ `npx ${options.bin} blueprints init --organization-id ${options.organizationId} --stack-id ${stack.id}`,
42
+ ],
43
+ };
44
+ }
45
+ options.log('');
46
+ options.log(check(`Stack ${styleText('bold', stack.name)} created ${styleText('dim', '(org scope, nothing deployed yet)')}`));
47
+ options.log('');
48
+ options.log('');
49
+ options.log(styleText('bold', 'Next steps:'));
50
+ for (const step of options.nextSteps)
51
+ options.log(step);
52
+ return {
53
+ success: true,
54
+ json: {
55
+ organizationId: options.organizationId,
56
+ projectId: options.projectId,
57
+ dataset: options.datasetName,
58
+ stackId: stack.id,
59
+ stackName: stack.name,
60
+ files: [...options.files, '.sanity/blueprint.config.json'],
61
+ },
62
+ };
63
+ }
64
+ /** A command to run next, and what it does. */
65
+ export function alignSteps(steps) {
66
+ return renderRows(steps.map(([command, description]) => {
67
+ const cells = [{ text: command, style: 'cyan', gap: 2 }];
68
+ if (description)
69
+ cells.push({ text: description, style: 'dim' });
70
+ return cells;
71
+ }));
72
+ }
@@ -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,219 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { corsResourceName, projectBaseName, projectResourceName, STARTER_FUNCTION_NAME, studioSourcePath, } from '../../actions/blueprinit/manifest.js';
3
+ import { getLatestNpmRange } from '../../actions/blueprinit/versions.js';
4
+ import { scaffoldWorkspace } from '../../actions/blueprinit/workspace.js';
5
+ import { createProject } from '../../actions/sanity/create-project.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 { isInteractive } from '../../utils/interactive.js';
10
+ import { styleText } from '../../utils/style-text.js';
11
+ import { alignSteps, createStackAndFinish } from './finish.js';
12
+ import { chooseNames } from './naming.js';
13
+ import { fileLines, printBlueprintPreview } from './preview.js';
14
+ import { chooseProjectSource } from './project-choice.js';
15
+ import { confirmUnlessSkipped, studioResourceName } from './prompts.js';
16
+ import { coreFailure } from './results.js';
17
+ import { chooseDataset, chooseOrganization, chooseProject } from './selection.js';
18
+ import { promptForTargetDirectory, relativeTargetPath, resolveTargetDirectory } from './target.js';
19
+ /**
20
+ * Registry versions for the files this flow writes.
21
+ *
22
+ * Started before the preview so the lookups overlap the time the reader spends on it.
23
+ * `getLatestNpmRange` answers `latest` rather than rejecting, so nothing awaits an error.
24
+ */
25
+ function scaffoldVersions(log) {
26
+ return Promise.all([
27
+ getLatestNpmRange('@sanity/blueprints', log),
28
+ getLatestNpmRange('sanity', log),
29
+ getLatestNpmRange('@sanity/vision', log),
30
+ ]);
31
+ }
32
+ export async function freshFlow(options) {
33
+ const { bin, dir, flags, log, token } = options;
34
+ const grouped = await groupProjectsByOrganization({ token, logger: log });
35
+ if (!grouped.ok) {
36
+ return { success: false, error: `Could not list organizations: ${grouped.error}` };
37
+ }
38
+ const organizationResult = await chooseOrganization(grouped.organizations, flags.organizationId, isInteractive);
39
+ if (!organizationResult.ok)
40
+ return coreFailure(organizationResult.error);
41
+ const { organization, projects = [] } = organizationResult.value;
42
+ const sourceResult = await chooseProjectSource({
43
+ flags,
44
+ interactive: isInteractive,
45
+ projects,
46
+ organizationName: organization.name,
47
+ });
48
+ if (!sourceResult.ok)
49
+ return coreFailure(sourceResult.error);
50
+ const source = sourceResult.value;
51
+ // A project this run creates has no ID yet, and no datasets to list or choose.
52
+ const selected = source.create
53
+ ? { displayName: source.displayName, projectId: undefined, datasetName: source.datasetName }
54
+ : await selectExistingProject({ ...options, projects, organizationName: organization.name });
55
+ if ('error' in selected)
56
+ return coreFailure(selected.error);
57
+ const datasetAction = source.create ? 'create' : 'attach';
58
+ log(check(`Org: ${organization.name} (${organization.id})`));
59
+ log(check(source.create
60
+ ? `New project: ${selected.displayName}, dataset: ${selected.datasetName} (both made for you)`
61
+ : `Project: ${selected.displayName} (${selected.projectId}), dataset: ${selected.datasetName}`));
62
+ log('');
63
+ const baseName = projectBaseName(selected.displayName, selected.projectId ?? organization.id);
64
+ const projectName = projectResourceName(baseName);
65
+ const studioName = studioResourceName(flags);
66
+ const studioSource = studioSourcePath(studioName);
67
+ const target = options.targetProvided
68
+ ? { ok: true, value: dir }
69
+ : resolveTargetDirectory(dir, await promptForTargetDirectory(dir, baseName, isInteractive && !flags.yes), bin);
70
+ if (!target.ok)
71
+ return coreFailure(target.error);
72
+ const targetDir = target.value;
73
+ const names = await chooseNames({
74
+ flags,
75
+ log,
76
+ token,
77
+ organizationId: organization.id,
78
+ baseName,
79
+ datasetName: selected.datasetName,
80
+ resourceNames: [
81
+ projectName,
82
+ selected.datasetName,
83
+ corsResourceName(studioName),
84
+ studioName,
85
+ STARTER_FUNCTION_NAME,
86
+ ],
87
+ });
88
+ if (!names.ok)
89
+ return coreFailure(names.error);
90
+ const { stackName, studioOrigin } = names.value;
91
+ const versions = scaffoldVersions(log);
92
+ const { attaches, creates } = printBlueprintPreview({
93
+ log,
94
+ organizationName: organization.name,
95
+ projectId: selected.projectId,
96
+ projectName: selected.displayName,
97
+ datasetName: selected.datasetName,
98
+ datasetAction,
99
+ createsProject: source.create,
100
+ studioName,
101
+ studioOrigin,
102
+ studioSource,
103
+ stackName,
104
+ functionName: STARTER_FUNCTION_NAME,
105
+ });
106
+ log(styleText('bold', 'Files to write:'));
107
+ for (const line of fileLines([
108
+ ['+', 'sanity.blueprint.ts', 'project + dataset, CORS, studio, function'],
109
+ ['+', `${studioSource}/`, 'Studio with a "post" type'],
110
+ ['+', `functions/${STARTER_FUNCTION_NAME}/`, 'sets a post slug from its title'],
111
+ ['+', '.sanity/blueprint.config.json', `org scope, stack ${stackName}`],
112
+ ['+', '.gitignore'],
113
+ ['+', 'package.json'],
114
+ ])) {
115
+ log(line);
116
+ }
117
+ log('');
118
+ const confirmed = await confirmUnlessSkipped(flags, isInteractive, `Create this Blueprint workspace for "${selected.displayName}"?`);
119
+ if (!confirmed)
120
+ return { success: false, error: 'Initialization cancelled.' };
121
+ // Created after the confirmation, so a cancelled run leaves no project behind.
122
+ let projectId = selected.projectId;
123
+ if (!projectId) {
124
+ const created = await createProject({
125
+ token,
126
+ organizationId: organization.id,
127
+ displayName: selected.displayName,
128
+ logger: log,
129
+ });
130
+ if (!created.ok || !created.project?.id) {
131
+ return {
132
+ success: false,
133
+ error: `Could not create project "${selected.displayName}": ${created.error}`,
134
+ suggestions: ['Nothing was written. Run init again, or pass --project-id to use another.'],
135
+ };
136
+ }
137
+ projectId = created.project.id;
138
+ log('');
139
+ log(check(`Project ${styleText('bold', selected.displayName)} created (${projectId})`));
140
+ }
141
+ const relativeDir = relativeTargetPath(targetDir);
142
+ const [blueprintsVersion, sanityVersion, visionVersion] = await versions;
143
+ mkdirSync(targetDir, { recursive: true });
144
+ const scaffold = scaffoldWorkspace({
145
+ dir: targetDir,
146
+ projectId,
147
+ displayName: selected.displayName,
148
+ datasetName: selected.datasetName,
149
+ datasetAction,
150
+ stackName,
151
+ projectName,
152
+ studioName,
153
+ studioSlug: stackName,
154
+ studioOrigin,
155
+ blueprintsVersion,
156
+ studioVersions: { sanity: sanityVersion, vision: visionVersion },
157
+ });
158
+ return await createStackAndFinish({
159
+ ...options,
160
+ organizationId: organization.id,
161
+ projectId,
162
+ datasetName: selected.datasetName,
163
+ stackName,
164
+ blueprintFilePath: scaffold.blueprintFilePath,
165
+ files: scaffold.files,
166
+ nextSteps: [
167
+ ...alignSteps([
168
+ ...(relativeDir ? [[`cd ${relativeDir}`]] : []),
169
+ ['npm install'],
170
+ // The dataset does not exist until deploy, so a created one is deployed before
171
+ // `sanity dev` opens a Studio that would query it.
172
+ ...(datasetAction === 'create'
173
+ ? [
174
+ [
175
+ `npx ${bin} blueprints plan`,
176
+ `preview the ${attaches} attaches and ${creates} creates`,
177
+ ],
178
+ [`npx ${bin} blueprints deploy`, `create ${selected.datasetName} and the rest`],
179
+ [`npm run dev -w ${studioSource}`, 'open the Studio and add a post'],
180
+ ]
181
+ : [
182
+ [`npm run dev -w ${studioSource}`, 'open the Studio and add a post'],
183
+ [
184
+ `npx ${bin} blueprints plan`,
185
+ `preview the ${attaches} attaches and ${creates} creates`,
186
+ ],
187
+ [`npx ${bin} blueprints deploy`, 'attach and create them'],
188
+ ]),
189
+ [`npx ${bin} blueprints info`, 'review what the Stack owns'],
190
+ ]),
191
+ '',
192
+ `Your Studio is hosted at ${studioOrigin} after deploy.`,
193
+ '',
194
+ `${STARTER_FUNCTION_NAME} fills in a post's slug. Try it on a post you added, then follow the deployed one:`,
195
+ ` npx ${bin} functions test ${STARTER_FUNCTION_NAME} --document-id <id>` +
196
+ ` --project-id ${projectId} --dataset ${selected.datasetName} --with-user-token`,
197
+ ` npx ${bin} functions logs ${STARTER_FUNCTION_NAME}`,
198
+ ],
199
+ });
200
+ }
201
+ /** The existing-project path: pick a project, then pick one of its datasets. */
202
+ async function selectExistingProject(options) {
203
+ const { flags, log, token, organizationName } = options;
204
+ const projectResult = await chooseProject(options.projects ?? [], flags.projectId, organizationName, isInteractive);
205
+ if (!projectResult.ok)
206
+ return { error: projectResult.error };
207
+ const project = projectResult.value;
208
+ const datasets = await listDatasets({ token, projectId: project.id, logger: log });
209
+ if (!datasets.ok)
210
+ return { error: { message: `Could not list datasets: ${datasets.error}` } };
211
+ const datasetResult = await chooseDataset(datasets.datasets, flags.datasetName, project.displayName, isInteractive);
212
+ if (!datasetResult.ok)
213
+ return { error: datasetResult.error };
214
+ return {
215
+ displayName: project.displayName,
216
+ projectId: project.id,
217
+ datasetName: datasetResult.value.name,
218
+ };
219
+ }
@@ -0,0 +1,27 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { Result } from '../../utils/types.js';
3
+ import type { FlowError } from './results.js';
4
+ import type { BlueprinitFlags } from './types.js';
5
+ export interface ChooseNamesOptions {
6
+ flags: BlueprinitFlags;
7
+ log: Logger;
8
+ token: string;
9
+ organizationId: string;
10
+ /** The slug form of the project display name, which both defaults are built from. */
11
+ baseName: string;
12
+ datasetName: string;
13
+ /** Every Blueprint-local resource name this flow will write. */
14
+ resourceNames: string[];
15
+ }
16
+ export interface ChosenNames {
17
+ stackName: string;
18
+ studioOrigin: string;
19
+ }
20
+ /**
21
+ * The two names a flow must settle before it previews anything.
22
+ *
23
+ * The Stack name is also the hosted Studio slug, so one answer names both, and the origin
24
+ * follows from it. Resource names are checked afterwards, because a repeated name writes
25
+ * a manifest that cannot deploy.
26
+ */
27
+ export declare function chooseNames(options: ChooseNamesOptions): Promise<Result<ChosenNames, FlowError>>;
@@ -0,0 +1,43 @@
1
+ import { env } from 'node:process';
2
+ import { defaultStackName, hostedStudioOrigin } from '../../actions/blueprinit/manifest.js';
3
+ import { isInteractive } from '../../utils/interactive.js';
4
+ import { duplicateResourceNames } from './prompts.js';
5
+ import { chooseStackName, takenStackNames } from './stack-name.js';
6
+ /**
7
+ * The two names a flow must settle before it previews anything.
8
+ *
9
+ * The Stack name is also the hosted Studio slug, so one answer names both, and the origin
10
+ * follows from it. Resource names are checked afterwards, because a repeated name writes
11
+ * a manifest that cannot deploy.
12
+ */
13
+ export async function chooseNames(options) {
14
+ const { flags, log, token, organizationId, baseName, datasetName, resourceNames } = options;
15
+ const stackNameResult = await chooseStackName({
16
+ flags,
17
+ interactive: isInteractive,
18
+ defaultName: defaultStackName(baseName, datasetName),
19
+ taken: await takenStackNames({ token, organizationId, log }),
20
+ log,
21
+ });
22
+ if (!stackNameResult.ok) {
23
+ return {
24
+ ok: false,
25
+ error: { message: stackNameResult.error, suggestions: stackNameResult.suggestions },
26
+ };
27
+ }
28
+ const duplicates = duplicateResourceNames(resourceNames);
29
+ if (duplicates.length > 0) {
30
+ return {
31
+ ok: false,
32
+ error: {
33
+ message: `Blueprint resource names must be unique. Used twice: ${duplicates.join(', ')}.`,
34
+ suggestions: ['Pass --studio-name with a different value.'],
35
+ },
36
+ };
37
+ }
38
+ const stackName = stackNameResult.value;
39
+ return {
40
+ ok: true,
41
+ value: { stackName, studioOrigin: hostedStudioOrigin(stackName, env.SANITY_INTERNAL_ENV) },
42
+ };
43
+ }
@@ -0,0 +1,34 @@
1
+ import { type DatasetAction } from '../../actions/blueprinit/manifest.js';
2
+ import type { Logger } from '../../utils/logger.js';
3
+ interface BlueprintPreviewOptions {
4
+ log: Logger;
5
+ /** Absent when this run creates the project, because the ID exists only afterwards. */
6
+ projectId?: string;
7
+ organizationName: string;
8
+ projectName: string;
9
+ datasetName: string;
10
+ studioName: string;
11
+ studioOrigin: string;
12
+ studioSource: string;
13
+ stackName: string;
14
+ functionName?: string;
15
+ datasetAction: DatasetAction;
16
+ /** Set when this run creates the project before it writes anything. */
17
+ createsProject?: boolean;
18
+ }
19
+ /** The counts `blueprints plan` will report for this Blueprint. */
20
+ export interface PreviewCounts {
21
+ attaches: number;
22
+ creates: number;
23
+ }
24
+ /**
25
+ * Print the plan, then report the counts it showed.
26
+ *
27
+ * The counts come from the rows that were printed, so the table and the totals in the
28
+ * next steps cannot disagree.
29
+ */
30
+ export declare function printBlueprintPreview(options: BlueprintPreviewOptions): PreviewCounts;
31
+ /** A local change: a marker, the path, and what the file is for. */
32
+ export type FileLine = [marker: '+' | '~', path: string, description?: string];
33
+ export declare function fileLines(lines: FileLine[]): string[];
34
+ export {};
@@ -0,0 +1,110 @@
1
+ import { corsResourceName } from '../../actions/blueprinit/manifest.js';
2
+ import { styleText } from '../../utils/style-text.js';
3
+ import { renderRows } from './columns.js';
4
+ /** The icon and colour `blueprints plan` uses for the same action. */
5
+ const ACTION_STYLE = {
6
+ attach: { icon: '←', color: 'blue' },
7
+ create: { icon: '+', color: 'green' },
8
+ };
9
+ const RETAIN_SUFFIX = 'retain on Stack removal';
10
+ const RETAIN = `on deploy; ${RETAIN_SUFFIX}`;
11
+ function previewResources(options) {
12
+ const resources = [
13
+ {
14
+ action: 'attach',
15
+ kind: 'Project',
16
+ name: options.projectId
17
+ ? `${options.projectName} (${options.projectId})`
18
+ : options.projectName,
19
+ detail: options.projectId ? RETAIN : `once created, ${RETAIN_SUFFIX}`,
20
+ },
21
+ {
22
+ action: options.datasetAction,
23
+ kind: 'Dataset',
24
+ name: options.datasetName,
25
+ detail: options.datasetAction === 'attach' ? RETAIN : `on deploy; ${RETAIN_SUFFIX}`,
26
+ },
27
+ {
28
+ action: 'create',
29
+ kind: 'CORS Origin',
30
+ name: corsResourceName(options.studioName),
31
+ detail: `for ${options.studioOrigin} on deploy`,
32
+ },
33
+ {
34
+ action: 'create',
35
+ kind: 'Studio',
36
+ name: options.studioName,
37
+ // The origin is on the CORS line directly above, so it is not repeated here.
38
+ detail: `from ${options.studioSource} on deploy`,
39
+ },
40
+ ];
41
+ if (options.functionName) {
42
+ resources.push({
43
+ action: 'create',
44
+ kind: 'Function',
45
+ name: options.functionName,
46
+ detail: 'on deploy',
47
+ });
48
+ }
49
+ return resources;
50
+ }
51
+ function actionRow(resource) {
52
+ const { icon, color } = ACTION_STYLE[resource.action];
53
+ return [
54
+ { text: icon, style: ['bold', color] },
55
+ { text: resource.action, style: color },
56
+ { text: resource.kind },
57
+ { text: resource.name, style: 'bold', gap: 2 },
58
+ { text: resource.detail, style: 'dim' },
59
+ ];
60
+ }
61
+ /**
62
+ * Print the plan, then report the counts it showed.
63
+ *
64
+ * The counts come from the rows that were printed, so the table and the totals in the
65
+ * next steps cannot disagree.
66
+ */
67
+ export function printBlueprintPreview(options) {
68
+ const resources = previewResources(options);
69
+ const remote = [];
70
+ if (options.createsProject) {
71
+ remote.push({
72
+ action: 'create',
73
+ kind: 'Project',
74
+ name: options.projectName,
75
+ detail: `in ${options.organizationName}, before any file is written`,
76
+ });
77
+ }
78
+ remote.push({
79
+ action: 'create',
80
+ kind: 'Stack',
81
+ name: options.stackName,
82
+ detail: `in ${options.organizationName} (empty, organization scope)`,
83
+ });
84
+ // One call across both blocks, so one set of widths makes the two read as one table.
85
+ const rows = renderRows([...resources, ...remote].map(actionRow));
86
+ const remoteRows = rows.slice(resources.length);
87
+ options.log(styleText('bold', 'Blueprint plan:'));
88
+ for (const row of rows.slice(0, resources.length))
89
+ options.log(row);
90
+ options.log('');
91
+ options.log(styleText('bold', remote.length === 1 ? 'One remote change now:' : 'Remote changes now:'));
92
+ for (const row of remoteRows)
93
+ options.log(row);
94
+ options.log('');
95
+ return {
96
+ attaches: resources.filter(({ action }) => action === 'attach').length,
97
+ creates: resources.filter(({ action }) => action === 'create').length,
98
+ };
99
+ }
100
+ export function fileLines(lines) {
101
+ return renderRows(lines.map(([marker, path, description]) => {
102
+ const cells = [
103
+ { text: marker, style: ['bold', marker === '+' ? 'green' : 'yellow'] },
104
+ { text: path, style: 'bold', gap: 3 },
105
+ ];
106
+ if (description)
107
+ cells.push({ text: description, style: 'dim' });
108
+ return cells;
109
+ }));
110
+ }
@@ -0,0 +1,28 @@
1
+ import type { Project } from '../../actions/sanity/projects.js';
2
+ import type { Result } from '../../utils/types.js';
3
+ import type { FlowError } from './results.js';
4
+ import type { BlueprinitFlags } from './types.js';
5
+ export declare const DEFAULT_DATASET_NAME = "production";
6
+ export declare function datasetNameError(name: string): string | undefined;
7
+ /** What the fresh flow attaches a Blueprint to: a project that exists, or one to create. */
8
+ export type ProjectChoice = {
9
+ create: false;
10
+ } | {
11
+ create: true;
12
+ displayName: string;
13
+ datasetName: string;
14
+ };
15
+ interface ChooseProjectSourceOptions {
16
+ flags: BlueprinitFlags;
17
+ interactive: boolean;
18
+ projects: Project[];
19
+ organizationName: string;
20
+ }
21
+ /**
22
+ * Whether this run creates a project.
23
+ *
24
+ * `--create-project` decides it without asking. An organization with no projects has
25
+ * nothing to select, so creation is the only way forward there.
26
+ */
27
+ export declare function chooseProjectSource(options: ChooseProjectSourceOptions): Promise<Result<ProjectChoice, FlowError>>;
28
+ export {};
@@ -0,0 +1,82 @@
1
+ import { input, select } from '@inquirer/prompts';
2
+ export const DEFAULT_DATASET_NAME = 'production';
3
+ /**
4
+ * The dataset name rule the projects API enforces, with `convert: false`.
5
+ *
6
+ * Underscores are legal, so a name is checked and refused rather than slugified. The
7
+ * pattern also rejects a single character, which is why a short name fails here and not
8
+ * at deploy.
9
+ */
10
+ const DATASET_NAME_PATTERN = /^[a-z0-9][-_a-z0-9]+$/;
11
+ export function datasetNameError(name) {
12
+ if (name.length > 64)
13
+ return 'A dataset name can be at most 64 characters.';
14
+ if (!DATASET_NAME_PATTERN.test(name)) {
15
+ return 'A dataset name must be at least 2 characters, and use only a-z, 0-9, - and _, starting with a letter or a digit.';
16
+ }
17
+ return undefined;
18
+ }
19
+ /**
20
+ * Whether this run creates a project.
21
+ *
22
+ * `--create-project` decides it without asking. An organization with no projects has
23
+ * nothing to select, so creation is the only way forward there.
24
+ */
25
+ export async function chooseProjectSource(options) {
26
+ const { flags, interactive, projects, organizationName } = options;
27
+ const named = flags.createProject?.trim();
28
+ if (!named && (flags.projectId || projects.length > 0)) {
29
+ if (flags.projectId || !interactive || flags.yes)
30
+ return { ok: true, value: { create: false } };
31
+ const source = await select({
32
+ message: `Use a project in ${organizationName}, or create one?`,
33
+ choices: [
34
+ { name: 'Use an existing project', value: 'existing' },
35
+ { name: 'Create a new project', value: 'new' },
36
+ ],
37
+ });
38
+ if (source === 'existing')
39
+ return { ok: true, value: { create: false } };
40
+ }
41
+ if (!named && !interactive) {
42
+ return {
43
+ ok: false,
44
+ error: {
45
+ message: `No projects found in ${organizationName}.`,
46
+ suggestions: ['Pass --create-project <name> to create one.'],
47
+ },
48
+ };
49
+ }
50
+ const displayName = named || (await promptForProjectName(organizationName));
51
+ if (!displayName) {
52
+ return { ok: false, error: { message: 'A new project needs a display name.' } };
53
+ }
54
+ const datasetName = await chooseDatasetName(flags, interactive && !flags.yes);
55
+ if (!datasetName.ok)
56
+ return datasetName;
57
+ return { ok: true, value: { create: true, displayName, datasetName: datasetName.value } };
58
+ }
59
+ async function promptForProjectName(organizationName) {
60
+ const answer = await input({
61
+ message: `Name your new project in ${organizationName}:`,
62
+ validate: (value) => value.trim().length > 0 || 'Enter a project name.',
63
+ });
64
+ return answer.trim();
65
+ }
66
+ /** The dataset the Blueprint will create, which no API call has made yet. */
67
+ async function chooseDatasetName(flags, interactive) {
68
+ if (flags.datasetName) {
69
+ const problem = datasetNameError(flags.datasetName);
70
+ return problem
71
+ ? { ok: false, error: { message: `Dataset "${flags.datasetName}" is not valid. ${problem}` } }
72
+ : { ok: true, value: flags.datasetName };
73
+ }
74
+ if (!interactive)
75
+ return { ok: true, value: DEFAULT_DATASET_NAME };
76
+ const answer = await input({
77
+ message: 'Name the dataset this Blueprint will create:',
78
+ default: DEFAULT_DATASET_NAME,
79
+ validate: (value) => datasetNameError(value.trim()) ?? true,
80
+ });
81
+ return { ok: true, value: answer.trim() };
82
+ }
@@ -0,0 +1,13 @@
1
+ import type { BlueprinitFlags } from './types.js';
2
+ /**
3
+ * Resource names must be unique within a Blueprint, so report every name used twice.
4
+ *
5
+ * The generated names cannot collide with each other, because each one carries its
6
+ * resource type. A dataset name or a `--studio-name` value can still land on one.
7
+ */
8
+ export declare function duplicateResourceNames(names: string[]): string[];
9
+ export declare function confirmUnlessSkipped(flags: BlueprinitFlags, interactive: boolean, message: string): Promise<boolean>;
10
+ /** The new Stack's name, which is also the hosted Studio slug. */
11
+ export declare function promptForStackName(flags: BlueprinitFlags, interactive: boolean, defaultName: string): Promise<string>;
12
+ /** The Blueprint-local Studio name, which is also its directory under `studios/`. */
13
+ export declare function studioResourceName(flags: BlueprinitFlags): string;