@sanity/runtime-cli 17.3.2 → 17.4.0-blueprinit.g596ac51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +50 -47
  2. package/SKILL.md +59 -0
  3. package/dist/actions/blueprinit/manifest.d.ts +15 -0
  4. package/dist/actions/blueprinit/manifest.js +82 -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 +16 -0
  10. package/dist/actions/blueprinit/workspace.js +143 -0
  11. package/dist/actions/blueprints/pre-deploy.d.ts +15 -6
  12. package/dist/actions/blueprints/pre-deploy.js +29 -70
  13. package/dist/actions/blueprints/process-assets.d.ts +29 -0
  14. package/dist/actions/blueprints/process-assets.js +80 -0
  15. package/dist/actions/functions/list.d.ts +34 -0
  16. package/dist/actions/functions/list.js +33 -0
  17. package/dist/actions/sanity/datasets.d.ts +15 -0
  18. package/dist/actions/sanity/datasets.js +26 -0
  19. package/dist/baseCommands.d.ts +2 -0
  20. package/dist/baseCommands.js +3 -0
  21. package/dist/commands/blueprinit.d.ts +17 -0
  22. package/dist/commands/blueprinit.js +59 -0
  23. package/dist/commands/blueprints/deploy.d.ts +2 -0
  24. package/dist/commands/blueprints/deploy.js +22 -1
  25. package/dist/commands/blueprints/destroy.js +1 -1
  26. package/dist/commands/blueprints/info.d.ts +1 -0
  27. package/dist/commands/blueprints/info.js +5 -0
  28. package/dist/commands/blueprints/init.js +1 -2
  29. package/dist/commands/blueprints/logs.js +1 -0
  30. package/dist/commands/functions/add.js +3 -3
  31. package/dist/commands/functions/dev.js +2 -2
  32. package/dist/commands/functions/list.d.ts +14 -0
  33. package/dist/commands/functions/list.js +38 -0
  34. package/dist/commands/functions/logs.d.ts +1 -1
  35. package/dist/commands/functions/logs.js +3 -0
  36. package/dist/cores/blueprinit/adopt.d.ts +6 -0
  37. package/dist/cores/blueprinit/adopt.js +119 -0
  38. package/dist/cores/blueprinit/finish.d.ts +14 -0
  39. package/dist/cores/blueprinit/finish.js +64 -0
  40. package/dist/cores/blueprinit/fresh.d.ts +3 -0
  41. package/dist/cores/blueprinit/fresh.js +120 -0
  42. package/dist/cores/blueprinit/preview.d.ts +15 -0
  43. package/dist/cores/blueprinit/preview.js +14 -0
  44. package/dist/cores/blueprinit/prompts.d.ts +5 -0
  45. package/dist/cores/blueprinit/prompts.js +22 -0
  46. package/dist/cores/blueprinit/selection.d.ts +15 -0
  47. package/dist/cores/blueprinit/selection.js +90 -0
  48. package/dist/cores/blueprinit/types.d.ts +16 -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 +48 -0
  52. package/dist/cores/blueprints/deploy.js +69 -19
  53. package/dist/cores/functions/add.js +1 -1
  54. package/dist/cores/functions/list.d.ts +10 -0
  55. package/dist/cores/functions/list.js +34 -0
  56. package/dist/utils/display/blueprints-formatting.js +3 -3
  57. package/dist/utils/display/functions-formatting.d.ts +7 -0
  58. package/dist/utils/display/functions-formatting.js +95 -0
  59. package/dist/utils/style-text.js +1 -0
  60. package/oclif.manifest.json +271 -15
  61. package/package.json +31 -17
  62. package/skills/_functions-build.md +23 -0
  63. package/skills/_functions-list.md +28 -0
  64. package/skills/blueprinit.md +155 -0
  65. package/skills/blueprints-add.md +8 -0
  66. package/skills/blueprints-config.md +33 -0
  67. package/skills/blueprints-deploy.md +36 -0
  68. package/skills/blueprints-destroy.md +30 -0
  69. package/skills/blueprints-doctor.md +31 -0
  70. package/skills/blueprints-info.md +23 -0
  71. package/skills/blueprints-init.md +48 -0
  72. package/skills/blueprints-logs.md +33 -0
  73. package/skills/blueprints-mint-deploy-token.md +25 -0
  74. package/skills/blueprints-plan.md +28 -0
  75. package/skills/blueprints-promote.md +31 -0
  76. package/skills/blueprints-stacks.md +28 -0
  77. package/skills/functions-add.md +35 -0
  78. package/skills/functions-dev.md +21 -0
  79. package/skills/functions-env-add.md +29 -0
  80. package/skills/functions-env-list.md +21 -0
  81. package/skills/functions-env-remove.md +22 -0
  82. package/skills/functions-logs.md +39 -0
  83. package/skills/functions-test.md +31 -0
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, posix } from 'node:path';
3
+ import { renderBlueprint, STARTER_FUNCTION_NAME } from './manifest.js';
4
+ import { FUNCTION_SET_SLUG, functionPackageJson, renderStudioConfig, STUDIO_POST_TYPE, STUDIO_SCHEMA_TYPES, 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 } = options;
13
+ assertTargetsAvailable(dir, [
14
+ posix.join('studios', 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: `./studios/${studioName}`,
23
+ withFunction: true,
24
+ }));
25
+ write(posix.join('studios', studioName, 'package.json'), formatJson(studioPackageJson(studioName)));
26
+ write(posix.join('studios', studioName, 'sanity.config.ts'), renderStudioConfig(options));
27
+ write(posix.join('studios', studioName, 'schemaTypes', 'index.ts'), STUDIO_SCHEMA_TYPES);
28
+ write(posix.join('studios', studioName, 'schemaTypes', 'post.ts'), STUDIO_POST_TYPE);
29
+ write(posix.join('studios', studioName, 'static', '.gitkeep'), '');
30
+ write(posix.join('functions', STARTER_FUNCTION_NAME, 'package.json'), formatJson(functionPackageJson(STARTER_FUNCTION_NAME)));
31
+ write(posix.join('functions', STARTER_FUNCTION_NAME, 'index.ts'), FUNCTION_SET_SLUG);
32
+ writePreparedRootFiles(dir, packageJson, files);
33
+ return { blueprintFilePath, files };
34
+ }
35
+ export function adoptWorkspace(options) {
36
+ const { dir } = options;
37
+ const packageJson = preparePackageJson(options, []);
38
+ const files = [];
39
+ const write = createWriter(dir, files);
40
+ const blueprintFilePath = write('sanity.blueprint.ts', renderBlueprint({ ...manifestOptions(options), studioSrc: '.', withFunction: false }));
41
+ writePreparedRootFiles(dir, packageJson, files);
42
+ return { blueprintFilePath, files };
43
+ }
44
+ function manifestOptions(options) {
45
+ return {
46
+ projectName: options.stackName,
47
+ displayName: options.displayName,
48
+ projectId: options.projectId,
49
+ datasetName: options.datasetName,
50
+ studioName: options.studioName,
51
+ studioOrigin: options.studioOrigin,
52
+ };
53
+ }
54
+ function assertTargetsAvailable(dir, paths) {
55
+ const conflicts = paths.filter((path) => existsSync(join(dir, path)));
56
+ if (conflicts.length > 0) {
57
+ throw new Error(`Refusing to overwrite existing paths: ${conflicts.join(', ')}`);
58
+ }
59
+ }
60
+ function createWriter(dir, files) {
61
+ return (relativePath, content) => {
62
+ const filePath = join(dir, relativePath);
63
+ mkdirSync(dirname(filePath), { recursive: true });
64
+ writeFileSync(filePath, content);
65
+ files.push(relativePath);
66
+ return filePath;
67
+ };
68
+ }
69
+ function writePreparedRootFiles(dir, packageJson, files) {
70
+ if (writeGitignore(dir))
71
+ files.push('.gitignore');
72
+ writeFileSync(join(dir, 'package.json'), formatJson(packageJson));
73
+ files.push('package.json');
74
+ }
75
+ function writeGitignore(dir) {
76
+ const filePath = join(dir, '.gitignore');
77
+ if (!existsSync(filePath)) {
78
+ writeFileSync(filePath, DEFAULT_GITIGNORE);
79
+ return true;
80
+ }
81
+ const existing = readFileSync(filePath, 'utf8');
82
+ if (existing.split(/\r?\n/).some((line) => line.trim() === '.sanity/'))
83
+ return false;
84
+ writeFileSync(filePath, `${existing.trimEnd()}\n\n${GITIGNORE_ENTRY}\n`);
85
+ return true;
86
+ }
87
+ function preparePackageJson(options, requiredWorkspaces) {
88
+ const filePath = join(options.dir, 'package.json');
89
+ const parsed = existsSync(filePath)
90
+ ? JSON.parse(readFileSync(filePath, 'utf8'))
91
+ : {
92
+ name: options.stackName,
93
+ version: '0.1.0',
94
+ private: true,
95
+ type: 'module',
96
+ };
97
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
98
+ throw new Error('The existing package.json must contain a JSON object.');
99
+ }
100
+ const packageJson = parsed;
101
+ addWorkspaces(packageJson, requiredWorkspaces);
102
+ addBlueprintsDependency(packageJson, options.blueprintsVersion);
103
+ return packageJson;
104
+ }
105
+ function addWorkspaces(packageJson, required) {
106
+ if (required.length === 0)
107
+ return;
108
+ if (!packageJson.workspaces) {
109
+ packageJson.workspaces = required;
110
+ return;
111
+ }
112
+ if (Array.isArray(packageJson.workspaces)) {
113
+ packageJson.workspaces = [...new Set([...packageJson.workspaces, ...required])];
114
+ return;
115
+ }
116
+ if (!packageJson.workspaces || typeof packageJson.workspaces !== 'object') {
117
+ throw new Error('Cannot add generated packages to the existing package.json workspaces.');
118
+ }
119
+ const workspaces = packageJson.workspaces;
120
+ if (Array.isArray(workspaces.packages)) {
121
+ workspaces.packages = [...new Set([...workspaces.packages, ...required])];
122
+ return;
123
+ }
124
+ throw new Error('Cannot add generated packages to the existing package.json workspaces.');
125
+ }
126
+ function addBlueprintsDependency(packageJson, blueprintsVersion) {
127
+ const dependencies = recordOrEmpty(packageJson.dependencies);
128
+ const devDependencies = recordOrEmpty(packageJson.devDependencies);
129
+ if (dependencies['@sanity/blueprints'] || devDependencies['@sanity/blueprints'])
130
+ return;
131
+ packageJson.devDependencies = {
132
+ ...devDependencies,
133
+ '@sanity/blueprints': blueprintsVersion,
134
+ };
135
+ }
136
+ function recordOrEmpty(value) {
137
+ return value && typeof value === 'object' && !Array.isArray(value)
138
+ ? value
139
+ : {};
140
+ }
141
+ function formatJson(value) {
142
+ return `${JSON.stringify(value, null, 2)}\n`;
143
+ }
@@ -1,6 +1,7 @@
1
1
  import type { BlueprintResource } from '@sanity/blueprints';
2
2
  import type { Logger } from '../../utils/logger.js';
3
3
  import { type AuthParams, type InstallerType, type ScopeType } from '../../utils/types.js';
4
+ import { type ProcessAssetsProgress, type ProcessedAsset } from './process-assets.js';
4
5
  type PreDeployResult = {
5
6
  success: false;
6
7
  error: string;
@@ -10,11 +11,14 @@ type PreDeployResult = {
10
11
  } | {
11
12
  success: true;
12
13
  };
13
- type PreDeployResourceOptions = {
14
- auth: AuthParams;
15
- blueprintFileDir: string;
16
- installer: InstallerType;
17
- log: Logger;
14
+ export type PreDeployAssetEvent = {
15
+ type: 'progress';
16
+ progress: ProcessAssetsProgress;
17
+ } | {
18
+ type: 'complete';
19
+ } | {
20
+ type: 'result';
21
+ result: ProcessedAsset;
18
22
  };
19
23
  type PreDeployOptions = {
20
24
  scopeType: ScopeType;
@@ -24,6 +28,11 @@ type PreDeployOptions = {
24
28
  stackName: string;
25
29
  bin: string;
26
30
  verbose: boolean | undefined;
27
- } & PreDeployResourceOptions;
31
+ auth: AuthParams;
32
+ blueprintFileDir: string;
33
+ installer: InstallerType;
34
+ logger: Logger;
35
+ onAssetEvent?: (event: PreDeployAssetEvent) => void;
36
+ };
28
37
  export declare function preDeploy(options: PreDeployOptions): Promise<PreDeployResult>;
29
38
  export {};
@@ -1,18 +1,13 @@
1
1
  import { env } from 'node:process';
2
2
  import { resolveDeployedResources } from '../../utils/blueprints/resolve-deployed-resources.js';
3
- import { niceId } from '../../utils/display/presenters.js';
4
3
  import { CODE_OPERATION_UNCONFIRMED, EXIT_OPERATION_UNCONFIRMED } from '../../utils/exit-codes.js';
5
4
  import { styleText } from '../../utils/style-text.js';
6
5
  import { isAssetResource, } from '../../utils/types.js';
7
- import { stashAsset } from './assets.js';
6
+ import { processAssets } from './process-assets.js';
8
7
  import { updateStack } from './stacks.js';
9
8
  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
9
  export async function preDeploy(options) {
15
- const { resources, stackId, auth, blueprintFileDir, installer, log } = options;
10
+ const { resources, stackId, auth, blueprintFileDir, installer, logger } = options;
16
11
  const hasAssetResources = resources.some(isAssetResource);
17
12
  if (hasAssetResources) {
18
13
  let assetResources;
@@ -26,34 +21,45 @@ export async function preDeploy(options) {
26
21
  resources,
27
22
  stackId,
28
23
  auth,
29
- logger: log,
24
+ logger,
30
25
  });
31
26
  assetResources = resolvedResources.filter(isAssetResource);
32
27
  }
33
28
  else {
34
29
  assetResources = resources.filter(isAssetResource);
35
30
  }
36
- log('Processing assets...');
37
- for (const resource of assetResources) {
38
- const preDeployResult = await preDeployResource(resource, {
31
+ let processedAssets;
32
+ try {
33
+ processedAssets = await processAssets({
34
+ assetResources,
39
35
  auth,
40
36
  blueprintFileDir,
41
37
  installer,
42
- log,
38
+ logger,
39
+ onProgress: (progress) => options.onAssetEvent?.({ type: 'progress', progress }),
40
+ onResult: (result) => options.onAssetEvent?.({ type: 'result', result }),
43
41
  });
44
- if (!preDeployResult.success)
45
- return preDeployResult;
46
- const idx = resources.findIndex((r) => r.name === resource.name);
42
+ }
43
+ finally {
44
+ options.onAssetEvent?.({ type: 'complete' });
45
+ }
46
+ const failure = processedAssets.find((result) => !result.success);
47
+ if (failure)
48
+ return { success: false, error: failure.error };
49
+ // update resources with processed assets
50
+ for (const asset of processedAssets) {
51
+ const idx = resources.findIndex((r) => r.name === asset.resource.name);
52
+ // ! I think this error message is wrong. we somehow have an extra asset
47
53
  if (idx === -1)
48
54
  throw new Error('Unable to update resource asset');
49
- resources[idx] = preDeployResult.resource;
55
+ resources[idx] = asset.resource;
50
56
  }
51
57
  }
52
58
  return { success: true };
53
59
  }
54
60
  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();
61
+ const { scopeType, scopeId, stackId, stackName, resources, auth, logger, bin, verbose } = options;
62
+ const spinner = logger.ora('Starting pre-deploy...').start();
57
63
  const stackMutation = {
58
64
  scopeType,
59
65
  scopeId,
@@ -61,7 +67,7 @@ async function preDeployStack(options) {
61
67
  preDeploy: true,
62
68
  document: { resources },
63
69
  };
64
- const { ok: deployOk, stack, error: deployError, } = await updateStack({ stackId, stackMutation, auth, logger: log });
70
+ const { ok: deployOk, stack, error: deployError, } = await updateStack({ stackId, stackMutation, auth, logger });
65
71
  if (!deployOk) {
66
72
  spinner.fail(`${styleText('red', 'Failed')} to complete Stack pre-deployment`);
67
73
  return {
@@ -74,19 +80,19 @@ async function preDeployStack(options) {
74
80
  };
75
81
  }
76
82
  spinner.stop().clear();
77
- log(styleText('dim', 'Stack deployment progress:'));
78
- log('');
83
+ logger(styleText('dim', 'Stack deployment progress:'));
84
+ logger('');
79
85
  const outcome = await waitForOperation({
80
86
  stackId,
81
87
  operationId: stack.operationId,
82
88
  auth,
83
- log,
89
+ log: logger,
84
90
  bin,
85
91
  verbose,
86
92
  progressNoun: 'pre-deployment',
87
93
  });
88
94
  if (outcome.type === 'completed') {
89
- log(styleText(['bold', 'green'], 'Stack pre-deployment completed!'));
95
+ logger(styleText(['bold', 'green'], 'Stack pre-deployment completed!'));
90
96
  }
91
97
  else if (outcome.type === 'unconfirmed') {
92
98
  return {
@@ -115,50 +121,3 @@ async function preDeployStack(options) {
115
121
  }
116
122
  return { success: true };
117
123
  }
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,29 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { AssetResource, AuthParams, InstallerType } from '../../utils/types.js';
3
+ type ProcessAssetsOptions = {
4
+ assetResources: AssetResource[];
5
+ auth: AuthParams;
6
+ blueprintFileDir: string;
7
+ installer: InstallerType;
8
+ logger: Logger;
9
+ onProgress?: (progress: ProcessAssetsProgress) => void;
10
+ onResult?: (result: ProcessedAsset) => void;
11
+ };
12
+ export type ProcessAssetsProgress = {
13
+ completed: number;
14
+ total: number;
15
+ slow: boolean;
16
+ };
17
+ export type ProcessedAsset = {
18
+ success: false;
19
+ resource: AssetResource;
20
+ error: string;
21
+ failureKind: 'processing' | 'upload';
22
+ } | {
23
+ success: true;
24
+ resource: AssetResource;
25
+ hash?: string;
26
+ exists?: boolean;
27
+ };
28
+ export declare function processAssets(options: ProcessAssetsOptions): Promise<ProcessedAsset[]>;
29
+ export {};
@@ -0,0 +1,80 @@
1
+ import { env } from 'node:process';
2
+ import { stashAsset } from './assets.js';
3
+ const DEFAULT_ASSET_CONCURRENCY = 4;
4
+ const DEFAULT_ASSET_TIMEOUT = 180;
5
+ const assetTimeoutS = Number(env.SANITY_ASSET_TIMEOUT) || DEFAULT_ASSET_TIMEOUT;
6
+ const assetTimeoutMs = assetTimeoutS * 1000;
7
+ const warnTimeoutMs = assetTimeoutMs / 3;
8
+ export async function processAssets(options) {
9
+ const { assetResources, onProgress, onResult } = options;
10
+ const total = assetResources.length;
11
+ const processedAssets = [];
12
+ const concurrency = getAssetConcurrency();
13
+ let completed = 0;
14
+ let slow = false;
15
+ const reportProgress = () => onProgress?.({ completed, total, slow });
16
+ reportProgress();
17
+ const warnTimer = setTimeout(() => {
18
+ slow = true;
19
+ reportProgress();
20
+ }, warnTimeoutMs);
21
+ try {
22
+ for (let start = 0; start < total; start += concurrency) {
23
+ const batch = assetResources.slice(start, start + concurrency);
24
+ const batchResults = await Promise.all(batch.map(async (resource) => {
25
+ const result = await processAsset(resource, options);
26
+ completed += 1;
27
+ reportProgress();
28
+ onResult?.(result);
29
+ return result;
30
+ }));
31
+ processedAssets.push(...batchResults);
32
+ if (batchResults.some((result) => !result.success))
33
+ break;
34
+ }
35
+ }
36
+ finally {
37
+ clearTimeout(warnTimer);
38
+ }
39
+ return processedAssets;
40
+ }
41
+ function getAssetConcurrency() {
42
+ const configured = Number(env.SANITY_ASSET_CONCURRENCY);
43
+ return Number.isSafeInteger(configured) && configured > 0 ? configured : DEFAULT_ASSET_CONCURRENCY;
44
+ }
45
+ async function processAsset(resource, options) {
46
+ const { auth, blueprintFileDir, installer, logger } = options;
47
+ let assetTimeoutTimer;
48
+ let result;
49
+ try {
50
+ result = await Promise.race([
51
+ stashAsset({ resource, blueprintFileDir, auth, logger, installer }),
52
+ new Promise((_, reject) => {
53
+ assetTimeoutTimer = setTimeout(() => {
54
+ reject(new Error(`Processing ${resource.name} timed out after ${assetTimeoutS}s`));
55
+ }, assetTimeoutMs);
56
+ }),
57
+ ]);
58
+ }
59
+ catch (err) {
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ return { success: false, resource, error: msg, failureKind: 'processing' };
62
+ }
63
+ finally {
64
+ clearTimeout(assetTimeoutTimer);
65
+ }
66
+ if (result.success && result.assetId) {
67
+ return {
68
+ success: true,
69
+ resource: { ...resource, src: result.assetId },
70
+ hash: result.hash,
71
+ exists: result.exists,
72
+ };
73
+ }
74
+ return {
75
+ success: false,
76
+ resource,
77
+ error: result.error || 'Failed to process asset',
78
+ failureKind: 'upload',
79
+ };
80
+ }
@@ -0,0 +1,34 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { AuthParams } from '../../utils/types.js';
3
+ /** A deployed function returned by the Functions API. */
4
+ export interface DeployedFunction {
5
+ id: string;
6
+ name: string | null;
7
+ displayName: string | null;
8
+ type: string | null;
9
+ runtime: string | null;
10
+ status: string | null;
11
+ memory: number | null;
12
+ storage: number | null;
13
+ timeout: number | null;
14
+ resourceType: string | null;
15
+ resourceId: string | null;
16
+ robotId: string | null;
17
+ url: string | null;
18
+ eventRule?: unknown;
19
+ contentSub?: Record<string, unknown> | null;
20
+ createdAt: string | null;
21
+ updatedAt: string | null;
22
+ deletedAt?: string | null;
23
+ }
24
+ type ListFunctionsResponse = {
25
+ ok: true;
26
+ error: null;
27
+ functions: DeployedFunction[];
28
+ } | {
29
+ ok: false;
30
+ error: string;
31
+ functions: [];
32
+ };
33
+ export declare function listFunctions(auth: AuthParams, logger: Logger): Promise<ListFunctionsResponse>;
34
+ export {};
@@ -0,0 +1,33 @@
1
+ import config from '../../config.js';
2
+ import getHeaders from '../../utils/get-headers.js';
3
+ import { createTracedFetch } from '../../utils/traced-fetch.js';
4
+ const { apiUrl } = config;
5
+ export async function listFunctions(auth, logger) {
6
+ try {
7
+ const fetchFn = createTracedFetch(logger);
8
+ const response = await fetchFn(`${apiUrl}v2025-07-30/functions/`, {
9
+ headers: getHeaders(auth),
10
+ method: 'GET',
11
+ });
12
+ const payload = (await response.json());
13
+ if (!response.ok) {
14
+ return {
15
+ ok: false,
16
+ error: payload.error?.message ?? payload.message ?? 'Failed to list functions',
17
+ functions: [],
18
+ };
19
+ }
20
+ return {
21
+ ok: true,
22
+ error: null,
23
+ functions: Array.isArray(payload.functions) ? payload.functions : [],
24
+ };
25
+ }
26
+ catch (error) {
27
+ return {
28
+ ok: false,
29
+ error: error instanceof Error ? error.message : String(error),
30
+ functions: [],
31
+ };
32
+ }
33
+ }
@@ -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
+ }
@@ -138,5 +138,7 @@ export declare abstract class ResolvedCommand<T extends typeof Command> extends
138
138
  protected blueprintConfig?: LocatedBlueprintsConfig | null;
139
139
  protected auth: AuthParams;
140
140
  protected deployedStack: Stack;
141
+ /** Runs after flag parsing and before declared command context is resolved. */
142
+ protected preResolve(): Promise<void>;
141
143
  init(): Promise<void>;
142
144
  }
@@ -220,8 +220,11 @@ export class ResolvedCommand extends RuntimeCommand {
220
220
  blueprintConfig;
221
221
  auth;
222
222
  deployedStack;
223
+ /** Runs after flag parsing and before declared command context is resolved. */
224
+ async preResolve() { }
223
225
  async init() {
224
226
  await super.init(); // RuntimeCommand: parse flags + args
227
+ await this.preResolve();
225
228
  const needs = new Set(this.constructor.needs);
226
229
  if (needs.size === 0)
227
230
  return;
@@ -0,0 +1,17 @@
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
+ dataset: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'stack-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'studio-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<Record<string, unknown> | undefined>;
17
+ }
@@ -0,0 +1,59 @@
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
+ '<%= config.bin %> <%= command.id %> --project-id <id> --dataset <name> --yes',
16
+ ];
17
+ static flags = {
18
+ llms: Flags.boolean(llmsFlagConfig),
19
+ yes: Flags.boolean({
20
+ char: 'y',
21
+ description: 'Skip confirmation prompts',
22
+ default: false,
23
+ }),
24
+ 'organization-id': Flags.string({
25
+ description: 'Organization to use when the account has several',
26
+ aliases: ['organization', 'organizationId', 'org'],
27
+ }),
28
+ 'project-id': Flags.string({
29
+ description: 'Project to use when the organization has several',
30
+ aliases: ['project', 'projectId'],
31
+ }),
32
+ dataset: Flags.string({
33
+ description: 'Dataset to attach',
34
+ }),
35
+ 'stack-name': Flags.string({
36
+ description: 'Name for the new organization-scoped Stack',
37
+ }),
38
+ 'studio-name': Flags.string({
39
+ description: 'Name and hosted slug for the Studio resource',
40
+ }),
41
+ };
42
+ async run() {
43
+ const result = await blueprinitCore({
44
+ bin: this.config.bin,
45
+ log: Logger(this.log.bind(this), this.flags),
46
+ flags: {
47
+ yes: this.flags.yes,
48
+ organizationId: this.flags['organization-id'],
49
+ projectId: this.flags['project-id'],
50
+ datasetName: this.flags.dataset,
51
+ stackName: this.flags['stack-name'],
52
+ studioName: this.flags['studio-name'],
53
+ },
54
+ });
55
+ if (!result.success)
56
+ this.coreError(result);
57
+ return result.json;
58
+ }
59
+ }