@sanity/runtime-cli 17.8.0 → 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.
- package/README.md +28 -22
- package/dist/actions/blueprinit/manifest.d.ts +46 -0
- package/dist/actions/blueprinit/manifest.js +121 -0
- package/dist/actions/blueprinit/quote.d.ts +6 -0
- package/dist/actions/blueprinit/quote.js +8 -0
- package/dist/actions/blueprinit/studio.d.ts +6 -0
- package/dist/actions/blueprinit/studio.js +43 -0
- package/dist/actions/blueprinit/templates.d.ts +73 -0
- package/dist/actions/blueprinit/templates.js +138 -0
- package/dist/actions/blueprinit/versions.d.ts +8 -0
- package/dist/actions/blueprinit/versions.js +11 -0
- package/dist/actions/blueprinit/workspace.d.ts +27 -0
- package/dist/actions/blueprinit/workspace.js +145 -0
- package/dist/actions/blueprints/pre-deploy.js +2 -0
- package/dist/actions/blueprints/stacks.d.ts +3 -0
- package/dist/actions/blueprints/stacks.js +1 -0
- package/dist/actions/sanity/create-project.d.ts +19 -0
- package/dist/actions/sanity/create-project.js +32 -0
- package/dist/actions/sanity/datasets.d.ts +15 -0
- package/dist/actions/sanity/datasets.js +26 -0
- package/dist/actions/sanity/projects.d.ts +11 -0
- package/dist/actions/sanity/projects.js +48 -22
- package/dist/baseCommands.d.ts +6 -0
- package/dist/baseCommands.js +9 -1
- package/dist/commands/blueprints/stacks.d.ts +2 -1
- package/dist/commands/blueprints/stacks.js +14 -6
- package/dist/commands/functions/test.d.ts +1 -0
- package/dist/commands/functions/test.js +5 -0
- package/dist/commands/init.d.ts +21 -0
- package/dist/commands/init.js +82 -0
- package/dist/cores/blueprinit/adopt.d.ts +6 -0
- package/dist/cores/blueprinit/adopt.js +137 -0
- package/dist/cores/blueprinit/columns.d.ts +26 -0
- package/dist/cores/blueprinit/columns.js +27 -0
- package/dist/cores/blueprinit/finish.d.ts +15 -0
- package/dist/cores/blueprinit/finish.js +72 -0
- package/dist/cores/blueprinit/fresh.d.ts +3 -0
- package/dist/cores/blueprinit/fresh.js +219 -0
- package/dist/cores/blueprinit/naming.d.ts +27 -0
- package/dist/cores/blueprinit/naming.js +43 -0
- package/dist/cores/blueprinit/preview.d.ts +34 -0
- package/dist/cores/blueprinit/preview.js +110 -0
- package/dist/cores/blueprinit/project-choice.d.ts +28 -0
- package/dist/cores/blueprinit/project-choice.js +82 -0
- package/dist/cores/blueprinit/prompts.d.ts +13 -0
- package/dist/cores/blueprinit/prompts.js +42 -0
- package/dist/cores/blueprinit/results.d.ts +8 -0
- package/dist/cores/blueprinit/results.js +4 -0
- package/dist/cores/blueprinit/selection.d.ts +15 -0
- package/dist/cores/blueprinit/selection.js +90 -0
- package/dist/cores/blueprinit/stack-name.d.ts +39 -0
- package/dist/cores/blueprinit/stack-name.js +56 -0
- package/dist/cores/blueprinit/target.d.ts +33 -0
- package/dist/cores/blueprinit/target.js +80 -0
- package/dist/cores/blueprinit/types.d.ts +20 -0
- package/dist/cores/blueprinit/types.js +1 -0
- package/dist/cores/blueprinit.d.ts +7 -0
- package/dist/cores/blueprinit.js +64 -0
- package/dist/cores/blueprints/stacks.d.ts +8 -3
- package/dist/cores/blueprints/stacks.js +282 -85
- package/dist/cores/functions/test.d.ts +2 -19
- package/dist/cores/functions/test.js +63 -138
- package/dist/server/app.js +4 -6
- package/dist/server/static/components/payload-panel.js +1 -1
- package/dist/utils/display/intro.d.ts +6 -0
- package/dist/utils/display/intro.js +63 -0
- package/dist/utils/display/prompt.d.ts +4 -1
- package/dist/utils/display/prompt.js +16 -2
- package/dist/utils/functions/child-process-wrapper.js +3 -2
- package/dist/utils/functions/child-runtime/durable.d.ts +2 -2
- package/dist/utils/functions/child-runtime/durable.js +53 -3
- package/dist/utils/functions/durable-logs.d.ts +16 -0
- package/dist/utils/functions/durable-logs.js +52 -0
- package/dist/utils/functions/invoke/local.js +16 -8
- package/dist/utils/functions/invoke/resolve-invoke-type.d.ts +51 -0
- package/dist/utils/functions/invoke/resolve-invoke-type.js +131 -0
- package/dist/utils/types.d.ts +39 -6
- package/oclif.manifest.json +177 -5
- package/package.json +1 -1
- package/skills/blueprints-stacks.md +14 -2
- package/skills/init.md +199 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { llmsFlagConfig } from '@sanity-labs/oclif-plugin-skills-flag';
|
|
4
|
+
import { RuntimeCommand } from '../baseCommands.js';
|
|
5
|
+
import { blueprinitCore } from '../cores/blueprinit.js';
|
|
6
|
+
import { playIntro } from '../utils/display/intro.js';
|
|
7
|
+
import { Logger } from '../utils/logger.js';
|
|
8
|
+
export default class InitCommand extends RuntimeCommand {
|
|
9
|
+
static hidden = true;
|
|
10
|
+
static summary = 'Experimental blueprints-first project init';
|
|
11
|
+
static description = `Scaffolds a Blueprint, Studio, and starter function around a Sanity project.
|
|
12
|
+
|
|
13
|
+
Use a project you already have, or create one with --create-project. A new project has no
|
|
14
|
+
datasets, so the Blueprint creates the dataset rather than attaching it.
|
|
15
|
+
|
|
16
|
+
Pass a directory to scaffold into it. Without one, an interactive run asks where the files go.
|
|
17
|
+
|
|
18
|
+
This hidden command is an experiment; its interface and generated files are not stable.`;
|
|
19
|
+
static examples = [
|
|
20
|
+
'<%= config.bin %> <%= command.id %>',
|
|
21
|
+
'<%= config.bin %> <%= command.id %> ./my-workspace',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> --yes',
|
|
23
|
+
'<%= config.bin %> <%= command.id %> --project-id <id> --dataset <name> --yes',
|
|
24
|
+
'<%= config.bin %> <%= command.id %> --create-project "My Shop" --yes',
|
|
25
|
+
];
|
|
26
|
+
static args = {
|
|
27
|
+
directory: Args.string({
|
|
28
|
+
description: 'Directory to initialize. Interactive runs are asked for one instead.',
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
static flags = {
|
|
33
|
+
llms: Flags.boolean(llmsFlagConfig),
|
|
34
|
+
yes: Flags.boolean({
|
|
35
|
+
char: 'y',
|
|
36
|
+
description: 'Skip confirmation prompts',
|
|
37
|
+
default: false,
|
|
38
|
+
}),
|
|
39
|
+
'organization-id': Flags.string({
|
|
40
|
+
description: 'Organization to use when the account has several',
|
|
41
|
+
aliases: ['organization', 'organizationId', 'org'],
|
|
42
|
+
}),
|
|
43
|
+
'project-id': Flags.string({
|
|
44
|
+
description: 'Project to use when the organization has several',
|
|
45
|
+
aliases: ['project', 'projectId'],
|
|
46
|
+
}),
|
|
47
|
+
'create-project': Flags.string({
|
|
48
|
+
description: 'Create a project with this display name instead of using an existing one',
|
|
49
|
+
aliases: ['createProject', 'new-project'],
|
|
50
|
+
exclusive: ['project-id'],
|
|
51
|
+
}),
|
|
52
|
+
dataset: Flags.string({
|
|
53
|
+
description: 'Dataset to attach, or to create alongside a new project',
|
|
54
|
+
}),
|
|
55
|
+
'stack-name': Flags.string({
|
|
56
|
+
description: 'Name for the new organization-scoped Stack, and the hosted Studio slug',
|
|
57
|
+
}),
|
|
58
|
+
'studio-name': Flags.string({
|
|
59
|
+
description: 'Blueprint-local name for the Studio resource and its directory',
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
async run() {
|
|
63
|
+
await playIntro({ json: this.jsonEnabled() });
|
|
64
|
+
const result = await blueprinitCore({
|
|
65
|
+
bin: this.config.bin,
|
|
66
|
+
dir: this.args.directory ? resolve(this.args.directory) : undefined,
|
|
67
|
+
log: Logger(this.log.bind(this), this.flags),
|
|
68
|
+
flags: {
|
|
69
|
+
yes: this.flags.yes,
|
|
70
|
+
organizationId: this.flags['organization-id'],
|
|
71
|
+
projectId: this.flags['project-id'],
|
|
72
|
+
createProject: this.flags['create-project'],
|
|
73
|
+
datasetName: this.flags.dataset,
|
|
74
|
+
stackName: this.flags['stack-name'],
|
|
75
|
+
studioName: this.flags['studio-name'],
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
if (!result.success)
|
|
79
|
+
this.coreError(result);
|
|
80
|
+
return result.json;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -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,137 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { corsResourceName, projectBaseName, projectResourceName, } from '../../actions/blueprinit/manifest.js';
|
|
4
|
+
import { getLatestNpmRange } from '../../actions/blueprinit/versions.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 { 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 { confirmUnlessSkipped, studioResourceName } from './prompts.js';
|
|
15
|
+
import { coreFailure } from './results.js';
|
|
16
|
+
import { findProject } from './selection.js';
|
|
17
|
+
export async function adoptFlow(options) {
|
|
18
|
+
const { bin, dir, flags, log, studio, token } = options;
|
|
19
|
+
if (flags.projectId && studio.projectId && flags.projectId !== studio.projectId) {
|
|
20
|
+
return { success: false, error: `The Studio uses project "${studio.projectId}".` };
|
|
21
|
+
}
|
|
22
|
+
if (flags.datasetName && studio.dataset && flags.datasetName !== studio.dataset) {
|
|
23
|
+
return { success: false, error: `The Studio uses dataset "${studio.dataset}".` };
|
|
24
|
+
}
|
|
25
|
+
const projectId = studio.projectId ?? flags.projectId;
|
|
26
|
+
const datasetName = studio.dataset ?? flags.datasetName;
|
|
27
|
+
if (!projectId || !datasetName) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: `Found ${studio.configFile}, but could not read a projectId and dataset from it.`,
|
|
31
|
+
suggestions: ['Use literal values in the Studio config or pass --project-id and --dataset.'],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
log(check(`Studio detected (${studio.configFile})`));
|
|
35
|
+
const grouped = await groupProjectsByOrganization({ token, logger: log });
|
|
36
|
+
if (!grouped.ok) {
|
|
37
|
+
return { success: false, error: `Could not list organizations: ${grouped.error}` };
|
|
38
|
+
}
|
|
39
|
+
const match = findProject(grouped.organizations, projectId);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
error: `Project "${projectId}" from ${studio.configFile} was not found in your account.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (flags.organizationId && flags.organizationId !== match.organization.id) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: `Project "${studio.projectId}" belongs to organization "${match.organization.id}".`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const datasets = await listDatasets({ token, projectId: match.project.id, logger: log });
|
|
53
|
+
if (!datasets.ok) {
|
|
54
|
+
return { success: false, error: `Could not list datasets: ${datasets.error}` };
|
|
55
|
+
}
|
|
56
|
+
if (!datasets.datasets.some(({ name }) => name === datasetName)) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: `Dataset "${datasetName}" was not found in project ${match.project.displayName}.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
log(check(`Matches project ${match.project.displayName} (${match.project.id}), dataset ${datasetName}`));
|
|
63
|
+
log('');
|
|
64
|
+
const baseName = projectBaseName(match.project.displayName, match.project.id);
|
|
65
|
+
const projectName = projectResourceName(baseName);
|
|
66
|
+
const studioName = studioResourceName(flags);
|
|
67
|
+
const names = await chooseNames({
|
|
68
|
+
flags,
|
|
69
|
+
log,
|
|
70
|
+
token,
|
|
71
|
+
organizationId: match.organization.id,
|
|
72
|
+
baseName,
|
|
73
|
+
datasetName,
|
|
74
|
+
resourceNames: [projectName, datasetName, corsResourceName(studioName), studioName],
|
|
75
|
+
});
|
|
76
|
+
if (!names.ok)
|
|
77
|
+
return coreFailure(names.error);
|
|
78
|
+
const { stackName, studioOrigin } = names.value;
|
|
79
|
+
// Started before the preview so the lookup overlaps the time the reader spends on it.
|
|
80
|
+
const blueprintsVersion = getLatestNpmRange('@sanity/blueprints', log);
|
|
81
|
+
const marker = (file) => (existsSync(join(dir, file)) ? '~' : '+');
|
|
82
|
+
const { attaches, creates } = printBlueprintPreview({
|
|
83
|
+
log,
|
|
84
|
+
organizationName: match.organization.name,
|
|
85
|
+
projectId: match.project.id,
|
|
86
|
+
projectName: match.project.displayName,
|
|
87
|
+
datasetName,
|
|
88
|
+
datasetAction: 'attach',
|
|
89
|
+
studioName,
|
|
90
|
+
studioOrigin,
|
|
91
|
+
studioSource: '.',
|
|
92
|
+
stackName,
|
|
93
|
+
});
|
|
94
|
+
log(styleText('bold', 'Changes:'));
|
|
95
|
+
for (const line of fileLines([
|
|
96
|
+
['+', 'sanity.blueprint.ts', "attach project + dataset; add CORS and Studio at '.'"],
|
|
97
|
+
['+', '.sanity/blueprint.config.json', `org scope, stack ${stackName}`],
|
|
98
|
+
[marker('.gitignore'), '.gitignore'],
|
|
99
|
+
[marker('package.json'), 'package.json'],
|
|
100
|
+
])) {
|
|
101
|
+
log(line);
|
|
102
|
+
}
|
|
103
|
+
log('');
|
|
104
|
+
const confirmed = await confirmUnlessSkipped(flags, isInteractive, 'Adopt this Studio into the Blueprint?');
|
|
105
|
+
if (!confirmed)
|
|
106
|
+
return { success: false, error: 'Initialization cancelled.' };
|
|
107
|
+
const adoption = adoptWorkspace({
|
|
108
|
+
dir,
|
|
109
|
+
projectId: match.project.id,
|
|
110
|
+
displayName: match.project.displayName,
|
|
111
|
+
datasetName,
|
|
112
|
+
stackName,
|
|
113
|
+
projectName,
|
|
114
|
+
studioName,
|
|
115
|
+
studioSlug: stackName,
|
|
116
|
+
studioOrigin,
|
|
117
|
+
blueprintsVersion: await blueprintsVersion,
|
|
118
|
+
});
|
|
119
|
+
return await createStackAndFinish({
|
|
120
|
+
...options,
|
|
121
|
+
organizationId: match.organization.id,
|
|
122
|
+
projectId: match.project.id,
|
|
123
|
+
datasetName,
|
|
124
|
+
stackName,
|
|
125
|
+
blueprintFilePath: adoption.blueprintFilePath,
|
|
126
|
+
files: adoption.files,
|
|
127
|
+
nextSteps: [
|
|
128
|
+
...alignSteps([
|
|
129
|
+
[`npx ${bin} blueprints plan`, `preview the ${attaches} attaches and ${creates} creates`],
|
|
130
|
+
[`npx ${bin} blueprints deploy`, 'attach and create them'],
|
|
131
|
+
[`npx ${bin} blueprints info`, 'review what the Stack owns'],
|
|
132
|
+
]),
|
|
133
|
+
'',
|
|
134
|
+
`Your Studio is hosted at ${studioOrigin} after deploy.`,
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { styleText } from '../../utils/style-text.js';
|
|
2
|
+
type TextStyle = Parameters<typeof styleText>[0];
|
|
3
|
+
/**
|
|
4
|
+
* One cell in an aligned row.
|
|
5
|
+
*
|
|
6
|
+
* `text` is the raw string. It sets the column width and is styled only after padding,
|
|
7
|
+
* so a colour code can never widen a column.
|
|
8
|
+
*/
|
|
9
|
+
export interface Cell {
|
|
10
|
+
text: string;
|
|
11
|
+
style?: TextStyle;
|
|
12
|
+
/**
|
|
13
|
+
* Spaces between this column and the next, beyond the padding. Defaults to 1.
|
|
14
|
+
*
|
|
15
|
+
* A gap is a property of the column, so one row may set it for every row.
|
|
16
|
+
*/
|
|
17
|
+
gap?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Render rows as columns of one shared width, then style each cell.
|
|
21
|
+
*
|
|
22
|
+
* The last cell in a row is never padded, so a row that stops early prints no trailing
|
|
23
|
+
* space. Pass every row that should line up in one call.
|
|
24
|
+
*/
|
|
25
|
+
export declare function renderRows(rows: Cell[][]): string[];
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { styleText } from '../../utils/style-text.js';
|
|
2
|
+
const INDENT = ' ';
|
|
3
|
+
/**
|
|
4
|
+
* Render rows as columns of one shared width, then style each cell.
|
|
5
|
+
*
|
|
6
|
+
* The last cell in a row is never padded, so a row that stops early prints no trailing
|
|
7
|
+
* space. Pass every row that should line up in one call.
|
|
8
|
+
*/
|
|
9
|
+
export function renderRows(rows) {
|
|
10
|
+
const widths = [];
|
|
11
|
+
const gaps = [];
|
|
12
|
+
for (const row of rows) {
|
|
13
|
+
row.forEach((cell, index) => {
|
|
14
|
+
widths[index] = Math.max(widths[index] ?? 0, cell.text.length);
|
|
15
|
+
gaps[index] = Math.max(gaps[index] ?? 1, cell.gap ?? 1);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return rows.map((row) => {
|
|
19
|
+
const cells = row.map((cell, index) => {
|
|
20
|
+
const styled = cell.style ? styleText(cell.style, cell.text) : cell.text;
|
|
21
|
+
if (index === row.length - 1)
|
|
22
|
+
return styled;
|
|
23
|
+
return styled + ' '.repeat(widths[index] - cell.text.length + gaps[index]);
|
|
24
|
+
});
|
|
25
|
+
return INDENT + cells.join('');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
/** A command to run next, and what it does. */
|
|
14
|
+
export declare function alignSteps(steps: Array<[command: string, description?: string]>): string[];
|
|
15
|
+
export {};
|
|
@@ -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,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
|
+
}
|