@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,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;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
2
|
+
import { DEFAULT_STUDIO_NAME, slugify } from '../../actions/blueprinit/manifest.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resource names must be unique within a Blueprint, so report every name used twice.
|
|
5
|
+
*
|
|
6
|
+
* The generated names cannot collide with each other, because each one carries its
|
|
7
|
+
* resource type. A dataset name or a `--studio-name` value can still land on one.
|
|
8
|
+
*/
|
|
9
|
+
export function duplicateResourceNames(names) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
const duplicates = new Set();
|
|
12
|
+
for (const name of names) {
|
|
13
|
+
if (seen.has(name))
|
|
14
|
+
duplicates.add(name);
|
|
15
|
+
else
|
|
16
|
+
seen.add(name);
|
|
17
|
+
}
|
|
18
|
+
return [...duplicates];
|
|
19
|
+
}
|
|
20
|
+
export async function confirmUnlessSkipped(flags, interactive, message) {
|
|
21
|
+
if (flags.yes || !interactive)
|
|
22
|
+
return true;
|
|
23
|
+
return await confirm({ message, default: true });
|
|
24
|
+
}
|
|
25
|
+
/** The new Stack's name, which is also the hosted Studio slug. */
|
|
26
|
+
export async function promptForStackName(flags, interactive, defaultName) {
|
|
27
|
+
if (flags.stackName)
|
|
28
|
+
return slugify(flags.stackName) || defaultName;
|
|
29
|
+
if (flags.yes || !interactive)
|
|
30
|
+
return defaultName;
|
|
31
|
+
const answer = await input({
|
|
32
|
+
message: 'Name your Stack and hosted Studio?',
|
|
33
|
+
default: defaultName,
|
|
34
|
+
});
|
|
35
|
+
return slugify(answer) || defaultName;
|
|
36
|
+
}
|
|
37
|
+
/** The Blueprint-local Studio name, which is also its directory under `studios/`. */
|
|
38
|
+
export function studioResourceName(flags) {
|
|
39
|
+
if (!flags.studioName)
|
|
40
|
+
return DEFAULT_STUDIO_NAME;
|
|
41
|
+
return slugify(flags.studioName) || DEFAULT_STUDIO_NAME;
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CoreResult } from '../index.js';
|
|
2
|
+
/** A failed step, in the shape every helper in this flow reports. */
|
|
3
|
+
export interface FlowError {
|
|
4
|
+
message: string;
|
|
5
|
+
suggestions?: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Turn a failed step into the result the command returns. */
|
|
8
|
+
export declare function coreFailure(error: FlowError): CoreResult;
|
|
@@ -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 {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
export async function chooseOrganization(groups, organizationId, interactive) {
|
|
3
|
+
if (groups.length === 0) {
|
|
4
|
+
return failure('No organizations found for this account.');
|
|
5
|
+
}
|
|
6
|
+
if (organizationId) {
|
|
7
|
+
const organization = groups.find((group) => group.organization.id === organizationId);
|
|
8
|
+
return organization
|
|
9
|
+
? { ok: true, value: organization }
|
|
10
|
+
: failure(`No organization "${organizationId}" found for this account.`);
|
|
11
|
+
}
|
|
12
|
+
if (groups.length === 1)
|
|
13
|
+
return { ok: true, value: groups[0] };
|
|
14
|
+
if (!interactive) {
|
|
15
|
+
return failure('Multiple organizations found.', ['Pass --organization-id <id> to choose one.']);
|
|
16
|
+
}
|
|
17
|
+
const selectedId = await select({
|
|
18
|
+
message: 'Choose a Sanity organization:',
|
|
19
|
+
choices: groups.map(({ organization, projects }) => ({
|
|
20
|
+
name: organization.name,
|
|
21
|
+
value: organization.id,
|
|
22
|
+
description: `${projects?.length ?? 0} projects`,
|
|
23
|
+
})),
|
|
24
|
+
});
|
|
25
|
+
const selected = groups.find(({ organization }) => organization.id === selectedId);
|
|
26
|
+
return selected ? { ok: true, value: selected } : failure('Organization not found.');
|
|
27
|
+
}
|
|
28
|
+
export async function chooseProject(projects, projectId, organizationName, interactive) {
|
|
29
|
+
if (projects.length === 0) {
|
|
30
|
+
return failure(`No projects found in ${organizationName}.`);
|
|
31
|
+
}
|
|
32
|
+
if (projectId) {
|
|
33
|
+
const project = projects.find(({ id }) => id === projectId);
|
|
34
|
+
return project
|
|
35
|
+
? { ok: true, value: project }
|
|
36
|
+
: failure(`No project "${projectId}" found in ${organizationName}.`);
|
|
37
|
+
}
|
|
38
|
+
if (projects.length === 1)
|
|
39
|
+
return { ok: true, value: projects[0] };
|
|
40
|
+
if (!interactive) {
|
|
41
|
+
return failure('Multiple projects found.', ['Pass --project-id <id> to choose one.']);
|
|
42
|
+
}
|
|
43
|
+
const selectedId = await select({
|
|
44
|
+
message: 'Choose a Sanity project:',
|
|
45
|
+
choices: projects.map((project) => ({ name: project.displayName, value: project.id })),
|
|
46
|
+
});
|
|
47
|
+
const selected = projects.find(({ id }) => id === selectedId);
|
|
48
|
+
return selected ? { ok: true, value: selected } : failure('Project not found.');
|
|
49
|
+
}
|
|
50
|
+
export async function chooseDataset(datasets, datasetName, projectName, interactive) {
|
|
51
|
+
if (datasets.length === 0) {
|
|
52
|
+
return failure(`No datasets found in ${projectName}.`);
|
|
53
|
+
}
|
|
54
|
+
if (datasetName) {
|
|
55
|
+
const dataset = datasets.find(({ name }) => name === datasetName);
|
|
56
|
+
return dataset
|
|
57
|
+
? { ok: true, value: dataset }
|
|
58
|
+
: failure(`No dataset "${datasetName}" found in ${projectName}.`);
|
|
59
|
+
}
|
|
60
|
+
if (datasets.length === 1)
|
|
61
|
+
return { ok: true, value: datasets[0] };
|
|
62
|
+
if (!interactive) {
|
|
63
|
+
return failure('Multiple datasets found.', ['Pass --dataset <name> to choose one.']);
|
|
64
|
+
}
|
|
65
|
+
const production = datasets.find(({ name }) => name === 'production');
|
|
66
|
+
const orderedDatasets = production
|
|
67
|
+
? [production, ...datasets.filter(({ name }) => name !== 'production')]
|
|
68
|
+
: datasets;
|
|
69
|
+
const selectedName = await select({
|
|
70
|
+
message: 'Choose a Sanity dataset:',
|
|
71
|
+
choices: orderedDatasets.map((dataset) => ({
|
|
72
|
+
name: dataset.name,
|
|
73
|
+
value: dataset.name,
|
|
74
|
+
description: dataset.aclMode,
|
|
75
|
+
})),
|
|
76
|
+
});
|
|
77
|
+
const selected = datasets.find(({ name }) => name === selectedName);
|
|
78
|
+
return selected ? { ok: true, value: selected } : failure('Dataset not found.');
|
|
79
|
+
}
|
|
80
|
+
export function findProject(groups, projectId) {
|
|
81
|
+
for (const { organization, projects = [] } of groups) {
|
|
82
|
+
const project = projects.find(({ id }) => id === projectId);
|
|
83
|
+
if (project)
|
|
84
|
+
return { organization, project };
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function failure(message, suggestions) {
|
|
89
|
+
return { ok: false, error: { message, suggestions } };
|
|
90
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Logger } from '../../utils/logger.js';
|
|
2
|
+
import type { BlueprinitFlags } from './types.js';
|
|
3
|
+
interface TakenNamesOptions {
|
|
4
|
+
token: string;
|
|
5
|
+
organizationId: string;
|
|
6
|
+
log: Logger;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Stack names already used in the organization.
|
|
10
|
+
*
|
|
11
|
+
* Returns nothing when the listing fails. A token that cannot read Stacks is not a reason
|
|
12
|
+
* to stop, because the create call reports the same conflict a moment later.
|
|
13
|
+
*/
|
|
14
|
+
export declare function takenStackNames(options: TakenNamesOptions): Promise<Set<string>>;
|
|
15
|
+
interface ChooseStackNameOptions {
|
|
16
|
+
flags: BlueprinitFlags;
|
|
17
|
+
interactive: boolean;
|
|
18
|
+
defaultName: string;
|
|
19
|
+
taken: Set<string>;
|
|
20
|
+
log: Logger;
|
|
21
|
+
}
|
|
22
|
+
type ChooseStackNameResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
value: string;
|
|
25
|
+
} | {
|
|
26
|
+
ok: false;
|
|
27
|
+
error: string;
|
|
28
|
+
suggestions: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* A Stack name the organization does not already hold.
|
|
32
|
+
*
|
|
33
|
+
* Asks again rather than failing, because the conflict is only visible after `init` has
|
|
34
|
+
* written every local file.
|
|
35
|
+
*/
|
|
36
|
+
export declare function chooseStackName(options: ChooseStackNameOptions): Promise<ChooseStackNameResult>;
|
|
37
|
+
/** `field-notes-production`, then `field-notes-production-2`, until one is free. */
|
|
38
|
+
export declare function nextFreeName(name: string, taken: Set<string>): string;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { listStacks } from '../../actions/blueprints/stacks.js';
|
|
2
|
+
import { warn } from '../../utils/display/presenters.js';
|
|
3
|
+
import { promptForStackName } from './prompts.js';
|
|
4
|
+
/**
|
|
5
|
+
* Stack names already used in the organization.
|
|
6
|
+
*
|
|
7
|
+
* Returns nothing when the listing fails. A token that cannot read Stacks is not a reason
|
|
8
|
+
* to stop, because the create call reports the same conflict a moment later.
|
|
9
|
+
*/
|
|
10
|
+
export async function takenStackNames(options) {
|
|
11
|
+
const { token, organizationId, log } = options;
|
|
12
|
+
const result = await listStacks({ token, scopeType: 'organization', scopeId: organizationId }, log);
|
|
13
|
+
if (!result.ok) {
|
|
14
|
+
log.verbose(`Could not list Stacks to check the name: ${result.error}`);
|
|
15
|
+
return new Set();
|
|
16
|
+
}
|
|
17
|
+
return new Set(result.stacks.map(({ name }) => name));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A Stack name the organization does not already hold.
|
|
21
|
+
*
|
|
22
|
+
* Asks again rather than failing, because the conflict is only visible after `init` has
|
|
23
|
+
* written every local file.
|
|
24
|
+
*/
|
|
25
|
+
export async function chooseStackName(options) {
|
|
26
|
+
const { flags, interactive, defaultName, taken, log } = options;
|
|
27
|
+
const suggested = nextFreeName(defaultName, taken);
|
|
28
|
+
if (suggested !== defaultName) {
|
|
29
|
+
log(warn(`A Stack named "${defaultName}" already exists. Suggesting "${suggested}".`));
|
|
30
|
+
}
|
|
31
|
+
let name = await promptForStackName(flags, interactive, suggested);
|
|
32
|
+
while (taken.has(name)) {
|
|
33
|
+
// Only a --stack-name value can be taken here; every other path starts from a free name.
|
|
34
|
+
if (!interactive || flags.yes || flags.stackName) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
error: `A Stack named "${name}" already exists in this organization.`,
|
|
38
|
+
suggestions: [`Pass --stack-name with a different value, such as ${suggested}.`],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
log(warn(`A Stack named "${name}" already exists in this organization.`));
|
|
42
|
+
name = await promptForStackName({}, interactive, nextFreeName(name, taken));
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, value: name };
|
|
45
|
+
}
|
|
46
|
+
/** `field-notes-production`, then `field-notes-production-2`, until one is free. */
|
|
47
|
+
export function nextFreeName(name, taken) {
|
|
48
|
+
if (!taken.has(name))
|
|
49
|
+
return name;
|
|
50
|
+
for (let suffix = 2; suffix < 100; suffix += 1) {
|
|
51
|
+
const candidate = `${name}-${suffix}`;
|
|
52
|
+
if (!taken.has(candidate))
|
|
53
|
+
return candidate;
|
|
54
|
+
}
|
|
55
|
+
return name;
|
|
56
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Result } from '../../utils/types.js';
|
|
2
|
+
interface TargetError {
|
|
3
|
+
message: string;
|
|
4
|
+
suggestions?: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function isEmptyDirectory(dir: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* What the directory prompt offers.
|
|
9
|
+
*
|
|
10
|
+
* The current directory when it is free, and a new directory named for the project when it
|
|
11
|
+
* is not, so accepting the default never writes a scaffold beside unrelated files.
|
|
12
|
+
*/
|
|
13
|
+
export declare function defaultTargetPath(dir: string, baseName: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Where to scaffold, asked rather than assumed.
|
|
16
|
+
*
|
|
17
|
+
* An unattended run keeps the current directory. Relocating a scaffold with nobody watching
|
|
18
|
+
* would move files a script expects to find where it started.
|
|
19
|
+
*/
|
|
20
|
+
export declare function promptForTargetDirectory(dir: string, baseName: string, interactive: boolean): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the answer against the starting directory and refuse a target that is already
|
|
23
|
+
* something else: a file, a Blueprint, or a Studio that adoption should handle instead.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveTargetDirectory(dir: string, answer: string, bin: string): Result<string, TargetError>;
|
|
26
|
+
/**
|
|
27
|
+
* The `cd` the printed next steps need, or nothing when the files landed here.
|
|
28
|
+
*
|
|
29
|
+
* Measured from the process working directory, not from the flow's starting directory,
|
|
30
|
+
* because a directory argument moves the target without moving the reader.
|
|
31
|
+
*/
|
|
32
|
+
export declare function relativeTargetPath(target: string, from?: string): string | undefined;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { cwd } from 'node:process';
|
|
4
|
+
import { input } from '@inquirer/prompts';
|
|
5
|
+
import { detectStudio } from '../../actions/blueprinit/studio.js';
|
|
6
|
+
import { findBlueprintFile } from '../../actions/blueprints/blueprint.js';
|
|
7
|
+
/** Entries that say nothing about whether a directory is in use. */
|
|
8
|
+
const IGNORED_ENTRIES = new Set(['.git', '.DS_Store']);
|
|
9
|
+
export function isEmptyDirectory(dir) {
|
|
10
|
+
try {
|
|
11
|
+
return readdirSync(dir).every((entry) => IGNORED_ENTRIES.has(entry));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// A directory that does not exist yet has nothing to overwrite.
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What the directory prompt offers.
|
|
20
|
+
*
|
|
21
|
+
* The current directory when it is free, and a new directory named for the project when it
|
|
22
|
+
* is not, so accepting the default never writes a scaffold beside unrelated files.
|
|
23
|
+
*/
|
|
24
|
+
export function defaultTargetPath(dir, baseName) {
|
|
25
|
+
return isEmptyDirectory(dir) ? '.' : `./${baseName}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Where to scaffold, asked rather than assumed.
|
|
29
|
+
*
|
|
30
|
+
* An unattended run keeps the current directory. Relocating a scaffold with nobody watching
|
|
31
|
+
* would move files a script expects to find where it started.
|
|
32
|
+
*/
|
|
33
|
+
export async function promptForTargetDirectory(dir, baseName, interactive) {
|
|
34
|
+
if (!interactive)
|
|
35
|
+
return '.';
|
|
36
|
+
const fallback = defaultTargetPath(dir, baseName);
|
|
37
|
+
const answer = await input({ message: 'Where should the files go?', default: fallback });
|
|
38
|
+
return answer.trim() || fallback;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the answer against the starting directory and refuse a target that is already
|
|
42
|
+
* something else: a file, a Blueprint, or a Studio that adoption should handle instead.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveTargetDirectory(dir, answer, bin) {
|
|
45
|
+
const target = isAbsolute(answer) ? answer : resolve(dir, answer);
|
|
46
|
+
if (existsSync(target) && !statSync(target).isDirectory()) {
|
|
47
|
+
return { ok: false, error: { message: `${answer} is not a directory.` } };
|
|
48
|
+
}
|
|
49
|
+
const blueprint = findBlueprintFile(target);
|
|
50
|
+
if (blueprint) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
error: {
|
|
54
|
+
message: `A Blueprint already exists in ${answer} (${blueprint.fileName}).`,
|
|
55
|
+
suggestions: [`Review it with: npx ${bin} blueprints doctor`],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const studio = detectStudio(target);
|
|
60
|
+
if (studio) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: {
|
|
64
|
+
message: `${answer} already holds a Studio (${studio.configFile}).`,
|
|
65
|
+
suggestions: [`Wrap it in a Blueprint with: npx ${bin} init ${answer}`],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, value: target };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The `cd` the printed next steps need, or nothing when the files landed here.
|
|
73
|
+
*
|
|
74
|
+
* Measured from the process working directory, not from the flow's starting directory,
|
|
75
|
+
* because a directory argument moves the target without moving the reader.
|
|
76
|
+
*/
|
|
77
|
+
export function relativeTargetPath(target, from = cwd()) {
|
|
78
|
+
const path = relative(from, target);
|
|
79
|
+
return path === '' ? undefined : path;
|
|
80
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Logger } from '../../utils/logger.js';
|
|
2
|
+
export interface BlueprinitFlags {
|
|
3
|
+
yes?: boolean;
|
|
4
|
+
organizationId?: string;
|
|
5
|
+
projectId?: string;
|
|
6
|
+
/** Display name for a project to create, instead of using one that exists. */
|
|
7
|
+
createProject?: string;
|
|
8
|
+
datasetName?: string;
|
|
9
|
+
stackName?: string;
|
|
10
|
+
studioName?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface BlueprinitFlowOptions {
|
|
13
|
+
bin: string;
|
|
14
|
+
dir: string;
|
|
15
|
+
/** True when a directory argument named the target, so the flow must not ask for one. */
|
|
16
|
+
targetProvided: boolean;
|
|
17
|
+
flags: BlueprinitFlags;
|
|
18
|
+
log: Logger;
|
|
19
|
+
token: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|