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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +21 -20
  2. package/dist/actions/blueprinit/manifest.d.ts +46 -0
  3. package/dist/actions/blueprinit/manifest.js +121 -0
  4. package/dist/actions/blueprinit/quote.d.ts +6 -0
  5. package/dist/actions/blueprinit/quote.js +8 -0
  6. package/dist/actions/blueprinit/studio.d.ts +6 -0
  7. package/dist/actions/blueprinit/studio.js +43 -0
  8. package/dist/actions/blueprinit/templates.d.ts +73 -0
  9. package/dist/actions/blueprinit/templates.js +138 -0
  10. package/dist/actions/blueprinit/versions.d.ts +8 -0
  11. package/dist/actions/blueprinit/versions.js +11 -0
  12. package/dist/actions/blueprinit/workspace.d.ts +27 -0
  13. package/dist/actions/blueprinit/workspace.js +145 -0
  14. package/dist/actions/blueprints/pre-deploy.js +2 -0
  15. package/dist/actions/blueprints/stacks.d.ts +1 -0
  16. package/dist/actions/sanity/create-project.d.ts +19 -0
  17. package/dist/actions/sanity/create-project.js +32 -0
  18. package/dist/actions/sanity/datasets.d.ts +15 -0
  19. package/dist/actions/sanity/datasets.js +26 -0
  20. package/dist/commands/functions/test.d.ts +1 -0
  21. package/dist/commands/functions/test.js +5 -0
  22. package/dist/commands/init.d.ts +21 -0
  23. package/dist/commands/init.js +82 -0
  24. package/dist/cores/blueprinit/adopt.d.ts +6 -0
  25. package/dist/cores/blueprinit/adopt.js +137 -0
  26. package/dist/cores/blueprinit/columns.d.ts +26 -0
  27. package/dist/cores/blueprinit/columns.js +27 -0
  28. package/dist/cores/blueprinit/finish.d.ts +15 -0
  29. package/dist/cores/blueprinit/finish.js +72 -0
  30. package/dist/cores/blueprinit/fresh.d.ts +3 -0
  31. package/dist/cores/blueprinit/fresh.js +219 -0
  32. package/dist/cores/blueprinit/naming.d.ts +27 -0
  33. package/dist/cores/blueprinit/naming.js +43 -0
  34. package/dist/cores/blueprinit/preview.d.ts +34 -0
  35. package/dist/cores/blueprinit/preview.js +110 -0
  36. package/dist/cores/blueprinit/project-choice.d.ts +28 -0
  37. package/dist/cores/blueprinit/project-choice.js +82 -0
  38. package/dist/cores/blueprinit/prompts.d.ts +13 -0
  39. package/dist/cores/blueprinit/prompts.js +42 -0
  40. package/dist/cores/blueprinit/results.d.ts +8 -0
  41. package/dist/cores/blueprinit/results.js +4 -0
  42. package/dist/cores/blueprinit/selection.d.ts +15 -0
  43. package/dist/cores/blueprinit/selection.js +90 -0
  44. package/dist/cores/blueprinit/stack-name.d.ts +39 -0
  45. package/dist/cores/blueprinit/stack-name.js +56 -0
  46. package/dist/cores/blueprinit/target.d.ts +33 -0
  47. package/dist/cores/blueprinit/target.js +80 -0
  48. package/dist/cores/blueprinit/types.d.ts +20 -0
  49. package/dist/cores/blueprinit/types.js +1 -0
  50. package/dist/cores/blueprinit.d.ts +7 -0
  51. package/dist/cores/blueprinit.js +64 -0
  52. package/dist/cores/functions/test.js +41 -26
  53. package/dist/utils/display/intro.d.ts +6 -0
  54. package/dist/utils/display/intro.js +63 -0
  55. package/dist/utils/functions/child-process-wrapper.js +3 -2
  56. package/dist/utils/functions/child-runtime/durable.d.ts +2 -2
  57. package/dist/utils/functions/child-runtime/durable.js +52 -2
  58. package/dist/utils/functions/durable-logs.d.ts +16 -0
  59. package/dist/utils/functions/durable-logs.js +52 -0
  60. package/dist/utils/functions/invoke/local.js +2 -1
  61. package/dist/utils/types.d.ts +18 -4
  62. package/oclif.manifest.json +159 -1
  63. package/package.json +1 -1
  64. package/skills/init.md +199 -0
@@ -0,0 +1,27 @@
1
+ import { type DatasetAction } from './manifest.js';
2
+ import { type StudioVersions } from './templates.js';
3
+ export interface WorkspaceOptions {
4
+ dir: string;
5
+ projectId: string;
6
+ displayName: string;
7
+ datasetName: string;
8
+ /** Attach a dataset the project already has, or create one. Defaults to attach. */
9
+ datasetAction?: DatasetAction;
10
+ stackName: string;
11
+ /** Blueprint-local name for the attached project resource. */
12
+ projectName: string;
13
+ studioName: string;
14
+ studioSlug: string;
15
+ studioOrigin: string;
16
+ blueprintsVersion: string;
17
+ }
18
+ export interface ScaffoldOptions extends WorkspaceOptions {
19
+ /** Resolved at scaffold time, the way `sanity init` resolves its own Studio versions. */
20
+ studioVersions: StudioVersions;
21
+ }
22
+ export interface WorkspaceResult {
23
+ blueprintFilePath: string;
24
+ files: string[];
25
+ }
26
+ export declare function scaffoldWorkspace(options: ScaffoldOptions): WorkspaceResult;
27
+ export declare function adoptWorkspace(options: WorkspaceOptions): WorkspaceResult;
@@ -0,0 +1,145 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, posix } from 'node:path';
3
+ import { renderBlueprint, STARTER_FUNCTION_NAME, studioSourcePath, } from './manifest.js';
4
+ import { FUNCTION_SET_SLUG, functionPackageJson, renderStudioCliConfig, renderStudioConfig, STUDIO_POST_TYPE, STUDIO_SCHEMA_TYPES, STUDIO_TSCONFIG, studioPackageJson, } from './templates.js';
5
+ const GITIGNORE_ENTRY = '# Sanity Blueprints\n.sanity/';
6
+ const DEFAULT_GITIGNORE = `node_modules
7
+ .env
8
+
9
+ ${GITIGNORE_ENTRY}
10
+ `;
11
+ export function scaffoldWorkspace(options) {
12
+ const { dir, studioName, studioVersions } = options;
13
+ assertTargetsAvailable(dir, [
14
+ studioSourcePath(studioName),
15
+ posix.join('functions', STARTER_FUNCTION_NAME),
16
+ ]);
17
+ const packageJson = preparePackageJson(options, ['studios/*', 'functions/*']);
18
+ const files = [];
19
+ const write = createWriter(dir, files);
20
+ const blueprintFilePath = write('sanity.blueprint.ts', renderBlueprint({
21
+ ...manifestOptions(options),
22
+ studioSrc: `./${studioSourcePath(studioName)}`,
23
+ withFunction: true,
24
+ }));
25
+ write(posix.join('studios', studioName, 'package.json'), formatJson(studioPackageJson(studioName, studioVersions)));
26
+ write(posix.join('studios', studioName, 'tsconfig.json'), formatJson(STUDIO_TSCONFIG));
27
+ write(posix.join('studios', studioName, 'sanity.config.ts'), renderStudioConfig(options));
28
+ write(posix.join('studios', studioName, 'sanity.cli.ts'), renderStudioCliConfig(options));
29
+ write(posix.join('studios', studioName, 'schemaTypes', 'index.ts'), STUDIO_SCHEMA_TYPES);
30
+ write(posix.join('studios', studioName, 'schemaTypes', 'post.ts'), STUDIO_POST_TYPE);
31
+ write(posix.join('studios', studioName, 'static', '.gitkeep'), '');
32
+ write(posix.join('functions', STARTER_FUNCTION_NAME, 'package.json'), formatJson(functionPackageJson(STARTER_FUNCTION_NAME)));
33
+ write(posix.join('functions', STARTER_FUNCTION_NAME, 'index.ts'), FUNCTION_SET_SLUG);
34
+ writePreparedRootFiles(dir, packageJson, files);
35
+ return { blueprintFilePath, files };
36
+ }
37
+ export function adoptWorkspace(options) {
38
+ const { dir } = options;
39
+ const packageJson = preparePackageJson(options, []);
40
+ const files = [];
41
+ const write = createWriter(dir, files);
42
+ const blueprintFilePath = write('sanity.blueprint.ts', renderBlueprint({ ...manifestOptions(options), studioSrc: '.', withFunction: false }));
43
+ writePreparedRootFiles(dir, packageJson, files);
44
+ return { blueprintFilePath, files };
45
+ }
46
+ function manifestOptions(options) {
47
+ return {
48
+ projectName: options.projectName,
49
+ displayName: options.displayName,
50
+ projectId: options.projectId,
51
+ datasetName: options.datasetName,
52
+ datasetAction: options.datasetAction ?? 'attach',
53
+ studioName: options.studioName,
54
+ studioSlug: options.studioSlug,
55
+ studioOrigin: options.studioOrigin,
56
+ };
57
+ }
58
+ function assertTargetsAvailable(dir, paths) {
59
+ const conflicts = paths.filter((path) => existsSync(join(dir, path)));
60
+ if (conflicts.length > 0) {
61
+ throw new Error(`Refusing to overwrite existing paths: ${conflicts.join(', ')}`);
62
+ }
63
+ }
64
+ function createWriter(dir, files) {
65
+ return (relativePath, content) => {
66
+ const filePath = join(dir, relativePath);
67
+ mkdirSync(dirname(filePath), { recursive: true });
68
+ writeFileSync(filePath, content);
69
+ files.push(relativePath);
70
+ return filePath;
71
+ };
72
+ }
73
+ function writePreparedRootFiles(dir, packageJson, files) {
74
+ if (writeGitignore(dir))
75
+ files.push('.gitignore');
76
+ writeFileSync(join(dir, 'package.json'), formatJson(packageJson));
77
+ files.push('package.json');
78
+ }
79
+ function writeGitignore(dir) {
80
+ const filePath = join(dir, '.gitignore');
81
+ if (!existsSync(filePath)) {
82
+ writeFileSync(filePath, DEFAULT_GITIGNORE);
83
+ return true;
84
+ }
85
+ const existing = readFileSync(filePath, 'utf8');
86
+ if (existing.split(/\r?\n/).some((line) => line.trim() === '.sanity/'))
87
+ return false;
88
+ writeFileSync(filePath, `${existing.trimEnd()}\n\n${GITIGNORE_ENTRY}\n`);
89
+ return true;
90
+ }
91
+ function preparePackageJson(options, requiredWorkspaces) {
92
+ const filePath = join(options.dir, 'package.json');
93
+ const parsed = existsSync(filePath)
94
+ ? JSON.parse(readFileSync(filePath, 'utf8'))
95
+ : {
96
+ name: options.stackName,
97
+ version: '0.1.0',
98
+ private: true,
99
+ type: 'module',
100
+ };
101
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
102
+ throw new Error('The existing package.json must contain a JSON object.');
103
+ }
104
+ const packageJson = parsed;
105
+ addWorkspaces(packageJson, requiredWorkspaces);
106
+ addBlueprintsDependency(packageJson, options.blueprintsVersion);
107
+ return packageJson;
108
+ }
109
+ function addWorkspaces(packageJson, required) {
110
+ if (required.length === 0)
111
+ return;
112
+ if (!packageJson.workspaces) {
113
+ packageJson.workspaces = required;
114
+ return;
115
+ }
116
+ if (Array.isArray(packageJson.workspaces)) {
117
+ packageJson.workspaces = [...new Set([...packageJson.workspaces, ...required])];
118
+ return;
119
+ }
120
+ // Neither falsy nor an array by now, so the only supported shape left is {packages: []}.
121
+ const workspaces = packageJson.workspaces;
122
+ if (typeof workspaces === 'object' && Array.isArray(workspaces.packages)) {
123
+ workspaces.packages = [...new Set([...workspaces.packages, ...required])];
124
+ return;
125
+ }
126
+ throw new Error('Cannot add generated packages to the existing package.json workspaces.');
127
+ }
128
+ function addBlueprintsDependency(packageJson, blueprintsVersion) {
129
+ const dependencies = recordOrEmpty(packageJson.dependencies);
130
+ const devDependencies = recordOrEmpty(packageJson.devDependencies);
131
+ if (dependencies['@sanity/blueprints'] || devDependencies['@sanity/blueprints'])
132
+ return;
133
+ packageJson.devDependencies = {
134
+ ...devDependencies,
135
+ '@sanity/blueprints': blueprintsVersion,
136
+ };
137
+ }
138
+ function recordOrEmpty(value) {
139
+ return value && typeof value === 'object' && !Array.isArray(value)
140
+ ? value
141
+ : {};
142
+ }
143
+ function formatJson(value) {
144
+ return `${JSON.stringify(value, null, 2)}\n`;
145
+ }
@@ -73,6 +73,8 @@ async function preDeployStack(options) {
73
73
  };
74
74
  }
75
75
  spinner.stop().clear();
76
+ if (stack.changeCount === 0)
77
+ return { success: true };
76
78
  logger(styleText('dim', 'Stack deployment progress:'));
77
79
  logger('');
78
80
  const outcome = await waitForOperation({
@@ -48,6 +48,7 @@ type UpdateStackResponse = {
48
48
  error: null;
49
49
  stack: Stack & {
50
50
  operationId: string;
51
+ changeCount?: number;
51
52
  };
52
53
  } | {
53
54
  ok: false;
@@ -0,0 +1,19 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { ActionResponse } from '../../utils/types.js';
3
+ import { type Project } from './projects.js';
4
+ export interface CreateProjectResult extends ActionResponse {
5
+ project?: Project;
6
+ }
7
+ /**
8
+ * Create a project in an organization.
9
+ *
10
+ * `createDefaultCorsOrigin` is false because the Blueprint declares the only origin the
11
+ * hosted Studio needs. The Blueprints project provider sends the same field for the same
12
+ * reason. No dataset comes back: this endpoint creates none, so the Blueprint creates it.
13
+ */
14
+ export declare function createProject({ token, organizationId, displayName, logger, }: {
15
+ token: string;
16
+ organizationId: string;
17
+ displayName: string;
18
+ logger: Logger;
19
+ }): Promise<CreateProjectResult>;
@@ -0,0 +1,32 @@
1
+ import { createTracedFetch } from '../../utils/traced-fetch.js';
2
+ import { projectsApiPath } from './projects.js';
3
+ /**
4
+ * Create a project in an organization.
5
+ *
6
+ * `createDefaultCorsOrigin` is false because the Blueprint declares the only origin the
7
+ * hosted Studio needs. The Blueprints project provider sends the same field for the same
8
+ * reason. No dataset comes back: this endpoint creates none, so the Blueprint creates it.
9
+ */
10
+ export async function createProject({ token, organizationId, displayName, logger, }) {
11
+ const fetchFn = createTracedFetch(logger);
12
+ try {
13
+ const response = await fetchFn(projectsApiPath, {
14
+ method: 'POST',
15
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({
17
+ displayName,
18
+ organizationId,
19
+ createDefaultCorsOrigin: false,
20
+ metadata: { integration: 'blueprints' },
21
+ }),
22
+ });
23
+ const body = await response.json();
24
+ if (!response.ok) {
25
+ return { ok: false, error: body?.error?.message || body?.message || `HTTP ${response.status}` };
26
+ }
27
+ return { ok: true, error: null, project: body };
28
+ }
29
+ catch (error) {
30
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
31
+ }
32
+ }
@@ -0,0 +1,15 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { ActionResponse } from '../../utils/types.js';
3
+ export interface Dataset {
4
+ name: string;
5
+ aclMode: string;
6
+ }
7
+ interface ListDatasetsResponse extends ActionResponse {
8
+ datasets: Dataset[];
9
+ }
10
+ export declare function listDatasets({ token, projectId, logger, }: {
11
+ token: string;
12
+ projectId: string;
13
+ logger: Logger;
14
+ }): Promise<ListDatasetsResponse>;
15
+ export {};
@@ -0,0 +1,26 @@
1
+ import { createTracedFetch } from '../../utils/traced-fetch.js';
2
+ import { projectsApiPath } from './projects.js';
3
+ export async function listDatasets({ token, projectId, logger, }) {
4
+ const fetchFn = createTracedFetch(logger);
5
+ const url = `${projectsApiPath}/${encodeURIComponent(projectId)}/datasets`;
6
+ const response = await fetchFn(url, {
7
+ method: 'GET',
8
+ headers: {
9
+ Authorization: `Bearer ${token}`,
10
+ 'Content-Type': 'application/json',
11
+ },
12
+ });
13
+ const body = await response.json();
14
+ if (!response.ok) {
15
+ return {
16
+ ok: false,
17
+ error: body.error?.message ?? response.statusText,
18
+ datasets: [],
19
+ };
20
+ }
21
+ return {
22
+ ok: true,
23
+ error: null,
24
+ datasets: body,
25
+ };
26
+ }
@@ -25,6 +25,7 @@ export default class TestCommand extends ResolvedCommand<typeof TestCommand> {
25
25
  'document-id-after': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
26
  'with-user-token': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
27
  'media-library-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
+ 'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
28
29
  };
29
30
  run(): Promise<Record<string, unknown> | undefined>;
30
31
  }
@@ -146,6 +146,11 @@ Provide test data via --data (inline JSON), --file (JSON file), or --document-id
146
146
  exclusive: ['project-id', 'dataset'],
147
147
  required: false,
148
148
  }),
149
+ 'no-wait': Flags.boolean({
150
+ description: 'Skip durable wait delays instead of sleeping',
151
+ default: false,
152
+ required: false,
153
+ }),
149
154
  };
150
155
  async run() {
151
156
  const { name } = this.args;
@@ -0,0 +1,21 @@
1
+ import { RuntimeCommand } from '../baseCommands.js';
2
+ export default class InitCommand extends RuntimeCommand<typeof InitCommand> {
3
+ static hidden: boolean;
4
+ static summary: string;
5
+ static description: string;
6
+ static examples: string[];
7
+ static args: {
8
+ directory: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
9
+ };
10
+ static flags: {
11
+ llms: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ 'organization-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ 'create-project': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ dataset: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ 'stack-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ 'studio-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ };
20
+ run(): Promise<Record<string, unknown> | undefined>;
21
+ }
@@ -0,0 +1,82 @@
1
+ import { resolve } from 'node:path';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { llmsFlagConfig } from '@sanity-labs/oclif-plugin-skills-flag';
4
+ import { RuntimeCommand } from '../baseCommands.js';
5
+ import { blueprinitCore } from '../cores/blueprinit.js';
6
+ import { playIntro } from '../utils/display/intro.js';
7
+ import { Logger } from '../utils/logger.js';
8
+ export default class InitCommand extends RuntimeCommand {
9
+ static hidden = true;
10
+ static summary = 'Experimental blueprints-first project init';
11
+ static description = `Scaffolds a Blueprint, Studio, and starter function around a Sanity project.
12
+
13
+ Use a project you already have, or create one with --create-project. A new project has no
14
+ datasets, so the Blueprint creates the dataset rather than attaching it.
15
+
16
+ Pass a directory to scaffold into it. Without one, an interactive run asks where the files go.
17
+
18
+ This hidden command is an experiment; its interface and generated files are not stable.`;
19
+ static examples = [
20
+ '<%= config.bin %> <%= command.id %>',
21
+ '<%= config.bin %> <%= command.id %> ./my-workspace',
22
+ '<%= config.bin %> <%= command.id %> --yes',
23
+ '<%= config.bin %> <%= command.id %> --project-id <id> --dataset <name> --yes',
24
+ '<%= config.bin %> <%= command.id %> --create-project "My Shop" --yes',
25
+ ];
26
+ static args = {
27
+ directory: Args.string({
28
+ description: 'Directory to initialize. Interactive runs are asked for one instead.',
29
+ required: false,
30
+ }),
31
+ };
32
+ static flags = {
33
+ llms: Flags.boolean(llmsFlagConfig),
34
+ yes: Flags.boolean({
35
+ char: 'y',
36
+ description: 'Skip confirmation prompts',
37
+ default: false,
38
+ }),
39
+ 'organization-id': Flags.string({
40
+ description: 'Organization to use when the account has several',
41
+ aliases: ['organization', 'organizationId', 'org'],
42
+ }),
43
+ 'project-id': Flags.string({
44
+ description: 'Project to use when the organization has several',
45
+ aliases: ['project', 'projectId'],
46
+ }),
47
+ 'create-project': Flags.string({
48
+ description: 'Create a project with this display name instead of using an existing one',
49
+ aliases: ['createProject', 'new-project'],
50
+ exclusive: ['project-id'],
51
+ }),
52
+ dataset: Flags.string({
53
+ description: 'Dataset to attach, or to create alongside a new project',
54
+ }),
55
+ 'stack-name': Flags.string({
56
+ description: 'Name for the new organization-scoped Stack, and the hosted Studio slug',
57
+ }),
58
+ 'studio-name': Flags.string({
59
+ description: 'Blueprint-local name for the Studio resource and its directory',
60
+ }),
61
+ };
62
+ async run() {
63
+ await playIntro({ json: this.jsonEnabled() });
64
+ const result = await blueprinitCore({
65
+ bin: this.config.bin,
66
+ dir: this.args.directory ? resolve(this.args.directory) : undefined,
67
+ log: Logger(this.log.bind(this), this.flags),
68
+ flags: {
69
+ yes: this.flags.yes,
70
+ organizationId: this.flags['organization-id'],
71
+ projectId: this.flags['project-id'],
72
+ createProject: this.flags['create-project'],
73
+ datasetName: this.flags.dataset,
74
+ stackName: this.flags['stack-name'],
75
+ studioName: this.flags['studio-name'],
76
+ },
77
+ });
78
+ if (!result.success)
79
+ this.coreError(result);
80
+ return result.json;
81
+ }
82
+ }
@@ -0,0 +1,6 @@
1
+ import type { DetectedStudio } from '../../actions/blueprinit/studio.js';
2
+ import type { CoreResult } from '../index.js';
3
+ import type { BlueprinitFlowOptions } from './types.js';
4
+ export declare function adoptFlow(options: BlueprinitFlowOptions & {
5
+ studio: DetectedStudio;
6
+ }): Promise<CoreResult>;
@@ -0,0 +1,137 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { corsResourceName, projectBaseName, projectResourceName, } from '../../actions/blueprinit/manifest.js';
4
+ import { getLatestNpmRange } from '../../actions/blueprinit/versions.js';
5
+ import { adoptWorkspace } from '../../actions/blueprinit/workspace.js';
6
+ import { listDatasets } from '../../actions/sanity/datasets.js';
7
+ import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
8
+ import { check } from '../../utils/display/presenters.js';
9
+ import { isInteractive } from '../../utils/interactive.js';
10
+ import { styleText } from '../../utils/style-text.js';
11
+ import { alignSteps, createStackAndFinish } from './finish.js';
12
+ import { chooseNames } from './naming.js';
13
+ import { fileLines, printBlueprintPreview } from './preview.js';
14
+ import { confirmUnlessSkipped, studioResourceName } from './prompts.js';
15
+ import { coreFailure } from './results.js';
16
+ import { findProject } from './selection.js';
17
+ export async function adoptFlow(options) {
18
+ const { bin, dir, flags, log, studio, token } = options;
19
+ if (flags.projectId && studio.projectId && flags.projectId !== studio.projectId) {
20
+ return { success: false, error: `The Studio uses project "${studio.projectId}".` };
21
+ }
22
+ if (flags.datasetName && studio.dataset && flags.datasetName !== studio.dataset) {
23
+ return { success: false, error: `The Studio uses dataset "${studio.dataset}".` };
24
+ }
25
+ const projectId = studio.projectId ?? flags.projectId;
26
+ const datasetName = studio.dataset ?? flags.datasetName;
27
+ if (!projectId || !datasetName) {
28
+ return {
29
+ success: false,
30
+ error: `Found ${studio.configFile}, but could not read a projectId and dataset from it.`,
31
+ suggestions: ['Use literal values in the Studio config or pass --project-id and --dataset.'],
32
+ };
33
+ }
34
+ log(check(`Studio detected (${studio.configFile})`));
35
+ const grouped = await groupProjectsByOrganization({ token, logger: log });
36
+ if (!grouped.ok) {
37
+ return { success: false, error: `Could not list organizations: ${grouped.error}` };
38
+ }
39
+ const match = findProject(grouped.organizations, projectId);
40
+ if (!match) {
41
+ return {
42
+ success: false,
43
+ error: `Project "${projectId}" from ${studio.configFile} was not found in your account.`,
44
+ };
45
+ }
46
+ if (flags.organizationId && flags.organizationId !== match.organization.id) {
47
+ return {
48
+ success: false,
49
+ error: `Project "${studio.projectId}" belongs to organization "${match.organization.id}".`,
50
+ };
51
+ }
52
+ const datasets = await listDatasets({ token, projectId: match.project.id, logger: log });
53
+ if (!datasets.ok) {
54
+ return { success: false, error: `Could not list datasets: ${datasets.error}` };
55
+ }
56
+ if (!datasets.datasets.some(({ name }) => name === datasetName)) {
57
+ return {
58
+ success: false,
59
+ error: `Dataset "${datasetName}" was not found in project ${match.project.displayName}.`,
60
+ };
61
+ }
62
+ log(check(`Matches project ${match.project.displayName} (${match.project.id}), dataset ${datasetName}`));
63
+ log('');
64
+ const baseName = projectBaseName(match.project.displayName, match.project.id);
65
+ const projectName = projectResourceName(baseName);
66
+ const studioName = studioResourceName(flags);
67
+ const names = await chooseNames({
68
+ flags,
69
+ log,
70
+ token,
71
+ organizationId: match.organization.id,
72
+ baseName,
73
+ datasetName,
74
+ resourceNames: [projectName, datasetName, corsResourceName(studioName), studioName],
75
+ });
76
+ if (!names.ok)
77
+ return coreFailure(names.error);
78
+ const { stackName, studioOrigin } = names.value;
79
+ // Started before the preview so the lookup overlaps the time the reader spends on it.
80
+ const blueprintsVersion = getLatestNpmRange('@sanity/blueprints', log);
81
+ const marker = (file) => (existsSync(join(dir, file)) ? '~' : '+');
82
+ const { attaches, creates } = printBlueprintPreview({
83
+ log,
84
+ organizationName: match.organization.name,
85
+ projectId: match.project.id,
86
+ projectName: match.project.displayName,
87
+ datasetName,
88
+ datasetAction: 'attach',
89
+ studioName,
90
+ studioOrigin,
91
+ studioSource: '.',
92
+ stackName,
93
+ });
94
+ log(styleText('bold', 'Changes:'));
95
+ for (const line of fileLines([
96
+ ['+', 'sanity.blueprint.ts', "attach project + dataset; add CORS and Studio at '.'"],
97
+ ['+', '.sanity/blueprint.config.json', `org scope, stack ${stackName}`],
98
+ [marker('.gitignore'), '.gitignore'],
99
+ [marker('package.json'), 'package.json'],
100
+ ])) {
101
+ log(line);
102
+ }
103
+ log('');
104
+ const confirmed = await confirmUnlessSkipped(flags, isInteractive, 'Adopt this Studio into the Blueprint?');
105
+ if (!confirmed)
106
+ return { success: false, error: 'Initialization cancelled.' };
107
+ const adoption = adoptWorkspace({
108
+ dir,
109
+ projectId: match.project.id,
110
+ displayName: match.project.displayName,
111
+ datasetName,
112
+ stackName,
113
+ projectName,
114
+ studioName,
115
+ studioSlug: stackName,
116
+ studioOrigin,
117
+ blueprintsVersion: await blueprintsVersion,
118
+ });
119
+ return await createStackAndFinish({
120
+ ...options,
121
+ organizationId: match.organization.id,
122
+ projectId: match.project.id,
123
+ datasetName,
124
+ stackName,
125
+ blueprintFilePath: adoption.blueprintFilePath,
126
+ files: adoption.files,
127
+ nextSteps: [
128
+ ...alignSteps([
129
+ [`npx ${bin} blueprints plan`, `preview the ${attaches} attaches and ${creates} creates`],
130
+ [`npx ${bin} blueprints deploy`, 'attach and create them'],
131
+ [`npx ${bin} blueprints info`, 'review what the Stack owns'],
132
+ ]),
133
+ '',
134
+ `Your Studio is hosted at ${studioOrigin} after deploy.`,
135
+ ],
136
+ });
137
+ }
@@ -0,0 +1,26 @@
1
+ import { styleText } from '../../utils/style-text.js';
2
+ type TextStyle = Parameters<typeof styleText>[0];
3
+ /**
4
+ * One cell in an aligned row.
5
+ *
6
+ * `text` is the raw string. It sets the column width and is styled only after padding,
7
+ * so a colour code can never widen a column.
8
+ */
9
+ export interface Cell {
10
+ text: string;
11
+ style?: TextStyle;
12
+ /**
13
+ * Spaces between this column and the next, beyond the padding. Defaults to 1.
14
+ *
15
+ * A gap is a property of the column, so one row may set it for every row.
16
+ */
17
+ gap?: number;
18
+ }
19
+ /**
20
+ * Render rows as columns of one shared width, then style each cell.
21
+ *
22
+ * The last cell in a row is never padded, so a row that stops early prints no trailing
23
+ * space. Pass every row that should line up in one call.
24
+ */
25
+ export declare function renderRows(rows: Cell[][]): string[];
26
+ export {};
@@ -0,0 +1,27 @@
1
+ import { styleText } from '../../utils/style-text.js';
2
+ const INDENT = ' ';
3
+ /**
4
+ * Render rows as columns of one shared width, then style each cell.
5
+ *
6
+ * The last cell in a row is never padded, so a row that stops early prints no trailing
7
+ * space. Pass every row that should line up in one call.
8
+ */
9
+ export function renderRows(rows) {
10
+ const widths = [];
11
+ const gaps = [];
12
+ for (const row of rows) {
13
+ row.forEach((cell, index) => {
14
+ widths[index] = Math.max(widths[index] ?? 0, cell.text.length);
15
+ gaps[index] = Math.max(gaps[index] ?? 1, cell.gap ?? 1);
16
+ });
17
+ }
18
+ return rows.map((row) => {
19
+ const cells = row.map((cell, index) => {
20
+ const styled = cell.style ? styleText(cell.style, cell.text) : cell.text;
21
+ if (index === row.length - 1)
22
+ return styled;
23
+ return styled + ' '.repeat(widths[index] - cell.text.length + gaps[index]);
24
+ });
25
+ return INDENT + cells.join('');
26
+ });
27
+ }
@@ -0,0 +1,15 @@
1
+ import type { CoreResult } from '../index.js';
2
+ import type { BlueprinitFlowOptions } from './types.js';
3
+ interface FinishOptions extends BlueprinitFlowOptions {
4
+ organizationId: string;
5
+ projectId: string;
6
+ datasetName: string;
7
+ stackName: string;
8
+ blueprintFilePath: string;
9
+ files: string[];
10
+ nextSteps: string[];
11
+ }
12
+ export declare function createStackAndFinish(options: FinishOptions): Promise<CoreResult>;
13
+ /** A command to run next, and what it does. */
14
+ export declare function alignSteps(steps: Array<[command: string, description?: string]>): string[];
15
+ export {};