@sanity/runtime-cli 17.7.1 → 17.8.0-blueprinit.g0993fa5
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 +26 -21
- package/dist/actions/blueprinit/manifest.d.ts +34 -0
- package/dist/actions/blueprinit/manifest.js +107 -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/workspace.d.ts +24 -0
- package/dist/actions/blueprinit/workspace.js +146 -0
- package/dist/actions/blueprints/stacks.d.ts +2 -0
- package/dist/actions/blueprints/stacks.js +1 -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/init.d.ts +20 -0
- package/dist/commands/init.js +72 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +5 -0
- package/dist/cores/blueprinit/adopt.d.ts +6 -0
- package/dist/cores/blueprinit/adopt.js +154 -0
- package/dist/cores/blueprinit/finish.d.ts +15 -0
- package/dist/cores/blueprinit/finish.js +73 -0
- package/dist/cores/blueprinit/fresh.d.ts +3 -0
- package/dist/cores/blueprinit/fresh.js +173 -0
- package/dist/cores/blueprinit/preview.d.ts +24 -0
- package/dist/cores/blueprinit/preview.js +97 -0
- package/dist/cores/blueprinit/prompts.d.ts +15 -0
- package/dist/cores/blueprinit/prompts.js +43 -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 +18 -0
- package/dist/cores/blueprinit/types.js +1 -0
- package/dist/cores/blueprinit.d.ts +7 -0
- package/dist/cores/blueprinit.js +55 -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 +55 -111
- 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/external/npmjs.d.ts +7 -0
- package/dist/utils/external/npmjs.js +10 -0
- package/dist/utils/functions/child-process-wrapper.js +18 -6
- package/dist/utils/functions/child-runtime/durable.d.ts +18 -0
- package/dist/utils/functions/child-runtime/durable.js +46 -0
- package/dist/utils/functions/invoke/local.js +26 -9
- 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/progress.js +6 -3
- package/dist/utils/types.d.ts +46 -2
- package/oclif.manifest.json +155 -5
- package/package.json +1 -1
- package/skills/blueprints-stacks.md +14 -2
- package/skills/init.md +170 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { env } from 'node:process';
|
|
4
|
+
import { defaultStackName, hostedStudioOrigin, projectBaseName, projectResourceName, } 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 { getLatestNpmRange } from '../../utils/external/npmjs.js';
|
|
10
|
+
import { isInteractive } from '../../utils/interactive.js';
|
|
11
|
+
import { styleText } from '../../utils/style-text.js';
|
|
12
|
+
import { alignSteps, createStackAndFinish } from './finish.js';
|
|
13
|
+
import { blueprintPreviewCounts, fileLines, printBlueprintPreview } from './preview.js';
|
|
14
|
+
import { confirmUnlessSkipped, duplicateResourceNames, studioResourceName } from './prompts.js';
|
|
15
|
+
import { findProject } from './selection.js';
|
|
16
|
+
import { chooseStackName, takenStackNames } from './stack-name.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
|
+
// The Stack name is the hosted Studio slug, so one answer names both.
|
|
68
|
+
const stackNameResult = await chooseStackName({
|
|
69
|
+
flags,
|
|
70
|
+
interactive: isInteractive,
|
|
71
|
+
defaultName: defaultStackName(baseName, datasetName),
|
|
72
|
+
taken: await takenStackNames({ token, organizationId: match.organization.id, log }),
|
|
73
|
+
log,
|
|
74
|
+
});
|
|
75
|
+
if (!stackNameResult.ok) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: stackNameResult.error,
|
|
79
|
+
suggestions: stackNameResult.suggestions,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const stackName = stackNameResult.value;
|
|
83
|
+
const duplicates = duplicateResourceNames([
|
|
84
|
+
projectName,
|
|
85
|
+
datasetName,
|
|
86
|
+
`${studioName}-cors`,
|
|
87
|
+
studioName,
|
|
88
|
+
]);
|
|
89
|
+
if (duplicates.length > 0) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: `Blueprint resource names must be unique. Used twice: ${duplicates.join(', ')}.`,
|
|
93
|
+
suggestions: ['Pass --studio-name with a different value.'],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const studioOrigin = hostedStudioOrigin(stackName, env.SANITY_INTERNAL_ENV);
|
|
97
|
+
const marker = (file) => (existsSync(join(dir, file)) ? '~' : '+');
|
|
98
|
+
const previewOptions = {
|
|
99
|
+
log,
|
|
100
|
+
organizationName: match.organization.name,
|
|
101
|
+
projectId: match.project.id,
|
|
102
|
+
projectName: match.project.displayName,
|
|
103
|
+
datasetName,
|
|
104
|
+
studioName,
|
|
105
|
+
studioOrigin,
|
|
106
|
+
studioSource: '.',
|
|
107
|
+
stackName,
|
|
108
|
+
};
|
|
109
|
+
printBlueprintPreview(previewOptions);
|
|
110
|
+
const { attaches, creates } = blueprintPreviewCounts(previewOptions);
|
|
111
|
+
log(styleText('bold', 'Changes:'));
|
|
112
|
+
for (const line of fileLines([
|
|
113
|
+
['+', 'sanity.blueprint.ts', "attach project + dataset; add CORS and Studio at '.'"],
|
|
114
|
+
['+', '.sanity/blueprint.config.json', `org scope, stack ${stackName}`],
|
|
115
|
+
[marker('.gitignore'), '.gitignore'],
|
|
116
|
+
[marker('package.json'), 'package.json'],
|
|
117
|
+
])) {
|
|
118
|
+
log(line);
|
|
119
|
+
}
|
|
120
|
+
log('');
|
|
121
|
+
const confirmed = await confirmUnlessSkipped(flags, isInteractive, 'Adopt this Studio into the Blueprint?');
|
|
122
|
+
if (!confirmed)
|
|
123
|
+
return { success: false, error: 'Initialization cancelled.' };
|
|
124
|
+
const adoption = adoptWorkspace({
|
|
125
|
+
dir,
|
|
126
|
+
projectId: match.project.id,
|
|
127
|
+
displayName: match.project.displayName,
|
|
128
|
+
datasetName,
|
|
129
|
+
stackName,
|
|
130
|
+
projectName,
|
|
131
|
+
studioName,
|
|
132
|
+
studioSlug: stackName,
|
|
133
|
+
studioOrigin,
|
|
134
|
+
blueprintsVersion: await getLatestNpmRange('@sanity/blueprints', log),
|
|
135
|
+
});
|
|
136
|
+
return await createStackAndFinish({
|
|
137
|
+
...options,
|
|
138
|
+
organizationId: match.organization.id,
|
|
139
|
+
projectId: match.project.id,
|
|
140
|
+
datasetName,
|
|
141
|
+
stackName,
|
|
142
|
+
blueprintFilePath: adoption.blueprintFilePath,
|
|
143
|
+
files: adoption.files,
|
|
144
|
+
nextSteps: [
|
|
145
|
+
...alignSteps([
|
|
146
|
+
[`npx ${bin} blueprints plan`, `preview the ${attaches} attaches and ${creates} creates`],
|
|
147
|
+
[`npx ${bin} blueprints deploy`, 'attach and create them'],
|
|
148
|
+
[`npx ${bin} blueprints info`, 'review what the Stack owns'],
|
|
149
|
+
]),
|
|
150
|
+
'',
|
|
151
|
+
`Your Studio is hosted at ${studioOrigin} after deploy.`,
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -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
|
+
/** Pads on the raw command, then styles, so colour codes never widen a column. */
|
|
14
|
+
export declare function alignSteps(steps: Array<[command: string, description?: string]>): string[];
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
export async function createStackAndFinish(options) {
|
|
6
|
+
let stack;
|
|
7
|
+
try {
|
|
8
|
+
stack = await createEmptyStack({
|
|
9
|
+
token: options.token,
|
|
10
|
+
scopeType: 'organization',
|
|
11
|
+
scopeId: options.organizationId,
|
|
12
|
+
name: options.stackName,
|
|
13
|
+
logger: options.log,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
error: `Local files were written, but Stack creation failed: ${message}`,
|
|
21
|
+
suggestions: [
|
|
22
|
+
'Resolve the Stack error, then finish setup with:',
|
|
23
|
+
`npx ${options.bin} blueprints init --organization-id ${options.organizationId} --stack-name ${options.stackName}`,
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
writeConfigFile(options.blueprintFilePath, {
|
|
29
|
+
organizationId: options.organizationId,
|
|
30
|
+
stackId: stack.id,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
error: `Stack ${stack.id} was created, but the local Blueprint config could not be written: ${message}`,
|
|
38
|
+
suggestions: [
|
|
39
|
+
'Finish setup with:',
|
|
40
|
+
`npx ${options.bin} blueprints init --organization-id ${options.organizationId} --stack-id ${stack.id}`,
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
options.log('');
|
|
45
|
+
options.log(check(`Stack ${styleText('bold', stack.name)} created ${styleText('dim', '(org scope, nothing deployed yet)')}`));
|
|
46
|
+
options.log('');
|
|
47
|
+
options.log('');
|
|
48
|
+
options.log(styleText('bold', 'Next steps:'));
|
|
49
|
+
for (const step of options.nextSteps)
|
|
50
|
+
options.log(step);
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
json: {
|
|
54
|
+
organizationId: options.organizationId,
|
|
55
|
+
projectId: options.projectId,
|
|
56
|
+
dataset: options.datasetName,
|
|
57
|
+
stackId: stack.id,
|
|
58
|
+
stackName: stack.name,
|
|
59
|
+
files: [...options.files, '.sanity/blueprint.config.json'],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Pads on the raw command, then styles, so colour codes never widen a column. */
|
|
64
|
+
export function alignSteps(steps) {
|
|
65
|
+
const width = Math.max(...steps.map(([command]) => command.length));
|
|
66
|
+
return steps.map(([command, description]) => {
|
|
67
|
+
const styled = ` ${styleText('cyan', command)}`;
|
|
68
|
+
if (!description)
|
|
69
|
+
return styled;
|
|
70
|
+
const gap = ' '.repeat(width - command.length + 2);
|
|
71
|
+
return `${styled}${gap}${styleText('dim', description)}`;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { env } from 'node:process';
|
|
3
|
+
import { defaultStackName, hostedStudioOrigin, projectBaseName, projectResourceName, STARTER_FUNCTION_NAME, } from '../../actions/blueprinit/manifest.js';
|
|
4
|
+
import { scaffoldWorkspace } from '../../actions/blueprinit/workspace.js';
|
|
5
|
+
import { listDatasets } from '../../actions/sanity/datasets.js';
|
|
6
|
+
import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
|
|
7
|
+
import { check } from '../../utils/display/presenters.js';
|
|
8
|
+
import { getLatestNpmRange } from '../../utils/external/npmjs.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 { blueprintPreviewCounts, fileLines, printBlueprintPreview } from './preview.js';
|
|
13
|
+
import { confirmUnlessSkipped, duplicateResourceNames, studioResourceName } from './prompts.js';
|
|
14
|
+
import { chooseDataset, chooseOrganization, chooseProject } from './selection.js';
|
|
15
|
+
import { chooseStackName, takenStackNames } from './stack-name.js';
|
|
16
|
+
import { promptForTargetDirectory, relativeTargetPath, resolveTargetDirectory } from './target.js';
|
|
17
|
+
export async function freshFlow(options) {
|
|
18
|
+
const { bin, dir, flags, log, token } = options;
|
|
19
|
+
const grouped = await groupProjectsByOrganization({ token, logger: log });
|
|
20
|
+
if (!grouped.ok) {
|
|
21
|
+
return { success: false, error: `Could not list organizations: ${grouped.error}` };
|
|
22
|
+
}
|
|
23
|
+
const organizationResult = await chooseOrganization(grouped.organizations, flags.organizationId, isInteractive);
|
|
24
|
+
if (!organizationResult.ok) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: organizationResult.error.message,
|
|
28
|
+
suggestions: organizationResult.error.suggestions,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const { organization, projects = [] } = organizationResult.value;
|
|
32
|
+
const projectResult = await chooseProject(projects, flags.projectId, organization.name, isInteractive);
|
|
33
|
+
if (!projectResult.ok) {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error: projectResult.error.message,
|
|
37
|
+
suggestions: projectResult.error.suggestions,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const project = projectResult.value;
|
|
41
|
+
const datasets = await listDatasets({ token, projectId: project.id, logger: log });
|
|
42
|
+
if (!datasets.ok) {
|
|
43
|
+
return { success: false, error: `Could not list datasets: ${datasets.error}` };
|
|
44
|
+
}
|
|
45
|
+
const datasetResult = await chooseDataset(datasets.datasets, flags.datasetName, project.displayName, isInteractive);
|
|
46
|
+
if (!datasetResult.ok) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: datasetResult.error.message,
|
|
50
|
+
suggestions: datasetResult.error.suggestions,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const dataset = datasetResult.value;
|
|
54
|
+
log(check(`Org: ${organization.name} (${organization.id})`));
|
|
55
|
+
log(check(`Project: ${project.displayName} (${project.id}), dataset: ${dataset.name}`));
|
|
56
|
+
log('');
|
|
57
|
+
const baseName = projectBaseName(project.displayName, project.id);
|
|
58
|
+
const projectName = projectResourceName(baseName);
|
|
59
|
+
const studioName = studioResourceName(flags);
|
|
60
|
+
const target = options.targetProvided
|
|
61
|
+
? { ok: true, value: dir }
|
|
62
|
+
: resolveTargetDirectory(dir, await promptForTargetDirectory(dir, baseName, isInteractive && !flags.yes), bin);
|
|
63
|
+
if (!target.ok) {
|
|
64
|
+
return { success: false, error: target.error.message, suggestions: target.error.suggestions };
|
|
65
|
+
}
|
|
66
|
+
const targetDir = target.value;
|
|
67
|
+
// The Stack name is the hosted Studio slug, so one answer names both.
|
|
68
|
+
const stackNameResult = await chooseStackName({
|
|
69
|
+
flags,
|
|
70
|
+
interactive: isInteractive,
|
|
71
|
+
defaultName: defaultStackName(baseName, dataset.name),
|
|
72
|
+
taken: await takenStackNames({ token, organizationId: organization.id, log }),
|
|
73
|
+
log,
|
|
74
|
+
});
|
|
75
|
+
if (!stackNameResult.ok) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: stackNameResult.error,
|
|
79
|
+
suggestions: stackNameResult.suggestions,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const stackName = stackNameResult.value;
|
|
83
|
+
const duplicates = duplicateResourceNames([
|
|
84
|
+
projectName,
|
|
85
|
+
dataset.name,
|
|
86
|
+
`${studioName}-cors`,
|
|
87
|
+
studioName,
|
|
88
|
+
STARTER_FUNCTION_NAME,
|
|
89
|
+
]);
|
|
90
|
+
if (duplicates.length > 0) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: `Blueprint resource names must be unique. Used twice: ${duplicates.join(', ')}.`,
|
|
94
|
+
suggestions: ['Pass --studio-name with a different value.'],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const studioOrigin = hostedStudioOrigin(stackName, env.SANITY_INTERNAL_ENV);
|
|
98
|
+
const previewOptions = {
|
|
99
|
+
log,
|
|
100
|
+
organizationName: organization.name,
|
|
101
|
+
projectId: project.id,
|
|
102
|
+
projectName: project.displayName,
|
|
103
|
+
datasetName: dataset.name,
|
|
104
|
+
studioName,
|
|
105
|
+
studioOrigin,
|
|
106
|
+
studioSource: `studios/${studioName}`,
|
|
107
|
+
stackName,
|
|
108
|
+
functionName: STARTER_FUNCTION_NAME,
|
|
109
|
+
};
|
|
110
|
+
printBlueprintPreview(previewOptions);
|
|
111
|
+
const { attaches, creates } = blueprintPreviewCounts(previewOptions);
|
|
112
|
+
log(styleText('bold', 'Files to write:'));
|
|
113
|
+
for (const line of fileLines([
|
|
114
|
+
['+', 'sanity.blueprint.ts', 'project + dataset (attached), CORS, studio, function'],
|
|
115
|
+
['+', `studios/${studioName}/`, 'Studio with a "post" type'],
|
|
116
|
+
['+', `functions/${STARTER_FUNCTION_NAME}/`, 'sets a post slug from its title'],
|
|
117
|
+
['+', '.sanity/blueprint.config.json', `org scope, stack ${stackName}`],
|
|
118
|
+
['+', '.gitignore'],
|
|
119
|
+
['+', 'package.json'],
|
|
120
|
+
])) {
|
|
121
|
+
log(line);
|
|
122
|
+
}
|
|
123
|
+
log('');
|
|
124
|
+
const confirmed = await confirmUnlessSkipped(flags, isInteractive, `Create this Blueprint workspace for "${project.displayName}"?`);
|
|
125
|
+
if (!confirmed)
|
|
126
|
+
return { success: false, error: 'Initialization cancelled.' };
|
|
127
|
+
const relativeDir = relativeTargetPath(targetDir);
|
|
128
|
+
const [blueprintsVersion, sanityVersion, visionVersion] = await Promise.all([
|
|
129
|
+
getLatestNpmRange('@sanity/blueprints', log),
|
|
130
|
+
getLatestNpmRange('sanity', log),
|
|
131
|
+
getLatestNpmRange('@sanity/vision', log),
|
|
132
|
+
]);
|
|
133
|
+
mkdirSync(targetDir, { recursive: true });
|
|
134
|
+
const scaffold = scaffoldWorkspace({
|
|
135
|
+
dir: targetDir,
|
|
136
|
+
projectId: project.id,
|
|
137
|
+
displayName: project.displayName,
|
|
138
|
+
datasetName: dataset.name,
|
|
139
|
+
stackName,
|
|
140
|
+
projectName,
|
|
141
|
+
studioName,
|
|
142
|
+
studioSlug: stackName,
|
|
143
|
+
studioOrigin,
|
|
144
|
+
blueprintsVersion,
|
|
145
|
+
studioVersions: { sanity: sanityVersion, vision: visionVersion },
|
|
146
|
+
});
|
|
147
|
+
return await createStackAndFinish({
|
|
148
|
+
...options,
|
|
149
|
+
organizationId: organization.id,
|
|
150
|
+
projectId: project.id,
|
|
151
|
+
datasetName: dataset.name,
|
|
152
|
+
stackName,
|
|
153
|
+
blueprintFilePath: scaffold.blueprintFilePath,
|
|
154
|
+
files: scaffold.files,
|
|
155
|
+
nextSteps: [
|
|
156
|
+
...alignSteps([
|
|
157
|
+
...(relativeDir ? [[`cd ${relativeDir}`]] : []),
|
|
158
|
+
['npm install'],
|
|
159
|
+
[`npm run dev -w studios/${studioName}`, 'open the Studio and add a post'],
|
|
160
|
+
[`npx ${bin} blueprints plan`, `preview the ${attaches} attaches and ${creates} creates`],
|
|
161
|
+
[`npx ${bin} blueprints deploy`, 'attach and create them'],
|
|
162
|
+
[`npx ${bin} blueprints info`, 'review what the Stack owns'],
|
|
163
|
+
]),
|
|
164
|
+
'',
|
|
165
|
+
`Your Studio is hosted at ${studioOrigin} after deploy.`,
|
|
166
|
+
'',
|
|
167
|
+
`${STARTER_FUNCTION_NAME} fills in a post's slug. Try it on a post you added, then follow the deployed one:`,
|
|
168
|
+
` npx ${bin} functions test ${STARTER_FUNCTION_NAME} --document-id <id>` +
|
|
169
|
+
` --project-id ${project.id} --dataset ${dataset.name} --with-user-token`,
|
|
170
|
+
` npx ${bin} functions logs ${STARTER_FUNCTION_NAME}`,
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
/** The counts `blueprints plan` will report for this Blueprint. */
|
|
15
|
+
export declare function blueprintPreviewCounts(options: BlueprintPreviewOptions): {
|
|
16
|
+
attaches: number;
|
|
17
|
+
creates: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function printBlueprintPreview(options: BlueprintPreviewOptions): void;
|
|
20
|
+
/** A local change: a marker, the path, and what the file is for. */
|
|
21
|
+
export type FileLine = [marker: '+' | '~', path: string, description?: string];
|
|
22
|
+
/** Pads on the raw path, then styles, so colour codes never widen a column. */
|
|
23
|
+
export declare function fileLines(lines: FileLine[]): string[];
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { styleText } from '../../utils/style-text.js';
|
|
2
|
+
/** The icon and colour `blueprints plan` uses for the same action. */
|
|
3
|
+
const ACTION_STYLE = {
|
|
4
|
+
attach: { icon: '←', color: 'blue' },
|
|
5
|
+
create: { icon: '+', color: 'green' },
|
|
6
|
+
};
|
|
7
|
+
const RETAIN = 'on deploy; retain on Stack removal';
|
|
8
|
+
function previewResources(options) {
|
|
9
|
+
const resources = [
|
|
10
|
+
{
|
|
11
|
+
action: 'attach',
|
|
12
|
+
kind: 'Project',
|
|
13
|
+
name: `${options.projectName} (${options.projectId})`,
|
|
14
|
+
detail: RETAIN,
|
|
15
|
+
},
|
|
16
|
+
{ action: 'attach', kind: 'Dataset', name: options.datasetName, detail: RETAIN },
|
|
17
|
+
{
|
|
18
|
+
action: 'create',
|
|
19
|
+
kind: 'CORS Origin',
|
|
20
|
+
name: `${options.studioName}-cors`,
|
|
21
|
+
detail: `for ${options.studioOrigin} on deploy`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
action: 'create',
|
|
25
|
+
kind: 'Studio',
|
|
26
|
+
name: options.studioName,
|
|
27
|
+
// The origin is on the CORS line directly above, so it is not repeated here.
|
|
28
|
+
detail: `from ${options.studioSource} on deploy`,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
if (options.functionName) {
|
|
32
|
+
resources.push({
|
|
33
|
+
action: 'create',
|
|
34
|
+
kind: 'Function',
|
|
35
|
+
name: options.functionName,
|
|
36
|
+
detail: 'on deploy',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return resources;
|
|
40
|
+
}
|
|
41
|
+
/** The counts `blueprints plan` will report for this Blueprint. */
|
|
42
|
+
export function blueprintPreviewCounts(options) {
|
|
43
|
+
const resources = previewResources(options);
|
|
44
|
+
return {
|
|
45
|
+
attaches: resources.filter(({ action }) => action === 'attach').length,
|
|
46
|
+
creates: resources.filter(({ action }) => action === 'create').length,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Pads on the raw text, then styles, so colour codes never widen a column. */
|
|
50
|
+
function actionLine(resource, pad) {
|
|
51
|
+
const { icon, color } = ACTION_STYLE[resource.action];
|
|
52
|
+
const gap = ' '.repeat(pad.name - resource.name.length + 2);
|
|
53
|
+
return [
|
|
54
|
+
` ${styleText(['bold', color], icon)} `,
|
|
55
|
+
`${styleText(color, resource.action.padEnd(pad.action))} `,
|
|
56
|
+
`${resource.kind.padEnd(pad.kind)} `,
|
|
57
|
+
`${styleText('bold', resource.name)}${gap}`,
|
|
58
|
+
styleText('dim', resource.detail),
|
|
59
|
+
].join('');
|
|
60
|
+
}
|
|
61
|
+
function columnWidths(resources) {
|
|
62
|
+
return {
|
|
63
|
+
action: Math.max(...resources.map(({ action }) => action.length)),
|
|
64
|
+
kind: Math.max(...resources.map(({ kind }) => kind.length)),
|
|
65
|
+
name: Math.max(...resources.map(({ name }) => name.length)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function printBlueprintPreview(options) {
|
|
69
|
+
const resources = previewResources(options);
|
|
70
|
+
const stack = {
|
|
71
|
+
action: 'create',
|
|
72
|
+
kind: 'Stack',
|
|
73
|
+
name: options.stackName,
|
|
74
|
+
detail: `in ${options.organizationName} (empty, organization scope)`,
|
|
75
|
+
};
|
|
76
|
+
// One set of widths across both blocks, so the two read as one table.
|
|
77
|
+
const pad = columnWidths([...resources, stack]);
|
|
78
|
+
options.log(styleText('bold', 'Blueprint plan:'));
|
|
79
|
+
for (const resource of resources)
|
|
80
|
+
options.log(actionLine(resource, pad));
|
|
81
|
+
options.log('');
|
|
82
|
+
options.log(styleText('bold', 'One remote change now:'));
|
|
83
|
+
options.log(actionLine(stack, pad));
|
|
84
|
+
options.log('');
|
|
85
|
+
}
|
|
86
|
+
/** Pads on the raw path, then styles, so colour codes never widen a column. */
|
|
87
|
+
export function fileLines(lines) {
|
|
88
|
+
const width = Math.max(...lines.map(([, path]) => path.length));
|
|
89
|
+
return lines.map(([marker, path, description]) => {
|
|
90
|
+
const color = marker === '+' ? 'green' : 'yellow';
|
|
91
|
+
const head = ` ${styleText(['bold', color], marker)} ${styleText('bold', path)}`;
|
|
92
|
+
if (!description)
|
|
93
|
+
return head;
|
|
94
|
+
const gap = ' '.repeat(width - path.length + 3);
|
|
95
|
+
return `${head}${gap}${styleText('dim', description)}`;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DEFAULT_STUDIO_NAME } from '../../actions/blueprinit/manifest.js';
|
|
2
|
+
import type { BlueprinitFlags } from './types.js';
|
|
3
|
+
export { DEFAULT_STUDIO_NAME };
|
|
4
|
+
/**
|
|
5
|
+
* Resource names must be unique within a Blueprint, so report every name used twice.
|
|
6
|
+
*
|
|
7
|
+
* The generated names cannot collide with each other, because each one carries its
|
|
8
|
+
* resource type. A dataset name or a `--studio-name` value can still land on one.
|
|
9
|
+
*/
|
|
10
|
+
export declare function duplicateResourceNames(names: string[]): string[];
|
|
11
|
+
export declare function confirmUnlessSkipped(flags: BlueprinitFlags, interactive: boolean, message: string): Promise<boolean>;
|
|
12
|
+
/** The new Stack's name, which is also the hosted Studio slug. */
|
|
13
|
+
export declare function promptForStackName(flags: BlueprinitFlags, interactive: boolean, defaultName: string): Promise<string>;
|
|
14
|
+
/** The Blueprint-local Studio name, which is also its directory under `studios/`. */
|
|
15
|
+
export declare function studioResourceName(flags: BlueprinitFlags): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
2
|
+
import { DEFAULT_STUDIO_NAME, slugify } from '../../actions/blueprinit/manifest.js';
|
|
3
|
+
export { DEFAULT_STUDIO_NAME };
|
|
4
|
+
/**
|
|
5
|
+
* Resource names must be unique within a Blueprint, so report every name used twice.
|
|
6
|
+
*
|
|
7
|
+
* The generated names cannot collide with each other, because each one carries its
|
|
8
|
+
* resource type. A dataset name or a `--studio-name` value can still land on one.
|
|
9
|
+
*/
|
|
10
|
+
export function duplicateResourceNames(names) {
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const duplicates = new Set();
|
|
13
|
+
for (const name of names) {
|
|
14
|
+
if (seen.has(name))
|
|
15
|
+
duplicates.add(name);
|
|
16
|
+
else
|
|
17
|
+
seen.add(name);
|
|
18
|
+
}
|
|
19
|
+
return [...duplicates];
|
|
20
|
+
}
|
|
21
|
+
export async function confirmUnlessSkipped(flags, interactive, message) {
|
|
22
|
+
if (flags.yes || !interactive)
|
|
23
|
+
return true;
|
|
24
|
+
return await confirm({ message, default: true });
|
|
25
|
+
}
|
|
26
|
+
/** The new Stack's name, which is also the hosted Studio slug. */
|
|
27
|
+
export async function promptForStackName(flags, interactive, defaultName) {
|
|
28
|
+
if (flags.stackName)
|
|
29
|
+
return slugify(flags.stackName) || defaultName;
|
|
30
|
+
if (flags.yes || !interactive)
|
|
31
|
+
return defaultName;
|
|
32
|
+
const answer = await input({
|
|
33
|
+
message: 'Name your Stack and hosted Studio?',
|
|
34
|
+
default: defaultName,
|
|
35
|
+
});
|
|
36
|
+
return slugify(answer) || defaultName;
|
|
37
|
+
}
|
|
38
|
+
/** The Blueprint-local Studio name, which is also its directory under `studios/`. */
|
|
39
|
+
export function studioResourceName(flags) {
|
|
40
|
+
if (!flags.studioName)
|
|
41
|
+
return DEFAULT_STUDIO_NAME;
|
|
42
|
+
return slugify(flags.studioName) || DEFAULT_STUDIO_NAME;
|
|
43
|
+
}
|
|
@@ -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 {};
|