@sanity/runtime-cli 17.2.0 → 17.3.0-blueprinit.gb1c5e1e

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 (55) hide show
  1. package/README.md +36 -27
  2. package/SKILL.md +21 -0
  3. package/dist/actions/blueprinit/manifest.d.ts +13 -0
  4. package/dist/actions/blueprinit/manifest.js +57 -0
  5. package/dist/actions/blueprinit/studio.d.ts +6 -0
  6. package/dist/actions/blueprinit/studio.js +43 -0
  7. package/dist/actions/blueprinit/templates.d.ts +34 -0
  8. package/dist/actions/blueprinit/templates.js +83 -0
  9. package/dist/actions/blueprinit/workspace.d.ts +15 -0
  10. package/dist/actions/blueprinit/workspace.js +142 -0
  11. package/dist/actions/blueprints/assets.js +15 -24
  12. package/dist/actions/blueprints/pre-deploy.d.ts +29 -0
  13. package/dist/actions/blueprints/pre-deploy.js +164 -0
  14. package/dist/actions/sanity/datasets.d.ts +15 -0
  15. package/dist/actions/sanity/datasets.js +26 -0
  16. package/dist/commands/blueprinit.d.ts +14 -0
  17. package/dist/commands/blueprinit.js +46 -0
  18. package/dist/commands/functions/env/add.d.ts +3 -0
  19. package/dist/commands/functions/env/add.js +6 -2
  20. package/dist/commands/functions/env/list.d.ts +3 -0
  21. package/dist/commands/functions/env/list.js +9 -3
  22. package/dist/commands/functions/env/remove.d.ts +3 -0
  23. package/dist/commands/functions/env/remove.js +9 -3
  24. package/dist/constants.d.ts +1 -0
  25. package/dist/constants.js +1 -0
  26. package/dist/cores/blueprinit/adopt.d.ts +6 -0
  27. package/dist/cores/blueprinit/adopt.js +90 -0
  28. package/dist/cores/blueprinit/finish.d.ts +14 -0
  29. package/dist/cores/blueprinit/finish.js +36 -0
  30. package/dist/cores/blueprinit/fresh.d.ts +3 -0
  31. package/dist/cores/blueprinit/fresh.js +88 -0
  32. package/dist/cores/blueprinit/prompts.d.ts +4 -0
  33. package/dist/cores/blueprinit/prompts.js +14 -0
  34. package/dist/cores/blueprinit/selection.d.ts +15 -0
  35. package/dist/cores/blueprinit/selection.js +63 -0
  36. package/dist/cores/blueprinit/types.d.ts +13 -0
  37. package/dist/cores/blueprinit/types.js +1 -0
  38. package/dist/cores/blueprinit.d.ts +7 -0
  39. package/dist/cores/blueprinit.js +39 -0
  40. package/dist/cores/blueprints/deploy.js +16 -64
  41. package/dist/server/static/vendor/vendor.bundle.js +10 -14
  42. package/dist/utils/functions/child-process-wrapper.js +13 -171
  43. package/dist/utils/functions/child-runtime/console.d.ts +1 -0
  44. package/dist/utils/functions/child-runtime/console.js +50 -0
  45. package/dist/utils/functions/child-runtime/invoke.d.ts +4 -0
  46. package/dist/utils/functions/child-runtime/invoke.js +47 -0
  47. package/dist/utils/functions/child-runtime/resources.d.ts +4 -0
  48. package/dist/utils/functions/child-runtime/resources.js +59 -0
  49. package/dist/utils/studios/prepare-asset.d.ts +6 -0
  50. package/dist/utils/studios/prepare-asset.js +139 -0
  51. package/dist/utils/types.d.ts +4 -7
  52. package/dist/utils/types.js +2 -3
  53. package/oclif.manifest.json +113 -7
  54. package/package.json +26 -16
  55. package/skills/blueprinit.md +137 -0
@@ -1,48 +1,52 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
3
4
  import AdmZip from 'adm-zip';
4
5
  import { blueprintsUrl } from '../../config.js';
5
6
  import { CONVERT_BYTES_TO_MB, MAX_ASSET_SIZE } from '../../constants.js';
6
7
  import { prepareAsset as prepareFunctionAsset } from '../../utils/functions/prepare-asset.js';
7
8
  import getHeaders from '../../utils/get-headers.js';
9
+ import { prepareStudioAsset } from '../../utils/studios/prepare-asset.js';
8
10
  import { createTracedFetch } from '../../utils/traced-fetch.js';
9
11
  import { isLocalFunctionResource, isStudioResource } from '../../utils/types.js';
10
12
  const assetsUrl = blueprintsUrl('assets');
11
13
  export const assetCheckUrl = `${assetsUrl}/check`;
12
14
  export const assetStashUrl = `${assetsUrl}/stash`;
13
15
  export async function stashAsset({ resource, auth, blueprintFileDir, logger, installer, }) {
16
+ let cleanup = async () => { };
14
17
  try {
15
18
  let b64;
16
19
  let hash;
17
20
  let extension;
18
- let cleanup;
19
21
  if (isLocalFunctionResource(resource)) {
20
22
  const prepResult = await prepareFunctionAsset({ resource, blueprintFileDir, installer });
21
23
  if (prepResult.success === false) {
24
+ await prepResult.cleanup();
22
25
  return {
23
26
  success: false,
24
27
  error: prepResult.error,
25
28
  };
26
29
  }
30
+ cleanup = prepResult.cleanup;
27
31
  const zipResult = await pathToB64ZipHash(prepResult.outputPath);
28
32
  b64 = zipResult.b64;
29
33
  hash = zipResult.hash;
30
34
  extension = 'zip';
31
- cleanup = prepResult.cleanup;
32
35
  }
33
36
  else if (isStudioResource(resource)) {
34
- const prepResult = await prepareStudioAsset({ resource });
37
+ const prepResult = await prepareStudioAsset({ resource, blueprintFileDir });
35
38
  if (prepResult.success === false) {
39
+ await prepResult.cleanup();
36
40
  return {
37
41
  success: false,
38
42
  error: prepResult.error,
39
43
  };
40
44
  }
41
- const buffer = await fs.promises.readFile(prepResult.outputPath);
45
+ cleanup = prepResult.cleanup;
46
+ const buffer = await readFile(prepResult.outputPath);
42
47
  b64 = buffer.toString('base64');
43
48
  hash = hashBuffer(buffer);
44
49
  extension = 'tar.gz';
45
- cleanup = prepResult.cleanup;
46
50
  }
47
51
  else {
48
52
  return {
@@ -61,28 +65,15 @@ export async function stashAsset({ resource, auth, blueprintFileDir, logger, ins
61
65
  });
62
66
  }
63
67
  catch (err) {
68
+ try {
69
+ await cleanup();
70
+ }
71
+ catch {
72
+ // ignore clean up errors
73
+ }
64
74
  return { success: false, error: err instanceof Error ? err.message : `${err}` };
65
75
  }
66
76
  }
67
- async function prepareStudioAsset({ resource, }) {
68
- if (!fs.existsSync(resource.src)) {
69
- return {
70
- success: false,
71
- async cleanup() {
72
- // no op
73
- },
74
- error: `Property 'src' on resource with type '${resource.type}' must be a directory`,
75
- };
76
- }
77
- return {
78
- success: true,
79
- async cleanup() {
80
- // no op
81
- },
82
- // studio must be pre built (e.g. with `npm run build`) and tarred/gzipped
83
- outputPath: resource.src,
84
- };
85
- }
86
77
  async function stashSingleAsset({ b64, hash, cleanup, resource, auth, logger, extension = 'zip', }) {
87
78
  const fetchFn = createTracedFetch(logger);
88
79
  try {
@@ -0,0 +1,29 @@
1
+ import type { BlueprintResource } from '@sanity/blueprints';
2
+ import type { Logger } from '../../utils/logger.js';
3
+ import { type AuthParams, type InstallerType, type ScopeType } from '../../utils/types.js';
4
+ type PreDeployResult = {
5
+ success: false;
6
+ error: string;
7
+ code?: string;
8
+ exitCode?: number;
9
+ suggestions?: string[];
10
+ } | {
11
+ success: true;
12
+ };
13
+ type PreDeployResourceOptions = {
14
+ auth: AuthParams;
15
+ blueprintFileDir: string;
16
+ installer: InstallerType;
17
+ log: Logger;
18
+ };
19
+ type PreDeployOptions = {
20
+ scopeType: ScopeType;
21
+ scopeId: string;
22
+ resources: BlueprintResource[];
23
+ stackId: string;
24
+ stackName: string;
25
+ bin: string;
26
+ verbose: boolean | undefined;
27
+ } & PreDeployResourceOptions;
28
+ export declare function preDeploy(options: PreDeployOptions): Promise<PreDeployResult>;
29
+ export {};
@@ -0,0 +1,164 @@
1
+ import { env } from 'node:process';
2
+ import { resolveDeployedResources } from '../../utils/blueprints/resolve-deployed-resources.js';
3
+ import { niceId } from '../../utils/display/presenters.js';
4
+ import { CODE_OPERATION_UNCONFIRMED, EXIT_OPERATION_UNCONFIRMED } from '../../utils/exit-codes.js';
5
+ import { styleText } from '../../utils/style-text.js';
6
+ import { isAssetResource, } from '../../utils/types.js';
7
+ import { stashAsset } from './assets.js';
8
+ import { updateStack } from './stacks.js';
9
+ import { waitForOperation } from './wait-for-operation.js';
10
+ const DEFAULT_ASSET_TIMEOUT = 180;
11
+ const assetTimeoutS = Number(process.env.SANITY_ASSET_TIMEOUT) || DEFAULT_ASSET_TIMEOUT;
12
+ const assetTimeoutMs = assetTimeoutS * 1000;
13
+ const warnTimeoutMs = assetTimeoutMs / 3;
14
+ export async function preDeploy(options) {
15
+ const { resources, stackId, auth, blueprintFileDir, installer, log } = options;
16
+ const hasAssetResources = resources.some(isAssetResource);
17
+ if (hasAssetResources) {
18
+ let assetResources;
19
+ if (env.SANITY_BLUEPRINTS_PRE_DEPLOY_ENABLED === 'true') {
20
+ const preDeployStackResult = await preDeployStack(options);
21
+ if (!preDeployStackResult.success) {
22
+ return preDeployStackResult;
23
+ }
24
+ /** Look up resources from the back-end and attach the externalId if possible */
25
+ const resolvedResources = await resolveDeployedResources({
26
+ resources,
27
+ stackId,
28
+ auth,
29
+ logger: log,
30
+ });
31
+ assetResources = resolvedResources.filter(isAssetResource);
32
+ }
33
+ else {
34
+ assetResources = resources.filter(isAssetResource);
35
+ }
36
+ log('Processing assets...');
37
+ for (const resource of assetResources) {
38
+ const preDeployResult = await preDeployResource(resource, {
39
+ auth,
40
+ blueprintFileDir,
41
+ installer,
42
+ log,
43
+ });
44
+ if (!preDeployResult.success)
45
+ return preDeployResult;
46
+ const idx = resources.findIndex((r) => r.name === resource.name);
47
+ if (idx === -1)
48
+ throw new Error('Unable to update resource asset');
49
+ resources[idx] = preDeployResult.resource;
50
+ }
51
+ }
52
+ return { success: true };
53
+ }
54
+ async function preDeployStack(options) {
55
+ const { scopeType, scopeId, stackId, stackName, resources, auth, log, bin, verbose } = options;
56
+ const spinner = log.ora('Starting pre-deploy...').start();
57
+ const stackMutation = {
58
+ scopeType,
59
+ scopeId,
60
+ name: stackName,
61
+ preDeploy: true,
62
+ document: { resources },
63
+ };
64
+ const { ok: deployOk, stack, error: deployError, } = await updateStack({ stackId, stackMutation, auth, logger: log });
65
+ if (!deployOk) {
66
+ spinner.fail(`${styleText('red', 'Failed')} to complete Stack pre-deployment`);
67
+ return {
68
+ success: false,
69
+ error: deployError || 'Failed to complete Stack pre-deployment',
70
+ suggestions: [
71
+ `Run \`npx ${bin} blueprints plan\` to preview changes before deploying.`,
72
+ `Run \`npx ${bin} blueprints doctor\` to check your configuration.`,
73
+ ],
74
+ };
75
+ }
76
+ spinner.stop().clear();
77
+ log(styleText('dim', 'Stack deployment progress:'));
78
+ log('');
79
+ const outcome = await waitForOperation({
80
+ stackId,
81
+ operationId: stack.operationId,
82
+ auth,
83
+ log,
84
+ bin,
85
+ verbose,
86
+ progressNoun: 'pre-deployment',
87
+ });
88
+ if (outcome.type === 'completed') {
89
+ log(styleText(['bold', 'green'], 'Stack pre-deployment completed!'));
90
+ }
91
+ else if (outcome.type === 'unconfirmed') {
92
+ return {
93
+ success: false,
94
+ error: `Stack pre-deployment was accepted but completion could not be confirmed${outcome.error ? ` (${outcome.error})` : ''}.`,
95
+ code: CODE_OPERATION_UNCONFIRMED,
96
+ exitCode: EXIT_OPERATION_UNCONFIRMED,
97
+ suggestions: [
98
+ 'The pre-deployment may or may not have finished on Sanity servers.',
99
+ `Run \`npx ${bin} blueprints info\` to check status.`,
100
+ `Run \`npx ${bin} blueprints logs --watch\` to keep streaming logs.`,
101
+ ],
102
+ };
103
+ }
104
+ else {
105
+ return {
106
+ success: false,
107
+ error: 'Stack pre-deployment failed',
108
+ suggestions: [
109
+ ...outcome.logHints,
110
+ 'Review the pre-deployment output above for error details.',
111
+ `Run \`npx ${bin} blueprints logs --verbose\` for more context.`,
112
+ `Run \`npx ${bin} blueprints plan\` to identify issues with your Blueprint.`,
113
+ ],
114
+ };
115
+ }
116
+ return { success: true };
117
+ }
118
+ async function preDeployResource(resource, options) {
119
+ const { auth, blueprintFileDir, installer, log } = options;
120
+ const fnSpinner = log.ora({ text: `Processing ${resource.name}...`, prefixText: ' ' }).start();
121
+ const warnTimer = setTimeout(() => {
122
+ fnSpinner.text = `Still processing ${resource.name}, this can take a moment...`;
123
+ }, warnTimeoutMs);
124
+ let assetTimeoutTimer;
125
+ let result;
126
+ try {
127
+ result = await Promise.race([
128
+ stashAsset({ resource, blueprintFileDir, auth, logger: log, installer }),
129
+ new Promise((_, reject) => {
130
+ assetTimeoutTimer = setTimeout(() => {
131
+ reject(new Error(`Processing ${resource.name} timed out after ${assetTimeoutS}s`));
132
+ }, assetTimeoutMs);
133
+ }),
134
+ ]);
135
+ }
136
+ catch (err) {
137
+ const msg = err instanceof Error ? err.message : String(err);
138
+ fnSpinner.fail(msg);
139
+ return { success: false, error: msg };
140
+ }
141
+ finally {
142
+ clearTimeout(warnTimer);
143
+ clearTimeout(assetTimeoutTimer);
144
+ }
145
+ if (result.success && result.assetId) {
146
+ const updatedResource = { ...resource, src: result.assetId };
147
+ fnSpinner.succeed(`${updatedResource.name} ${niceId(result.assetId)}`);
148
+ log(` Source: ${updatedResource.src}`);
149
+ if (result.hash) {
150
+ if (result.hash.length > 24) {
151
+ log.verbose(` Hash: ${result.hash.slice(0, 8)}...${result.hash.slice(-12)}`);
152
+ }
153
+ else {
154
+ log.verbose(` Hash: ${result.hash}`);
155
+ }
156
+ }
157
+ if (result.exists)
158
+ log.verbose(' Asset unchanged');
159
+ return { success: true, resource: updatedResource };
160
+ }
161
+ const errorMsg = `Failed uploading ${resource.name} asset, deploy has stopped`;
162
+ fnSpinner.fail(errorMsg);
163
+ return { success: false, error: result.error || 'Failed to process asset' };
164
+ }
@@ -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
+ }
@@ -0,0 +1,14 @@
1
+ import { RuntimeCommand } from '../baseCommands.js';
2
+ export default class BlueprinitCommand extends RuntimeCommand<typeof BlueprinitCommand> {
3
+ static hidden: boolean;
4
+ static summary: string;
5
+ static description: string;
6
+ static examples: string[];
7
+ static flags: {
8
+ llms: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ 'organization-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<Record<string, unknown> | undefined>;
14
+ }
@@ -0,0 +1,46 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { llmsFlagConfig } from '@sanity-labs/oclif-plugin-skills-flag';
3
+ import { RuntimeCommand } from '../baseCommands.js';
4
+ import { blueprinitCore } from '../cores/blueprinit.js';
5
+ import { Logger } from '../utils/logger.js';
6
+ export default class BlueprinitCommand extends RuntimeCommand {
7
+ static hidden = true;
8
+ static summary = 'Experimental blueprints-first project init';
9
+ static description = `Scaffolds a Blueprint, Studio, and starter function around an existing Sanity project.
10
+
11
+ This hidden command is an experiment; its interface and generated files are not stable.`;
12
+ static examples = [
13
+ '<%= config.bin %> <%= command.id %>',
14
+ '<%= config.bin %> <%= command.id %> --yes',
15
+ ];
16
+ static flags = {
17
+ llms: Flags.boolean(llmsFlagConfig),
18
+ yes: Flags.boolean({
19
+ char: 'y',
20
+ description: 'Skip confirmation prompts',
21
+ default: false,
22
+ }),
23
+ 'organization-id': Flags.string({
24
+ description: 'Organization to use when the account has several',
25
+ aliases: ['organization', 'organizationId', 'org'],
26
+ }),
27
+ 'project-id': Flags.string({
28
+ description: 'Project to use when the organization has several',
29
+ aliases: ['project', 'projectId'],
30
+ }),
31
+ };
32
+ async run() {
33
+ const result = await blueprinitCore({
34
+ bin: this.config.bin,
35
+ log: Logger(this.log.bind(this), this.flags),
36
+ flags: {
37
+ yes: this.flags.yes,
38
+ organizationId: this.flags['organization-id'],
39
+ projectId: this.flags['project-id'],
40
+ },
41
+ });
42
+ if (!result.success)
43
+ this.coreError(result);
44
+ return result.json;
45
+ }
46
+ }
@@ -8,6 +8,9 @@ export default class EnvAddCommand extends ResolvedCommand<typeof EnvAddCommand>
8
8
  key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
9
9
  value: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
10
10
  };
11
+ static flags: {
12
+ stack: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
11
14
  static examples: string[];
12
15
  run(): Promise<Record<string, unknown> | undefined>;
13
16
  }
@@ -1,5 +1,5 @@
1
- import { Args } from '@oclif/core';
2
- import { ResolvedCommand } from '../../../baseCommands.js';
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { ResolvedCommand, stackFlagConfig } from '../../../baseCommands.js';
3
3
  import { functionEnvAddCore } from '../../../cores/functions/env/add.js';
4
4
  import { Logger } from '../../../utils/logger.js';
5
5
  export default class EnvAddCommand extends ResolvedCommand {
@@ -13,8 +13,12 @@ Environment variables are useful for API keys, configuration values, and other s
13
13
  key: Args.string({ description: 'The name of the environment variable', required: true }),
14
14
  value: Args.string({ description: 'The value of the environment variable', required: true }),
15
15
  };
16
+ static flags = {
17
+ stack: Flags.string({ ...stackFlagConfig }),
18
+ };
16
19
  static examples = [
17
20
  '<%= config.bin %> <%= command.id %> MyFunction API_URL https://api.example.com/',
21
+ '<%= config.bin %> <%= command.id %> --stack <name-or-id> MyFunction API_URL https://api.example.com/',
18
22
  ];
19
23
  async run() {
20
24
  const result = await functionEnvAddCore({
@@ -6,6 +6,9 @@ export default class EnvListCommand extends ResolvedCommand<typeof EnvListComman
6
6
  static args: {
7
7
  name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
8
  };
9
+ static flags: {
10
+ stack: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
9
12
  static examples: string[];
10
13
  run(): Promise<Record<string, unknown> | undefined>;
11
14
  }
@@ -1,5 +1,5 @@
1
- import { Args } from '@oclif/core';
2
- import { ResolvedCommand } from '../../../baseCommands.js';
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { ResolvedCommand, stackFlagConfig } from '../../../baseCommands.js';
3
3
  import { functionEnvListCore } from '../../../cores/functions/env/list.js';
4
4
  import { Logger } from '../../../utils/logger.js';
5
5
  export default class EnvListCommand extends ResolvedCommand {
@@ -11,7 +11,13 @@ Use 'functions env add' to set variables or 'functions env remove' to delete the
11
11
  static args = {
12
12
  name: Args.string({ description: 'The name of the Sanity Function', required: true }),
13
13
  };
14
- static examples = ['<%= config.bin %> <%= command.id %> MyFunction'];
14
+ static flags = {
15
+ stack: Flags.string({ ...stackFlagConfig }),
16
+ };
17
+ static examples = [
18
+ '<%= config.bin %> <%= command.id %> MyFunction',
19
+ '<%= config.bin %> <%= command.id %> --stack <name-or-id> MyFunction',
20
+ ];
15
21
  async run() {
16
22
  const result = await functionEnvListCore({
17
23
  bin: this.config.bin,
@@ -7,6 +7,9 @@ export default class EnvRemoveCommand extends ResolvedCommand<typeof EnvRemoveCo
7
7
  name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
8
  key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
9
9
  };
10
+ static flags: {
11
+ stack: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
10
13
  static examples: string[];
11
14
  run(): Promise<Record<string, unknown> | undefined>;
12
15
  }
@@ -1,5 +1,5 @@
1
- import { Args } from '@oclif/core';
2
- import { ResolvedCommand } from '../../../baseCommands.js';
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { ResolvedCommand, stackFlagConfig } from '../../../baseCommands.js';
3
3
  import { functionEnvRemoveCore } from '../../../cores/functions/env/remove.js';
4
4
  import { Logger } from '../../../utils/logger.js';
5
5
  export default class EnvRemoveCommand extends ResolvedCommand {
@@ -12,7 +12,13 @@ Use 'functions env list' to see current variables before removing.`;
12
12
  name: Args.string({ description: 'The name of the Sanity Function', required: true }),
13
13
  key: Args.string({ description: 'The name of the environment variable', required: true }),
14
14
  };
15
- static examples = ['<%= config.bin %> <%= command.id %> MyFunction API_URL'];
15
+ static flags = {
16
+ stack: Flags.string({ ...stackFlagConfig }),
17
+ };
18
+ static examples = [
19
+ '<%= config.bin %> <%= command.id %> MyFunction API_URL',
20
+ '<%= config.bin %> <%= command.id %> --stack <name-or-id> MyFunction API_URL',
21
+ ];
16
22
  async run() {
17
23
  const result = await functionEnvRemoveCore({
18
24
  bin: this.config.bin,
@@ -9,6 +9,7 @@ export declare const SANITY_PROJECT = "sanity.project";
9
9
  export declare const SANITY_PROJECT_CORS = "sanity.project.cors";
10
10
  export declare const SANITY_PROJECT_DATASET = "sanity.project.dataset";
11
11
  export declare const SANITY_PROJECT_WEBHOOK = "sanity.project.webhook";
12
+ export declare const SANITY_STUDIO = "sanity.studio";
12
13
  export declare const EVENT_DOCUMENT_PUBLISH = "document-publish";
13
14
  export declare const EVENT_DOCUMENT_CREATE = "document-create";
14
15
  export declare const EVENT_DOCUMENT_UPDATE = "document-update";
package/dist/constants.js CHANGED
@@ -9,6 +9,7 @@ export const SANITY_PROJECT = 'sanity.project';
9
9
  export const SANITY_PROJECT_CORS = 'sanity.project.cors';
10
10
  export const SANITY_PROJECT_DATASET = 'sanity.project.dataset';
11
11
  export const SANITY_PROJECT_WEBHOOK = 'sanity.project.webhook';
12
+ export const SANITY_STUDIO = 'sanity.studio';
12
13
  export const EVENT_DOCUMENT_PUBLISH = 'document-publish';
13
14
  export const EVENT_DOCUMENT_CREATE = 'document-create';
14
15
  export const EVENT_DOCUMENT_UPDATE = 'document-update';
@@ -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,90 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { slugify } from '../../actions/blueprinit/manifest.js';
4
+ import { adoptWorkspace } from '../../actions/blueprinit/workspace.js';
5
+ import { listDatasets } from '../../actions/sanity/datasets.js';
6
+ import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
7
+ import { check } from '../../utils/display/presenters.js';
8
+ import { getLatestNpmVersion } from '../../utils/external/npmjs.js';
9
+ import { isInteractive } from '../../utils/interactive.js';
10
+ import { createStackAndFinish } from './finish.js';
11
+ import { confirmUnlessSkipped, promptForStudioName } from './prompts.js';
12
+ import { findProject } from './selection.js';
13
+ export async function adoptFlow(options) {
14
+ const { bin, dir, flags, log, studio, token } = options;
15
+ if (!studio.projectId || !studio.dataset) {
16
+ return {
17
+ success: false,
18
+ error: `Found ${studio.configFile}, but could not read a projectId and dataset from it.`,
19
+ suggestions: ['Use literal projectId and dataset values in the Studio config.'],
20
+ };
21
+ }
22
+ if (flags.projectId && flags.projectId !== studio.projectId) {
23
+ return { success: false, error: `The Studio uses project "${studio.projectId}".` };
24
+ }
25
+ log(check(`Studio detected (${studio.configFile})`));
26
+ const grouped = await groupProjectsByOrganization({ token, logger: log });
27
+ if (!grouped.ok) {
28
+ return { success: false, error: `Could not list organizations: ${grouped.error}` };
29
+ }
30
+ const match = findProject(grouped.organizations, studio.projectId);
31
+ if (!match) {
32
+ return {
33
+ success: false,
34
+ error: `Project "${studio.projectId}" from ${studio.configFile} was not found in your account.`,
35
+ };
36
+ }
37
+ if (flags.organizationId && flags.organizationId !== match.organization.id) {
38
+ return {
39
+ success: false,
40
+ error: `Project "${studio.projectId}" belongs to organization "${match.organization.id}".`,
41
+ };
42
+ }
43
+ const datasets = await listDatasets({ token, projectId: match.project.id, logger: log });
44
+ if (!datasets.ok) {
45
+ return { success: false, error: `Could not list datasets: ${datasets.error}` };
46
+ }
47
+ if (!datasets.datasets.some(({ name }) => name === studio.dataset)) {
48
+ return {
49
+ success: false,
50
+ error: `Dataset "${studio.dataset}" was not found in project ${match.project.displayName}.`,
51
+ };
52
+ }
53
+ log(check(`Matches project ${match.project.displayName} (${match.project.id}), dataset ${studio.dataset}`));
54
+ log('');
55
+ const confirmed = await confirmUnlessSkipped(flags, isInteractive, 'Adopt this Studio into a Blueprint?');
56
+ if (!confirmed)
57
+ return { success: false, error: 'Initialization cancelled.' };
58
+ const studioName = await promptForStudioName(flags, isInteractive);
59
+ const stackName = slugify(match.project.displayName) || match.project.id;
60
+ const marker = (file) => (existsSync(join(dir, file)) ? '~' : '+');
61
+ log('Changes:');
62
+ log(" + sanity.blueprint.ts attach project + dataset; adopt Studio at '.'");
63
+ log(` + .sanity/blueprint.config.json org scope, stack ${stackName}`);
64
+ log(` ${marker('.gitignore')} .gitignore`);
65
+ log(` ${marker('package.json')} package.json`);
66
+ log('');
67
+ const adoption = adoptWorkspace({
68
+ dir,
69
+ projectId: match.project.id,
70
+ displayName: match.project.displayName,
71
+ datasetName: studio.dataset,
72
+ stackName,
73
+ studioName,
74
+ blueprintsVersion: await getLatestNpmVersion('@sanity/blueprints', log),
75
+ });
76
+ return await createStackAndFinish({
77
+ ...options,
78
+ organizationId: match.organization.id,
79
+ projectId: match.project.id,
80
+ datasetName: studio.dataset,
81
+ stackName,
82
+ blueprintFilePath: adoption.blueprintFilePath,
83
+ files: adoption.files,
84
+ nextSteps: [
85
+ ` npx ${bin} blueprints plan`,
86
+ ` npx ${bin} blueprints deploy`,
87
+ ` npx ${bin} blueprints info`,
88
+ ],
89
+ });
90
+ }
@@ -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,36 @@
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
+ const stack = await createEmptyStack({
6
+ token: options.token,
7
+ scopeType: 'organization',
8
+ scopeId: options.organizationId,
9
+ name: options.stackName,
10
+ logger: options.log,
11
+ });
12
+ writeConfigFile(options.blueprintFilePath, {
13
+ organizationId: options.organizationId,
14
+ stackId: stack.id,
15
+ });
16
+ options.log(check(`Stack ${stack.name} created (org scope)`));
17
+ options.log('');
18
+ options.log('Next steps:');
19
+ for (const step of options.nextSteps)
20
+ options.log(step);
21
+ return {
22
+ success: true,
23
+ json: {
24
+ organizationId: options.organizationId,
25
+ projectId: options.projectId,
26
+ dataset: options.datasetName,
27
+ stackId: stack.id,
28
+ stackName: stack.name,
29
+ files: [...options.files, '.sanity/blueprint.config.json'],
30
+ },
31
+ };
32
+ }
33
+ export function alignSteps(steps) {
34
+ const width = Math.max(...steps.map(([command]) => command.length));
35
+ return steps.map(([command, description]) => description ? ` ${command.padEnd(width + 2)}${description}` : ` ${command}`);
36
+ }