@sanity/runtime-cli 17.9.0-blueprinit.g14aaebd → 17.10.0-next.g5e0bba6

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 (73) hide show
  1. package/README.md +19 -19
  2. package/dist/actions/blueprints/blueprint.js +1 -1
  3. package/dist/actions/blueprints/pre-deploy.js +7 -6
  4. package/dist/actions/blueprints/wait-for-operation.js +2 -1
  5. package/dist/cores/blueprints/config.js +18 -8
  6. package/dist/cores/blueprints/deploy.js +9 -8
  7. package/dist/cores/blueprints/destroy.js +7 -6
  8. package/dist/cores/blueprints/doctor.js +15 -2
  9. package/dist/cores/blueprints/init.d.ts +2 -0
  10. package/dist/cores/blueprints/init.js +56 -20
  11. package/dist/cores/blueprints/plan.js +11 -6
  12. package/dist/cores/blueprints/promote.js +3 -2
  13. package/dist/cores/functions/add.js +11 -7
  14. package/dist/cores/functions/logs.js +50 -6
  15. package/dist/cores/index.d.ts +5 -0
  16. package/dist/utils/blueprints/hints.js +8 -3
  17. package/dist/utils/display/prompt.d.ts +15 -2
  18. package/dist/utils/display/prompt.js +70 -8
  19. package/dist/utils/functions/child-runtime/durable.js +19 -2
  20. package/dist/utils/package-manager.d.ts +20 -0
  21. package/dist/utils/package-manager.js +63 -0
  22. package/dist/utils/types.d.ts +10 -1
  23. package/oclif.manifest.json +1 -152
  24. package/package.json +2 -2
  25. package/dist/actions/blueprinit/manifest.d.ts +0 -46
  26. package/dist/actions/blueprinit/manifest.js +0 -121
  27. package/dist/actions/blueprinit/quote.d.ts +0 -6
  28. package/dist/actions/blueprinit/quote.js +0 -8
  29. package/dist/actions/blueprinit/studio.d.ts +0 -6
  30. package/dist/actions/blueprinit/studio.js +0 -43
  31. package/dist/actions/blueprinit/templates.d.ts +0 -73
  32. package/dist/actions/blueprinit/templates.js +0 -138
  33. package/dist/actions/blueprinit/versions.d.ts +0 -8
  34. package/dist/actions/blueprinit/versions.js +0 -11
  35. package/dist/actions/blueprinit/workspace.d.ts +0 -27
  36. package/dist/actions/blueprinit/workspace.js +0 -145
  37. package/dist/actions/sanity/create-project.d.ts +0 -19
  38. package/dist/actions/sanity/create-project.js +0 -32
  39. package/dist/actions/sanity/datasets.d.ts +0 -15
  40. package/dist/actions/sanity/datasets.js +0 -26
  41. package/dist/commands/init.d.ts +0 -21
  42. package/dist/commands/init.js +0 -82
  43. package/dist/cores/blueprinit/adopt.d.ts +0 -6
  44. package/dist/cores/blueprinit/adopt.js +0 -137
  45. package/dist/cores/blueprinit/columns.d.ts +0 -26
  46. package/dist/cores/blueprinit/columns.js +0 -27
  47. package/dist/cores/blueprinit/finish.d.ts +0 -15
  48. package/dist/cores/blueprinit/finish.js +0 -72
  49. package/dist/cores/blueprinit/fresh.d.ts +0 -3
  50. package/dist/cores/blueprinit/fresh.js +0 -219
  51. package/dist/cores/blueprinit/naming.d.ts +0 -27
  52. package/dist/cores/blueprinit/naming.js +0 -43
  53. package/dist/cores/blueprinit/preview.d.ts +0 -34
  54. package/dist/cores/blueprinit/preview.js +0 -110
  55. package/dist/cores/blueprinit/project-choice.d.ts +0 -28
  56. package/dist/cores/blueprinit/project-choice.js +0 -82
  57. package/dist/cores/blueprinit/prompts.d.ts +0 -13
  58. package/dist/cores/blueprinit/prompts.js +0 -42
  59. package/dist/cores/blueprinit/results.d.ts +0 -8
  60. package/dist/cores/blueprinit/results.js +0 -4
  61. package/dist/cores/blueprinit/selection.d.ts +0 -15
  62. package/dist/cores/blueprinit/selection.js +0 -90
  63. package/dist/cores/blueprinit/stack-name.d.ts +0 -39
  64. package/dist/cores/blueprinit/stack-name.js +0 -56
  65. package/dist/cores/blueprinit/target.d.ts +0 -33
  66. package/dist/cores/blueprinit/target.js +0 -80
  67. package/dist/cores/blueprinit/types.d.ts +0 -20
  68. package/dist/cores/blueprinit/types.js +0 -1
  69. package/dist/cores/blueprinit.d.ts +0 -7
  70. package/dist/cores/blueprinit.js +0 -64
  71. package/dist/utils/display/intro.d.ts +0 -6
  72. package/dist/utils/display/intro.js +0 -63
  73. package/skills/init.md +0 -199
@@ -1,90 +0,0 @@
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
- }
@@ -1,39 +0,0 @@
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 {};
@@ -1,56 +0,0 @@
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
- }
@@ -1,33 +0,0 @@
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 {};
@@ -1,80 +0,0 @@
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
- }
@@ -1,20 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,7 +0,0 @@
1
- import type { BlueprinitFlags } from './blueprinit/types.js';
2
- import type { CoreConfig, CoreResult } from './index.js';
3
- export interface BlueprinitOptions extends CoreConfig {
4
- dir?: string;
5
- flags?: BlueprinitFlags;
6
- }
7
- export declare function blueprinitCore(options: BlueprinitOptions): Promise<CoreResult>;
@@ -1,64 +0,0 @@
1
- import { existsSync, statSync } from 'node:fs';
2
- import { cwd } from 'node:process';
3
- import { detectStudio } from '../actions/blueprinit/studio.js';
4
- import { findBlueprintFile } from '../actions/blueprints/blueprint.js';
5
- import { readConfigFile } from '../actions/blueprints/config.js';
6
- import { validTokenOrErrorMessage } from '../utils/validated-token.js';
7
- import { adoptFlow } from './blueprinit/adopt.js';
8
- import { freshFlow } from './blueprinit/fresh.js';
9
- export async function blueprinitCore(options) {
10
- const { bin, log, flags = {}, dir = cwd() } = options;
11
- try {
12
- // findBlueprintFile answers with any file it is handed, so refuse a file target first.
13
- if (existsSync(dir) && !statSync(dir).isDirectory()) {
14
- return {
15
- success: false,
16
- error: `${dir} is not a directory.`,
17
- suggestions: ['Pass a directory to initialize, or no argument to use this one.'],
18
- };
19
- }
20
- const existingBlueprint = findBlueprintFile(dir);
21
- if (existingBlueprint) {
22
- const blueprintConfig = readConfigFile(existingBlueprint.blueprintFilePath);
23
- if (!blueprintConfig?.stackId) {
24
- return {
25
- success: false,
26
- error: `A Blueprint exists here (${existingBlueprint.fileName}), but it is not connected to a Stack.`,
27
- suggestions: [`Finish setup with: npx ${bin} blueprints init`],
28
- };
29
- }
30
- log(`A blueprint already exists here (${existingBlueprint.fileName}).`);
31
- log('');
32
- log('Use these instead:');
33
- log(` npx ${bin} blueprints config --edit`);
34
- log(` npx ${bin} blueprints doctor`);
35
- return {
36
- success: true,
37
- json: { alreadyInitialized: true, blueprintFile: existingBlueprint.fileName },
38
- };
39
- }
40
- const tokenCheck = await validTokenOrErrorMessage(log);
41
- if (!tokenCheck.ok) {
42
- return {
43
- success: false,
44
- error: 'No Sanity session found.',
45
- suggestions: ['Sign in first: npx sanity login', 'Then run init again.'],
46
- };
47
- }
48
- log('Setting up your Sanity workspace.');
49
- log('');
50
- const flowOptions = {
51
- bin,
52
- dir,
53
- targetProvided: Boolean(options.dir),
54
- flags,
55
- log,
56
- token: tokenCheck.value,
57
- };
58
- const studio = detectStudio(dir);
59
- return studio ? await adoptFlow({ ...flowOptions, studio }) : await freshFlow(flowOptions);
60
- }
61
- catch (error) {
62
- return { success: false, error: error instanceof Error ? error.message : String(error) };
63
- }
64
- }
@@ -1,6 +0,0 @@
1
- /** A left-to-right reveal of the wordmark; the last frame is the finished line. */
2
- export declare function introFrames(): string[];
3
- /** Print the intro, animated when the terminal can show it and static when it cannot. */
4
- export declare function playIntro({ json }?: {
5
- json?: boolean;
6
- }): Promise<void>;
@@ -1,63 +0,0 @@
1
- import { env, stdout } from 'node:process';
2
- import { onExit } from 'signal-exit';
3
- import { isInteractive } from '../interactive.js';
4
- import { styleText } from '../style-text.js';
5
- const INDENT = ' ';
6
- const WORDMARK = 'sanity';
7
- const TAIL = ' with blueprints';
8
- /** The leading edge of the reveal, densest first. */
9
- const GRADIENT = ['▓', '▒', '░'];
10
- const REVEAL_PER_FRAME = 1;
11
- const FRAME_MS = 45;
12
- /** Time the finished wordmark holds before the command's own output follows. */
13
- const HOLD_MS = 250;
14
- const HIDE_CURSOR = '\x1b[?25l';
15
- const SHOW_CURSOR = '\x1b[?25h';
16
- const CLEAR_LINE = '\x1b[2K';
17
- function style(revealed, edge) {
18
- const wordmark = revealed.slice(0, WORDMARK.length);
19
- const tail = revealed.slice(WORDMARK.length);
20
- return INDENT + styleText(['bold', 'red'], wordmark) + styleText('dim', tail + edge);
21
- }
22
- /** A left-to-right reveal of the wordmark; the last frame is the finished line. */
23
- export function introFrames() {
24
- const full = WORDMARK + TAIL;
25
- const frames = [];
26
- for (let revealed = REVEAL_PER_FRAME; revealed < full.length; revealed += REVEAL_PER_FRAME) {
27
- const edge = GRADIENT.slice(0, Math.min(GRADIENT.length, full.length - revealed)).join('');
28
- frames.push(style(full.slice(0, revealed), edge));
29
- }
30
- frames.push(style(full, ''));
31
- return frames;
32
- }
33
- function animates() {
34
- return isInteractive && env.TERM !== 'dumb' && !env.CI;
35
- }
36
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
37
- /** Print the intro, animated when the terminal can show it and static when it cannot. */
38
- export async function playIntro({ json = false } = {}) {
39
- if (json)
40
- return;
41
- const frames = introFrames();
42
- const finished = frames.at(-1) ?? '';
43
- if (!animates()) {
44
- stdout.write(`\n${finished}\n\n`);
45
- return;
46
- }
47
- stdout.write(`\n${HIDE_CURSOR}`);
48
- const cancelExitHook = onExit(() => {
49
- stdout.write(SHOW_CURSOR);
50
- });
51
- try {
52
- for (const frame of frames) {
53
- stdout.write(`\r${CLEAR_LINE}${frame}`);
54
- await sleep(FRAME_MS);
55
- }
56
- await sleep(HOLD_MS);
57
- stdout.write('\n\n');
58
- }
59
- finally {
60
- stdout.write(SHOW_CURSOR);
61
- cancelExitHook();
62
- }
63
- }
package/skills/init.md DELETED
@@ -1,199 +0,0 @@
1
- # init
2
-
3
- Use `init` to scaffold a Blueprint around an existing Sanity project. The command is
4
- experimental, hidden from root help, and available from the `blueprinit` npm dist-tag.
5
-
6
- Use it for demos and evaluation, not as a stable project initializer. Its flags, generated files,
7
- and resource model may change.
8
-
9
- ## Know what it does
10
-
11
- `init` uses an existing Sanity organization. It uses a project that exists, or creates one with
12
- `--create-project`. It writes local Blueprint files, then creates an empty organization-scoped
13
- Stack. The generated Blueprint attaches the project and declares resources for a later deployment.
14
-
15
- The dataset works one of two ways:
16
-
17
- - **Existing project:** the user picks one of its datasets, and the Blueprint attaches it.
18
- - **New project:** a new project has no datasets, so the Blueprint creates one. Its name comes from
19
- `--dataset` and defaults to `production`. It does not exist until `blueprints deploy` runs.
20
-
21
- It does not create an organization. It does not deploy the attached resources, CORS origin, Studio,
22
- or Function. Those changes happen only if `blueprints deploy` is run later.
23
-
24
- ## Inspect the target directory
25
-
26
- Pass the directory as an argument, or run the command from the directory that should contain the
27
- Blueprint. Without an argument an interactive run asks where the files go; an unattended run keeps
28
- the current directory.
29
-
30
- Before running:
31
-
32
- 1. Check the working tree and existing files. Do not discard unrelated user changes.
33
- 2. Look for a root `sanity.blueprint.*` file. If one exists, use the existing Blueprint instead of
34
- initializing again.
35
- 3. Look for a root `sanity.config.*` file. Its presence selects the Studio adoption flow.
36
- 4. Confirm the user asked to initialize this directory. The command writes files and creates a
37
- remote Stack.
38
- 5. Confirm the user is signed in. If needed, have them run:
39
-
40
- ```sh
41
- npx sanity login
42
- ```
43
-
44
- The account must have at least one organization. A run against an existing project also needs one
45
- project in the selected organization and one dataset in that project. A run with `--create-project`
46
- needs neither.
47
-
48
- Interactive runs show the Blueprint resources, hosted Studio URL, local changes, and remote actions
49
- before the confirmation prompt. Everything the Blueprint declares stays pending until a later
50
- deployment.
51
-
52
- A run that creates a project makes two remote changes rather than one: the project, then the Stack.
53
- Both happen after the confirmation, so a cancelled run creates nothing. The project is created
54
- before any file is written, because the generated Studio config needs its ID.
55
-
56
- ## Choose the flow
57
-
58
- - **Existing Blueprint with a configured Stack:** Exit without changes and use
59
- `blueprints config --edit` or `blueprints doctor`.
60
- - **Existing Blueprint without a configured Stack:** Finish setup with `blueprints init`; it can
61
- connect the existing manifest without rewriting it.
62
- - **Root Studio config:** Adopt the Studio in place. Read literal `projectId` and `dataset` strings
63
- from `sanity.config.*`, falling back to `sanity.cli.*`. Do not move the Studio.
64
- - **Fresh workspace:** Create a Blueprint, an npm-workspace Studio, and a starter `set-slug`
65
- document function.
66
-
67
- Do not rewrite a Studio config merely to make adoption work unless the user asks. Dynamic
68
- `projectId` or `dataset` values built from environment variables or imports cannot be detected.
69
-
70
- For an existing Blueprint, inspect it with:
71
-
72
- ```sh
73
- npx @sanity/runtime-cli@blueprinit blueprints doctor
74
- ```
75
-
76
- ## Run unattended
77
-
78
- When the organization and project IDs are known, run this from the target directory:
79
-
80
- ```sh
81
- npx @sanity/runtime-cli@blueprinit init --yes \
82
- --organization-id <organization-id> \
83
- --project-id <project-id> \
84
- --dataset <dataset-name>
85
- ```
86
-
87
- To create the project instead, replace `--project-id` with `--create-project`:
88
-
89
- ```sh
90
- npx @sanity/runtime-cli@blueprinit init --yes \
91
- --organization-id <organization-id> \
92
- --create-project "<project display name>" \
93
- --dataset <dataset-name>
94
- ```
95
-
96
- `--create-project` and `--project-id` cannot both be passed. With `--create-project`, `--dataset`
97
- names a dataset to create and defaults to `production`.
98
-
99
- Replace all placeholders with real values. The project must belong to the organization, and the
100
- dataset must belong to the project. Do not guess any value.
101
-
102
- `--yes` skips confirmation. Without explicit values, an unattended run stops when the account has
103
- several organizations, projects, or datasets. Ask the user for the missing value or have them run
104
- the command interactively.
105
-
106
- Use `--stack-name <name>` and `--studio-name <name>` when those generated names need to be stable.
107
- The command normalizes both names to lowercase kebab case.
108
-
109
- The Stack name defaults to `<project>-<dataset>` and is also the hosted Studio slug, so it must be
110
- available across every Sanity organization. No API reports whether a slug is free, so a taken one
111
- fails during `blueprints deploy`.
112
-
113
- The Studio name is a Blueprint-local resource name and its directory under `studios/`. It defaults
114
- to `studio`. Every other generated name carries its resource type, so the defaults cannot collide.
115
- A dataset named `studio`, or `--studio-name production` against a `production` dataset, does
116
- collide; the command names the repeated value and writes nothing.
117
-
118
- When `SANITY_INTERNAL_ENV=staging`, the generated Studio URL and CORS origin use
119
- `https://<stack-name>.studio.sanity.work`. Other environments use
120
- `https://<stack-name>.sanity.studio`.
121
-
122
- For interactive organization, project, dataset, Stack-name, and confirmation prompts, have the
123
- user run:
124
-
125
- ```sh
126
- npx @sanity/runtime-cli@blueprinit init
127
- ```
128
-
129
- When the project has several datasets, the interactive command prompts for one and lists
130
- `production` first. Report the selected dataset from the command output.
131
-
132
- ## Review the result
133
-
134
- A successful fresh run creates or updates:
135
-
136
- ```text
137
- .
138
- ├── .sanity/blueprint.config.json
139
- ├── sanity.blueprint.ts
140
- ├── package.json
141
- ├── functions/
142
- │ └── set-slug/
143
- └── studios/
144
- └── studio/
145
- ```
146
-
147
- The command also creates or updates `.gitignore`. It preserves existing package metadata and adds
148
- the required npm workspaces and `@sanity/blueprints` dependency.
149
-
150
- The generated Studio matches `sanity init`: the same `sanity`, `@sanity/vision`, `react`,
151
- `react-dom`, `styled-components`, `@types/react`, and `typescript` versions, the same
152
- `tsconfig.json`, and `visionTool()` beside `structureTool()`. `sanity` and `@sanity/vision` are
153
- resolved from the registry at scaffold time, so no version is written into this repository.
154
- The Studio has no `deploy` script, because the Blueprint deploys it.
155
-
156
- An adopted Studio stays at the repository root. The command adds the Blueprint, Blueprint config,
157
- dependency, and `.gitignore` entry, but does not generate the starter Function.
158
-
159
- After the command exits:
160
-
161
- 1. Confirm the output names the expected organization, project, and dataset.
162
- 2. Confirm `.sanity/blueprint.config.json` exists. Its presence means Stack creation completed.
163
- 3. Review `sanity.blueprint.ts`, including the Studio slug and credentialed CORS origin, plus
164
- `package.json`, `.gitignore`, and the working-tree diff.
165
- 4. Report the Stack ID and every created or modified path.
166
-
167
- Do not deploy as part of initialization unless the user separately asks for deployment. To continue
168
- after reviewing the scaffold, install dependencies and preview the deployment:
169
-
170
- ```sh
171
- npm install
172
- npx @sanity/runtime-cli@blueprinit blueprints plan
173
- ```
174
-
175
- Treat dependency installation and planning as separate follow-up actions when they were not part of
176
- the user's request.
177
-
178
- ## Handle failures
179
-
180
- - **`No Sanity session found.`** Stop and ask the user to run `npx sanity login`. Authentication
181
- fails before files are written.
182
- - **Multiple organizations or projects found.** Rerun with the matching `--organization-id` and
183
- `--project-id`. Selection fails before files are written.
184
- - **No datasets found.** Ask the user to choose a project with a dataset, or to create a project
185
- with `--create-project`, which makes the Blueprint create the dataset too.
186
- - **No projects found in an organization.** In an unattended run, pass `--create-project <name>`.
187
- - **Dataset name is not valid.** The projects API allows 2 to 64 characters from `a-z`, `0-9`, `-`,
188
- and `_`, starting with a letter or a digit. Report the rule and ask for another name. Do not
189
- rewrite the name the user gave.
190
- - **Could not create project.** Nothing was written. Report the API message. A quota error needs a
191
- plan change or another organization.
192
- - **Could not read `projectId` and `dataset`.** The detected Studio config does not contain both
193
- values as string literals. Pass explicit `--project-id` and `--dataset` values or report the config
194
- file and let the user decide whether to change it.
195
- - **Refusing to overwrite an existing path.** Inspect the reported Studio or Function directory.
196
- Do not delete or replace it without the user's direction.
197
- - **Stack quota or duplicate-name error.** Local scaffold files may already exist while
198
- `.sanity/blueprint.config.json` does not. Resolve the Stack error, then run the recovery command
199
- printed by `init`, which is `blueprints init`. It connects the existing manifest without rewriting it.