@sanity/runtime-cli 17.3.2 → 17.4.0-blueprinit.g596ac51
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 +50 -47
- package/SKILL.md +59 -0
- package/dist/actions/blueprinit/manifest.d.ts +15 -0
- package/dist/actions/blueprinit/manifest.js +82 -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 +34 -0
- package/dist/actions/blueprinit/templates.js +83 -0
- package/dist/actions/blueprinit/workspace.d.ts +16 -0
- package/dist/actions/blueprinit/workspace.js +143 -0
- package/dist/actions/blueprints/pre-deploy.d.ts +15 -6
- package/dist/actions/blueprints/pre-deploy.js +29 -70
- package/dist/actions/blueprints/process-assets.d.ts +29 -0
- package/dist/actions/blueprints/process-assets.js +80 -0
- package/dist/actions/functions/list.d.ts +34 -0
- package/dist/actions/functions/list.js +33 -0
- package/dist/actions/sanity/datasets.d.ts +15 -0
- package/dist/actions/sanity/datasets.js +26 -0
- package/dist/baseCommands.d.ts +2 -0
- package/dist/baseCommands.js +3 -0
- package/dist/commands/blueprinit.d.ts +17 -0
- package/dist/commands/blueprinit.js +59 -0
- package/dist/commands/blueprints/deploy.d.ts +2 -0
- package/dist/commands/blueprints/deploy.js +22 -1
- package/dist/commands/blueprints/destroy.js +1 -1
- package/dist/commands/blueprints/info.d.ts +1 -0
- package/dist/commands/blueprints/info.js +5 -0
- package/dist/commands/blueprints/init.js +1 -2
- package/dist/commands/blueprints/logs.js +1 -0
- package/dist/commands/functions/add.js +3 -3
- package/dist/commands/functions/dev.js +2 -2
- package/dist/commands/functions/list.d.ts +14 -0
- package/dist/commands/functions/list.js +38 -0
- package/dist/commands/functions/logs.d.ts +1 -1
- package/dist/commands/functions/logs.js +3 -0
- package/dist/cores/blueprinit/adopt.d.ts +6 -0
- package/dist/cores/blueprinit/adopt.js +119 -0
- package/dist/cores/blueprinit/finish.d.ts +14 -0
- package/dist/cores/blueprinit/finish.js +64 -0
- package/dist/cores/blueprinit/fresh.d.ts +3 -0
- package/dist/cores/blueprinit/fresh.js +120 -0
- package/dist/cores/blueprinit/preview.d.ts +15 -0
- package/dist/cores/blueprinit/preview.js +14 -0
- package/dist/cores/blueprinit/prompts.d.ts +5 -0
- package/dist/cores/blueprinit/prompts.js +22 -0
- package/dist/cores/blueprinit/selection.d.ts +15 -0
- package/dist/cores/blueprinit/selection.js +90 -0
- package/dist/cores/blueprinit/types.d.ts +16 -0
- package/dist/cores/blueprinit/types.js +1 -0
- package/dist/cores/blueprinit.d.ts +7 -0
- package/dist/cores/blueprinit.js +48 -0
- package/dist/cores/blueprints/deploy.js +69 -19
- package/dist/cores/functions/add.js +1 -1
- package/dist/cores/functions/list.d.ts +10 -0
- package/dist/cores/functions/list.js +34 -0
- package/dist/utils/display/blueprints-formatting.js +3 -3
- package/dist/utils/display/functions-formatting.d.ts +7 -0
- package/dist/utils/display/functions-formatting.js +95 -0
- package/dist/utils/style-text.js +1 -0
- package/oclif.manifest.json +271 -15
- package/package.json +31 -17
- package/skills/_functions-build.md +23 -0
- package/skills/_functions-list.md +28 -0
- package/skills/blueprinit.md +155 -0
- package/skills/blueprints-add.md +8 -0
- package/skills/blueprints-config.md +33 -0
- package/skills/blueprints-deploy.md +36 -0
- package/skills/blueprints-destroy.md +30 -0
- package/skills/blueprints-doctor.md +31 -0
- package/skills/blueprints-info.md +23 -0
- package/skills/blueprints-init.md +48 -0
- package/skills/blueprints-logs.md +33 -0
- package/skills/blueprints-mint-deploy-token.md +25 -0
- package/skills/blueprints-plan.md +28 -0
- package/skills/blueprints-promote.md +31 -0
- package/skills/blueprints-stacks.md +28 -0
- package/skills/functions-add.md +35 -0
- package/skills/functions-dev.md +21 -0
- package/skills/functions-env-add.md +29 -0
- package/skills/functions-env-list.md +21 -0
- package/skills/functions-env-remove.md +22 -0
- package/skills/functions-logs.md +39 -0
- package/skills/functions-test.md +31 -0
|
@@ -11,7 +11,9 @@ export default class DeployCommand extends ResolvedCommand<typeof DeployCommand>
|
|
|
11
11
|
message: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
'fn-installer': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
configure: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
'new-stack-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
};
|
|
17
|
+
protected preResolve(): Promise<void>;
|
|
16
18
|
run(): Promise<Record<string, unknown> | undefined>;
|
|
17
19
|
}
|
|
@@ -14,13 +14,16 @@ Use --no-wait to queue the deployment and return immediately without waiting for
|
|
|
14
14
|
|
|
15
15
|
Use --fn-installer to force which package manager to use when deploying functions.
|
|
16
16
|
|
|
17
|
-
Set SANITY_ASSET_TIMEOUT (seconds) to override the
|
|
17
|
+
Set SANITY_ASSET_TIMEOUT (seconds) to override the 180-second timeout for processing resource assets.
|
|
18
|
+
|
|
19
|
+
Set SANITY_ASSET_CONCURRENCY to override how many resource assets are processed at once (default 4).
|
|
18
20
|
|
|
19
21
|
Exit codes: 0 deployed, 2 deployment failed, 75 deployment accepted but completion could not be confirmed (rerun 'blueprints info' to check).`;
|
|
20
22
|
static examples = [
|
|
21
23
|
'<%= config.bin %> <%= command.id %>',
|
|
22
24
|
'<%= config.bin %> <%= command.id %> --message "Enable staging dataset"',
|
|
23
25
|
'<%= config.bin %> <%= command.id %> --no-wait',
|
|
26
|
+
// '<%= config.bin %> <%= command.id %> --configure',
|
|
24
27
|
'<%= config.bin %> <%= command.id %> --fn-installer npm',
|
|
25
28
|
'<%= config.bin %> <%= command.id %> --stack <name-or-id>',
|
|
26
29
|
'<%= config.bin %> <%= command.id %> --organization-id <orgId> --stack <name-or-id>',
|
|
@@ -44,10 +47,28 @@ Exit codes: 0 deployed, 2 deployment failed, 75 deployment accepted but completi
|
|
|
44
47
|
description: 'Do not wait for Stack deployment to complete',
|
|
45
48
|
default: false,
|
|
46
49
|
}),
|
|
50
|
+
configure: Flags.boolean({
|
|
51
|
+
description: 'Configure the local Blueprint interactively before deploying',
|
|
52
|
+
aliases: ['config', 'init'],
|
|
53
|
+
default: false,
|
|
54
|
+
hidden: true,
|
|
55
|
+
}),
|
|
47
56
|
'new-stack-name': Flags.string({
|
|
48
57
|
description: 'Set a new name for the Stack',
|
|
49
58
|
}),
|
|
50
59
|
};
|
|
60
|
+
async preResolve() {
|
|
61
|
+
if (!this.flags.configure)
|
|
62
|
+
return;
|
|
63
|
+
const args = ['--edit'];
|
|
64
|
+
if (this.flags.path)
|
|
65
|
+
args.push('--path', this.flags.path);
|
|
66
|
+
if (!this.flags['validate-resources'])
|
|
67
|
+
args.push('--no-validate-resources');
|
|
68
|
+
if (this.flags.verbose)
|
|
69
|
+
args.push('--verbose');
|
|
70
|
+
await this.config.runCommand('blueprints:config', args);
|
|
71
|
+
}
|
|
51
72
|
async run() {
|
|
52
73
|
const result = await blueprintDeployCore({
|
|
53
74
|
bin: this.config.bin,
|
|
@@ -19,7 +19,7 @@ Exit codes: 0 destroyed, 2 destruction failed, 75 destruction accepted but compl
|
|
|
19
19
|
static flags = {
|
|
20
20
|
force: Flags.boolean({
|
|
21
21
|
description: 'Force Stack destruction (skip confirmation)',
|
|
22
|
-
|
|
22
|
+
char: 'f',
|
|
23
23
|
default: false,
|
|
24
24
|
}),
|
|
25
25
|
'project-id': Flags.string({
|
|
@@ -8,6 +8,7 @@ export default class InfoCommand extends ResolvedCommand<typeof InfoCommand> {
|
|
|
8
8
|
stack: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
9
|
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
'organization-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
};
|
|
12
13
|
run(): Promise<Record<string, unknown> | undefined>;
|
|
13
14
|
}
|
|
@@ -23,6 +23,11 @@ Run 'blueprints stacks' to see all available Stacks in your project or organizat
|
|
|
23
23
|
}),
|
|
24
24
|
'project-id': Flags.string({ ...projectIdFlagConfig }),
|
|
25
25
|
'organization-id': Flags.string({ ...organizationIdFlagConfig }),
|
|
26
|
+
verbose: Flags.boolean({
|
|
27
|
+
char: 'v',
|
|
28
|
+
description: 'Show resource and external IDs',
|
|
29
|
+
default: false,
|
|
30
|
+
}),
|
|
26
31
|
};
|
|
27
32
|
async run() {
|
|
28
33
|
const result = await blueprintInfoCore({
|
|
@@ -6,10 +6,9 @@ import { validTokenOrErrorMessage } from '../../utils/validated-token.js';
|
|
|
6
6
|
export default class InitCommand extends RuntimeCommand {
|
|
7
7
|
static summary = 'Initialize a Blueprint and create a remote Stack';
|
|
8
8
|
static description = `A Blueprint is your local infrastructure-as-code configuration that defines Sanity resources (datasets, functions, etc.). A Stack is the remote deployment target where your Blueprint is applied.
|
|
9
|
-
[NOTE: Currently, accounts are limited to three (3) Stacks per project scope.]
|
|
10
9
|
|
|
11
10
|
This is typically the first command you run in a new project. It creates a local Blueprint manifest file (sanity.blueprint.ts, .js, or .json) and provisions a new remote Stack.
|
|
12
|
-
Additionally, a Blueprint configuration file is created in .sanity/ containing the scope and Stack IDs.
|
|
11
|
+
Additionally, a Blueprint configuration file is created in .sanity/ containing the scope and Stack IDs. A .gitignore covering node_modules, .env, and Function build output is created or updated; the .sanity/ config itself is not ignored.
|
|
13
12
|
|
|
14
13
|
After initialization, use 'blueprints plan' to preview changes, then 'blueprints deploy' to apply them.
|
|
15
14
|
|
|
@@ -23,7 +23,7 @@ After adding, use 'functions dev' to test locally, then 'blueprints deploy' to p
|
|
|
23
23
|
example: Flags.string({
|
|
24
24
|
description: 'Example to use for the Function',
|
|
25
25
|
aliases: ['recipe'],
|
|
26
|
-
exclusive: ['name', '
|
|
26
|
+
exclusive: ['name', 'type', 'language', 'javascript', 'helpers', 'installer', 'install'],
|
|
27
27
|
}),
|
|
28
28
|
name: Flags.string({
|
|
29
29
|
description: 'Name of the Function to add',
|
|
@@ -56,9 +56,9 @@ After adding, use 'functions dev' to test locally, then 'blueprints deploy' to p
|
|
|
56
56
|
options: ['skip', ...INSTALLER_OPTIONS],
|
|
57
57
|
}),
|
|
58
58
|
install: Flags.boolean({
|
|
59
|
-
description: 'Shortcut for --
|
|
59
|
+
description: 'Shortcut for --installer npm',
|
|
60
60
|
char: 'i',
|
|
61
|
-
exclusive: ['
|
|
61
|
+
exclusive: ['installer'],
|
|
62
62
|
}),
|
|
63
63
|
};
|
|
64
64
|
async run() {
|
|
@@ -9,11 +9,11 @@ export default class DevCommand extends ResolvedCommand {
|
|
|
9
9
|
|
|
10
10
|
Open the emulator in your browser to interactively test your functions with the payload editor.
|
|
11
11
|
|
|
12
|
-
Optionally, set the host and port with the --host and --port flags. Function timeout can be configured with the --timeout flag.
|
|
12
|
+
Optionally, set the host and port with the --host and --port flags. Port 8974 is reserved for the emulator's live-reload WebSocket server. Function timeout can be configured with the --timeout flag.
|
|
13
13
|
|
|
14
14
|
To invoke a function with the CLI, use 'functions test'.`;
|
|
15
15
|
static examples = [
|
|
16
|
-
'<%= config.bin %> <%= command.id %> --host 127.0.0.1 --port
|
|
16
|
+
'<%= config.bin %> <%= command.id %> --host 127.0.0.1 --port 3333',
|
|
17
17
|
'<%= config.bin %> <%= command.id %> --timeout 60',
|
|
18
18
|
];
|
|
19
19
|
static flags = {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ResolvedCommand } from '../../baseCommands.js';
|
|
2
|
+
export default class FunctionsListCommand extends ResolvedCommand<typeof FunctionsListCommand> {
|
|
3
|
+
static needs: readonly ['scope'];
|
|
4
|
+
static hidden: boolean;
|
|
5
|
+
static summary: string;
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'organization-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<Record<string, unknown> | undefined>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { organizationIdFlagConfig, projectIdFlagConfig, ResolvedCommand } from '../../baseCommands.js';
|
|
3
|
+
import { functionListCore } from '../../cores/functions/list.js';
|
|
4
|
+
import { Logger } from '../../utils/logger.js';
|
|
5
|
+
export default class FunctionsListCommand extends ResolvedCommand {
|
|
6
|
+
static needs = ['scope'];
|
|
7
|
+
static hidden = true;
|
|
8
|
+
static summary = 'List deployed Sanity Functions';
|
|
9
|
+
static description = `Lists functions deployed directly in the selected project or organization scope. The local Blueprint config supplies the scope by default.
|
|
10
|
+
|
|
11
|
+
Use --project-id or --organization-id to select a scope without a local Blueprint. Organization results include organization-scoped functions only.`;
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> <%= command.id %>',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --verbose',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --project-id <projectId>',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> --organization-id <organizationId>',
|
|
17
|
+
'<%= config.bin %> <%= command.id %> --project-id <projectId> --json',
|
|
18
|
+
];
|
|
19
|
+
static flags = {
|
|
20
|
+
'project-id': Flags.string({ ...projectIdFlagConfig }),
|
|
21
|
+
'organization-id': Flags.string({ ...organizationIdFlagConfig }),
|
|
22
|
+
verbose: Flags.boolean({
|
|
23
|
+
char: 'v',
|
|
24
|
+
description: 'Show function configuration details',
|
|
25
|
+
default: false,
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
async run() {
|
|
29
|
+
const result = await functionListCore({
|
|
30
|
+
log: Logger(this.log.bind(this), this.flags),
|
|
31
|
+
auth: this.auth,
|
|
32
|
+
flags: this.flags,
|
|
33
|
+
});
|
|
34
|
+
if (!result.success)
|
|
35
|
+
this.coreError(result);
|
|
36
|
+
return result.json;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -15,5 +15,5 @@ export default class LogsCommand extends ResolvedCommand<typeof LogsCommand> {
|
|
|
15
15
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
16
|
watch: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
17
|
};
|
|
18
|
-
run(): Promise<Record<string, unknown
|
|
18
|
+
run(): Promise<void | Record<string, unknown>>;
|
|
19
19
|
}
|
|
@@ -46,6 +46,7 @@ Use --watch (-w) to stream logs in real-time. Use --delete to clear all logs for
|
|
|
46
46
|
char: 'w',
|
|
47
47
|
description: 'Watch for new logs (streaming mode)',
|
|
48
48
|
aliases: ['follow'],
|
|
49
|
+
exclusive: ['json'],
|
|
49
50
|
}),
|
|
50
51
|
};
|
|
51
52
|
async run() {
|
|
@@ -67,6 +68,8 @@ Use --watch (-w) to stream logs in real-time. Use --delete to clear all logs for
|
|
|
67
68
|
});
|
|
68
69
|
if (!result.success)
|
|
69
70
|
this.coreError(result);
|
|
71
|
+
if (result.streaming)
|
|
72
|
+
return result.streaming;
|
|
70
73
|
return result.json;
|
|
71
74
|
}
|
|
72
75
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DetectedStudio } from '../../actions/blueprinit/studio.js';
|
|
2
|
+
import type { CoreResult } from '../index.js';
|
|
3
|
+
import type { BlueprinitFlowOptions } from './types.js';
|
|
4
|
+
export declare function adoptFlow(options: BlueprinitFlowOptions & {
|
|
5
|
+
studio: DetectedStudio;
|
|
6
|
+
}): Promise<CoreResult>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { env } from 'node:process';
|
|
4
|
+
import { hostedStudioOrigin, slugify } from '../../actions/blueprinit/manifest.js';
|
|
5
|
+
import { adoptWorkspace } from '../../actions/blueprinit/workspace.js';
|
|
6
|
+
import { listDatasets } from '../../actions/sanity/datasets.js';
|
|
7
|
+
import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
|
|
8
|
+
import { check } from '../../utils/display/presenters.js';
|
|
9
|
+
import { getLatestNpmVersion } from '../../utils/external/npmjs.js';
|
|
10
|
+
import { isInteractive } from '../../utils/interactive.js';
|
|
11
|
+
import { createStackAndFinish } from './finish.js';
|
|
12
|
+
import { printBlueprintPreview } from './preview.js';
|
|
13
|
+
import { confirmUnlessSkipped, promptForStudioName, resourceNamesCollide } from './prompts.js';
|
|
14
|
+
import { findProject } from './selection.js';
|
|
15
|
+
export async function adoptFlow(options) {
|
|
16
|
+
const { bin, dir, flags, log, studio, token } = options;
|
|
17
|
+
if (flags.projectId && studio.projectId && flags.projectId !== studio.projectId) {
|
|
18
|
+
return { success: false, error: `The Studio uses project "${studio.projectId}".` };
|
|
19
|
+
}
|
|
20
|
+
if (flags.datasetName && studio.dataset && flags.datasetName !== studio.dataset) {
|
|
21
|
+
return { success: false, error: `The Studio uses dataset "${studio.dataset}".` };
|
|
22
|
+
}
|
|
23
|
+
const projectId = studio.projectId ?? flags.projectId;
|
|
24
|
+
const datasetName = studio.dataset ?? flags.datasetName;
|
|
25
|
+
if (!projectId || !datasetName) {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
error: `Found ${studio.configFile}, but could not read a projectId and dataset from it.`,
|
|
29
|
+
suggestions: ['Use literal values in the Studio config or pass --project-id and --dataset.'],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
log(check(`Studio detected (${studio.configFile})`));
|
|
33
|
+
const grouped = await groupProjectsByOrganization({ token, logger: log });
|
|
34
|
+
if (!grouped.ok) {
|
|
35
|
+
return { success: false, error: `Could not list organizations: ${grouped.error}` };
|
|
36
|
+
}
|
|
37
|
+
const match = findProject(grouped.organizations, projectId);
|
|
38
|
+
if (!match) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
error: `Project "${projectId}" from ${studio.configFile} was not found in your account.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (flags.organizationId && flags.organizationId !== match.organization.id) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: `Project "${studio.projectId}" belongs to organization "${match.organization.id}".`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const datasets = await listDatasets({ token, projectId: match.project.id, logger: log });
|
|
51
|
+
if (!datasets.ok) {
|
|
52
|
+
return { success: false, error: `Could not list datasets: ${datasets.error}` };
|
|
53
|
+
}
|
|
54
|
+
if (!datasets.datasets.some(({ name }) => name === datasetName)) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: `Dataset "${datasetName}" was not found in project ${match.project.displayName}.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
log(check(`Matches project ${match.project.displayName} (${match.project.id}), dataset ${datasetName}`));
|
|
61
|
+
log('');
|
|
62
|
+
const studioName = await promptForStudioName(flags, isInteractive);
|
|
63
|
+
const stackName = flags.stackName
|
|
64
|
+
? slugify(flags.stackName) || match.project.id
|
|
65
|
+
: slugify(match.project.displayName) || match.project.id;
|
|
66
|
+
if (resourceNamesCollide(stackName, studioName)) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
error: `Stack and Studio names both resolve to "${stackName}". Blueprint resource names must be unique.`,
|
|
70
|
+
suggestions: ['Pass --stack-name or --studio-name with a different value.'],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const studioOrigin = hostedStudioOrigin(studioName, env.SANITY_INTERNAL_ENV);
|
|
74
|
+
const marker = (file) => (existsSync(join(dir, file)) ? '~' : '+');
|
|
75
|
+
printBlueprintPreview({
|
|
76
|
+
log,
|
|
77
|
+
organizationName: match.organization.name,
|
|
78
|
+
projectId: match.project.id,
|
|
79
|
+
projectName: match.project.displayName,
|
|
80
|
+
datasetName,
|
|
81
|
+
studioName,
|
|
82
|
+
studioOrigin,
|
|
83
|
+
studioSource: '.',
|
|
84
|
+
stackName,
|
|
85
|
+
});
|
|
86
|
+
log('Changes:');
|
|
87
|
+
log(" + sanity.blueprint.ts attach project + dataset; add CORS and Studio at '.'");
|
|
88
|
+
log(` + .sanity/blueprint.config.json org scope, stack ${stackName}`);
|
|
89
|
+
log(` ${marker('.gitignore')} .gitignore`);
|
|
90
|
+
log(` ${marker('package.json')} package.json`);
|
|
91
|
+
log('');
|
|
92
|
+
const confirmed = await confirmUnlessSkipped(flags, isInteractive, 'Adopt this Studio into the Blueprint?');
|
|
93
|
+
if (!confirmed)
|
|
94
|
+
return { success: false, error: 'Initialization cancelled.' };
|
|
95
|
+
const adoption = adoptWorkspace({
|
|
96
|
+
dir,
|
|
97
|
+
projectId: match.project.id,
|
|
98
|
+
displayName: match.project.displayName,
|
|
99
|
+
datasetName,
|
|
100
|
+
stackName,
|
|
101
|
+
studioName,
|
|
102
|
+
studioOrigin,
|
|
103
|
+
blueprintsVersion: await getLatestNpmVersion('@sanity/blueprints', log),
|
|
104
|
+
});
|
|
105
|
+
return await createStackAndFinish({
|
|
106
|
+
...options,
|
|
107
|
+
organizationId: match.organization.id,
|
|
108
|
+
projectId: match.project.id,
|
|
109
|
+
datasetName,
|
|
110
|
+
stackName,
|
|
111
|
+
blueprintFilePath: adoption.blueprintFilePath,
|
|
112
|
+
files: adoption.files,
|
|
113
|
+
nextSteps: [
|
|
114
|
+
` npx ${bin} blueprints plan`,
|
|
115
|
+
` npx ${bin} blueprints deploy`,
|
|
116
|
+
` npx ${bin} blueprints info`,
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CoreResult } from '../index.js';
|
|
2
|
+
import type { BlueprinitFlowOptions } from './types.js';
|
|
3
|
+
interface FinishOptions extends BlueprinitFlowOptions {
|
|
4
|
+
organizationId: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
datasetName: string;
|
|
7
|
+
stackName: string;
|
|
8
|
+
blueprintFilePath: string;
|
|
9
|
+
files: string[];
|
|
10
|
+
nextSteps: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function createStackAndFinish(options: FinishOptions): Promise<CoreResult>;
|
|
13
|
+
export declare function alignSteps(steps: Array<[command: string, description?: string]>): string[];
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { writeConfigFile } from '../../actions/blueprints/config.js';
|
|
2
|
+
import { createEmptyStack } from '../../actions/blueprints/stacks.js';
|
|
3
|
+
import { check } from '../../utils/display/presenters.js';
|
|
4
|
+
export async function createStackAndFinish(options) {
|
|
5
|
+
let stack;
|
|
6
|
+
try {
|
|
7
|
+
stack = await createEmptyStack({
|
|
8
|
+
token: options.token,
|
|
9
|
+
scopeType: 'organization',
|
|
10
|
+
scopeId: options.organizationId,
|
|
11
|
+
name: options.stackName,
|
|
12
|
+
logger: options.log,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
error: `Local files were written, but Stack creation failed: ${message}`,
|
|
20
|
+
suggestions: [
|
|
21
|
+
'Resolve the Stack error, then finish setup with:',
|
|
22
|
+
`npx ${options.bin} blueprints init --organization-id ${options.organizationId} --stack-name ${options.stackName}`,
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
writeConfigFile(options.blueprintFilePath, {
|
|
28
|
+
organizationId: options.organizationId,
|
|
29
|
+
stackId: stack.id,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error: `Stack ${stack.id} was created, but the local Blueprint config could not be written: ${message}`,
|
|
37
|
+
suggestions: [
|
|
38
|
+
'Finish setup with:',
|
|
39
|
+
`npx ${options.bin} blueprints init --organization-id ${options.organizationId} --stack-id ${stack.id}`,
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
options.log(check(`Stack ${stack.name} created (org scope)`));
|
|
44
|
+
options.log('The Stack is empty; Blueprint resources have not been deployed.');
|
|
45
|
+
options.log('');
|
|
46
|
+
options.log('Next steps:');
|
|
47
|
+
for (const step of options.nextSteps)
|
|
48
|
+
options.log(step);
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
json: {
|
|
52
|
+
organizationId: options.organizationId,
|
|
53
|
+
projectId: options.projectId,
|
|
54
|
+
dataset: options.datasetName,
|
|
55
|
+
stackId: stack.id,
|
|
56
|
+
stackName: stack.name,
|
|
57
|
+
files: [...options.files, '.sanity/blueprint.config.json'],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function alignSteps(steps) {
|
|
62
|
+
const width = Math.max(...steps.map(([command]) => command.length));
|
|
63
|
+
return steps.map(([command, description]) => description ? ` ${command.padEnd(width + 2)}${description}` : ` ${command}`);
|
|
64
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { env } from 'node:process';
|
|
2
|
+
import { hostedStudioOrigin, STARTER_FUNCTION_NAME, slugify, } from '../../actions/blueprinit/manifest.js';
|
|
3
|
+
import { scaffoldWorkspace } from '../../actions/blueprinit/workspace.js';
|
|
4
|
+
import { listDatasets } from '../../actions/sanity/datasets.js';
|
|
5
|
+
import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
|
|
6
|
+
import { check } from '../../utils/display/presenters.js';
|
|
7
|
+
import { getLatestNpmVersion } from '../../utils/external/npmjs.js';
|
|
8
|
+
import { isInteractive } from '../../utils/interactive.js';
|
|
9
|
+
import { alignSteps, createStackAndFinish } from './finish.js';
|
|
10
|
+
import { printBlueprintPreview } from './preview.js';
|
|
11
|
+
import { confirmUnlessSkipped, promptForStudioName, resourceNamesCollide } from './prompts.js';
|
|
12
|
+
import { chooseDataset, chooseOrganization, chooseProject } from './selection.js';
|
|
13
|
+
export async function freshFlow(options) {
|
|
14
|
+
const { bin, dir, flags, log, token } = options;
|
|
15
|
+
const grouped = await groupProjectsByOrganization({ token, logger: log });
|
|
16
|
+
if (!grouped.ok) {
|
|
17
|
+
return { success: false, error: `Could not list organizations: ${grouped.error}` };
|
|
18
|
+
}
|
|
19
|
+
const organizationResult = await chooseOrganization(grouped.organizations, flags.organizationId, isInteractive);
|
|
20
|
+
if (!organizationResult.ok) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: organizationResult.error.message,
|
|
24
|
+
suggestions: organizationResult.error.suggestions,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const { organization, projects = [] } = organizationResult.value;
|
|
28
|
+
const projectResult = await chooseProject(projects, flags.projectId, organization.name, isInteractive);
|
|
29
|
+
if (!projectResult.ok) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
error: projectResult.error.message,
|
|
33
|
+
suggestions: projectResult.error.suggestions,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const project = projectResult.value;
|
|
37
|
+
const datasets = await listDatasets({ token, projectId: project.id, logger: log });
|
|
38
|
+
if (!datasets.ok) {
|
|
39
|
+
return { success: false, error: `Could not list datasets: ${datasets.error}` };
|
|
40
|
+
}
|
|
41
|
+
const datasetResult = await chooseDataset(datasets.datasets, flags.datasetName, project.displayName, isInteractive);
|
|
42
|
+
if (!datasetResult.ok) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
error: datasetResult.error.message,
|
|
46
|
+
suggestions: datasetResult.error.suggestions,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const dataset = datasetResult.value;
|
|
50
|
+
log(check(`Org: ${organization.name} (${organization.id})`));
|
|
51
|
+
log(check(`Project: ${project.displayName} (${project.id}), dataset: ${dataset.name}`));
|
|
52
|
+
log('');
|
|
53
|
+
const stackName = flags.stackName
|
|
54
|
+
? slugify(flags.stackName) || project.id
|
|
55
|
+
: slugify(project.displayName) || project.id;
|
|
56
|
+
const studioName = await promptForStudioName(flags, isInteractive);
|
|
57
|
+
if (resourceNamesCollide(stackName, studioName)) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: `Stack and Studio names both resolve to "${stackName}". Blueprint resource names must be unique.`,
|
|
61
|
+
suggestions: ['Pass --stack-name or --studio-name with a different value.'],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const studioOrigin = hostedStudioOrigin(studioName, env.SANITY_INTERNAL_ENV);
|
|
65
|
+
printBlueprintPreview({
|
|
66
|
+
log,
|
|
67
|
+
organizationName: organization.name,
|
|
68
|
+
projectId: project.id,
|
|
69
|
+
projectName: project.displayName,
|
|
70
|
+
datasetName: dataset.name,
|
|
71
|
+
studioName,
|
|
72
|
+
studioOrigin,
|
|
73
|
+
studioSource: `studios/${studioName}`,
|
|
74
|
+
stackName,
|
|
75
|
+
functionName: STARTER_FUNCTION_NAME,
|
|
76
|
+
});
|
|
77
|
+
log('Files to write:');
|
|
78
|
+
log(' + sanity.blueprint.ts project + dataset (attached), CORS, studio, function');
|
|
79
|
+
const studioDirectory = `studios/${studioName}/`;
|
|
80
|
+
log(` + ${studioDirectory.padEnd(Math.max(32, studioDirectory.length + 2))}Studio with a "post" type`);
|
|
81
|
+
log(` + ${`functions/${STARTER_FUNCTION_NAME}/`.padEnd(32)}sets a post slug from its title`);
|
|
82
|
+
log(` + .sanity/blueprint.config.json org scope, stack ${stackName}`);
|
|
83
|
+
log(' + .gitignore');
|
|
84
|
+
log(' + package.json');
|
|
85
|
+
log('');
|
|
86
|
+
const confirmed = await confirmUnlessSkipped(flags, isInteractive, `Create this Blueprint workspace for "${project.displayName}"?`);
|
|
87
|
+
if (!confirmed)
|
|
88
|
+
return { success: false, error: 'Initialization cancelled.' };
|
|
89
|
+
const scaffold = scaffoldWorkspace({
|
|
90
|
+
dir,
|
|
91
|
+
projectId: project.id,
|
|
92
|
+
displayName: project.displayName,
|
|
93
|
+
datasetName: dataset.name,
|
|
94
|
+
stackName,
|
|
95
|
+
studioName,
|
|
96
|
+
studioOrigin,
|
|
97
|
+
blueprintsVersion: await getLatestNpmVersion('@sanity/blueprints', log),
|
|
98
|
+
});
|
|
99
|
+
return await createStackAndFinish({
|
|
100
|
+
...options,
|
|
101
|
+
organizationId: organization.id,
|
|
102
|
+
projectId: project.id,
|
|
103
|
+
datasetName: dataset.name,
|
|
104
|
+
stackName,
|
|
105
|
+
blueprintFilePath: scaffold.blueprintFilePath,
|
|
106
|
+
files: scaffold.files,
|
|
107
|
+
nextSteps: [
|
|
108
|
+
...alignSteps([
|
|
109
|
+
['npm install'],
|
|
110
|
+
[`npm run dev -w studios/${studioName}`, 'open the Studio and add a post'],
|
|
111
|
+
[`npx ${bin} blueprints plan`, 'preview the deployment'],
|
|
112
|
+
[`npx ${bin} blueprints deploy`, 'deploy the resources'],
|
|
113
|
+
]),
|
|
114
|
+
'',
|
|
115
|
+
'Test the function on that post:',
|
|
116
|
+
` npx ${bin} functions test ${STARTER_FUNCTION_NAME} --document-id <id>` +
|
|
117
|
+
` --project-id ${project.id} --dataset ${dataset.name} --with-user-token`,
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Logger } from '../../utils/logger.js';
|
|
2
|
+
interface BlueprintPreviewOptions {
|
|
3
|
+
log: Logger;
|
|
4
|
+
organizationName: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
projectName: string;
|
|
7
|
+
datasetName: string;
|
|
8
|
+
studioName: string;
|
|
9
|
+
studioOrigin: string;
|
|
10
|
+
studioSource: string;
|
|
11
|
+
stackName: string;
|
|
12
|
+
functionName?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function printBlueprintPreview(options: BlueprintPreviewOptions): void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function printBlueprintPreview(options) {
|
|
2
|
+
options.log('Blueprint plan:');
|
|
3
|
+
options.log(` attach Project ${options.projectName} (${options.projectId}) on deploy; retain on Stack removal`);
|
|
4
|
+
options.log(` attach Dataset ${options.datasetName} on deploy; retain on Stack removal`);
|
|
5
|
+
options.log(` create CORS Origin ${options.studioName}-cors for ${options.studioOrigin} on deploy`);
|
|
6
|
+
options.log(` create Studio ${options.studioName} from ${options.studioSource} at ${options.studioOrigin} on deploy`);
|
|
7
|
+
if (options.functionName) {
|
|
8
|
+
options.log(` create Function ${options.functionName} on deploy`);
|
|
9
|
+
}
|
|
10
|
+
options.log('');
|
|
11
|
+
options.log('Remote action now:');
|
|
12
|
+
options.log(` create Stack ${options.stackName} in ${options.organizationName} (empty, organization scope)`);
|
|
13
|
+
options.log('');
|
|
14
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BlueprinitFlags } from './types.js';
|
|
2
|
+
export declare const DEFAULT_STUDIO_NAME = "studio";
|
|
3
|
+
export declare function resourceNamesCollide(stackName: string, studioName: string): boolean;
|
|
4
|
+
export declare function confirmUnlessSkipped(flags: BlueprinitFlags, interactive: boolean, message: string): Promise<boolean>;
|
|
5
|
+
export declare function promptForStudioName(flags: BlueprinitFlags, interactive: boolean): Promise<string>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
2
|
+
import { slugify } from '../../actions/blueprinit/manifest.js';
|
|
3
|
+
export const DEFAULT_STUDIO_NAME = 'studio';
|
|
4
|
+
export function resourceNamesCollide(stackName, studioName) {
|
|
5
|
+
return stackName === studioName;
|
|
6
|
+
}
|
|
7
|
+
export async function confirmUnlessSkipped(flags, interactive, message) {
|
|
8
|
+
if (flags.yes || !interactive)
|
|
9
|
+
return true;
|
|
10
|
+
return await confirm({ message, default: true });
|
|
11
|
+
}
|
|
12
|
+
export async function promptForStudioName(flags, interactive) {
|
|
13
|
+
if (flags.studioName)
|
|
14
|
+
return slugify(flags.studioName) || DEFAULT_STUDIO_NAME;
|
|
15
|
+
if (flags.yes || !interactive)
|
|
16
|
+
return DEFAULT_STUDIO_NAME;
|
|
17
|
+
const answer = await input({
|
|
18
|
+
message: 'Name your Studio and hosted slug?',
|
|
19
|
+
default: DEFAULT_STUDIO_NAME,
|
|
20
|
+
});
|
|
21
|
+
return slugify(answer) || DEFAULT_STUDIO_NAME;
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Dataset } from '../../actions/sanity/datasets.js';
|
|
2
|
+
import type { GroupedProjects, Project } from '../../actions/sanity/projects.js';
|
|
3
|
+
import type { Result } from '../../utils/types.js';
|
|
4
|
+
interface SelectionError {
|
|
5
|
+
message: string;
|
|
6
|
+
suggestions?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function chooseOrganization(groups: GroupedProjects[], organizationId: string | undefined, interactive: boolean): Promise<Result<GroupedProjects, SelectionError>>;
|
|
9
|
+
export declare function chooseProject(projects: Project[], projectId: string | undefined, organizationName: string, interactive: boolean): Promise<Result<Project, SelectionError>>;
|
|
10
|
+
export declare function chooseDataset(datasets: Dataset[], datasetName: string | undefined, projectName: string, interactive: boolean): Promise<Result<Dataset, SelectionError>>;
|
|
11
|
+
export declare function findProject(groups: GroupedProjects[], projectId: string): {
|
|
12
|
+
organization: GroupedProjects['organization'];
|
|
13
|
+
project: Project;
|
|
14
|
+
} | undefined;
|
|
15
|
+
export {};
|