@sanity/runtime-cli 17.1.0-next.gcfbd613 → 17.2.0-blueprinit.g62f8017
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -54
- package/SKILL.md +21 -0
- package/dist/actions/blueprinit/manifest.d.ts +13 -0
- package/dist/actions/blueprinit/manifest.js +57 -0
- package/dist/actions/blueprinit/studio.d.ts +6 -0
- package/dist/actions/blueprinit/studio.js +43 -0
- package/dist/actions/blueprinit/templates.d.ts +34 -0
- package/dist/actions/blueprinit/templates.js +83 -0
- package/dist/actions/blueprinit/workspace.d.ts +15 -0
- package/dist/actions/blueprinit/workspace.js +142 -0
- package/dist/actions/blueprints/assets.js +15 -24
- package/dist/actions/blueprints/pre-deploy.d.ts +29 -0
- package/dist/actions/blueprints/pre-deploy.js +164 -0
- package/dist/actions/sanity/datasets.d.ts +15 -0
- package/dist/actions/sanity/datasets.js +26 -0
- package/dist/baseCommands.d.ts +3 -0
- package/dist/baseCommands.js +17 -0
- package/dist/commands/blueprinit.d.ts +13 -0
- package/dist/commands/blueprinit.js +44 -0
- package/dist/commands/functions/env/add.d.ts +3 -0
- package/dist/commands/functions/env/add.js +6 -2
- package/dist/commands/functions/env/list.d.ts +3 -0
- package/dist/commands/functions/env/list.js +9 -3
- package/dist/commands/functions/env/remove.d.ts +3 -0
- package/dist/commands/functions/env/remove.js +9 -3
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/cores/blueprinit/adopt.d.ts +6 -0
- package/dist/cores/blueprinit/adopt.js +90 -0
- package/dist/cores/blueprinit/finish.d.ts +14 -0
- package/dist/cores/blueprinit/finish.js +36 -0
- package/dist/cores/blueprinit/fresh.d.ts +3 -0
- package/dist/cores/blueprinit/fresh.js +88 -0
- package/dist/cores/blueprinit/prompts.d.ts +4 -0
- package/dist/cores/blueprinit/prompts.js +14 -0
- package/dist/cores/blueprinit/selection.d.ts +15 -0
- package/dist/cores/blueprinit/selection.js +63 -0
- package/dist/cores/blueprinit/types.d.ts +13 -0
- package/dist/cores/blueprinit/types.js +1 -0
- package/dist/cores/blueprinit.d.ts +7 -0
- package/dist/cores/blueprinit.js +39 -0
- package/dist/cores/blueprints/deploy.js +16 -64
- package/dist/utils/command-skills.d.ts +2 -0
- package/dist/utils/command-skills.js +17 -0
- package/dist/utils/functions/child-process-wrapper.js +13 -171
- package/dist/utils/functions/child-runtime/console.d.ts +1 -0
- package/dist/utils/functions/child-runtime/console.js +50 -0
- package/dist/utils/functions/child-runtime/invoke.d.ts +4 -0
- package/dist/utils/functions/child-runtime/invoke.js +47 -0
- package/dist/utils/functions/child-runtime/resources.d.ts +4 -0
- package/dist/utils/functions/child-runtime/resources.js +59 -0
- package/dist/utils/studios/prepare-asset.d.ts +6 -0
- package/dist/utils/studios/prepare-asset.js +139 -0
- package/dist/utils/types.d.ts +4 -7
- package/dist/utils/types.js +2 -3
- package/oclif.manifest.json +293 -7
- package/package.json +12 -4
- package/skills/blueprinit.md +40 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare function renderStudioConfig(options: {
|
|
2
|
+
displayName: string;
|
|
3
|
+
projectId: string;
|
|
4
|
+
datasetName: string;
|
|
5
|
+
}): string;
|
|
6
|
+
export declare const STUDIO_SCHEMA_TYPES = "import {post} from './post'\n\nexport const schemaTypes = [post]\n";
|
|
7
|
+
export declare const STUDIO_POST_TYPE = "import {defineField, defineType} from 'sanity'\n\nexport const post = defineType({\n name: 'post',\n title: 'Post',\n type: 'document',\n fields: [\n defineField({name: 'title', type: 'string', validation: (rule) => rule.required()}),\n // The set-slug function fills this when it is empty.\n defineField({name: 'slug', type: 'slug', options: {source: 'title'}}),\n defineField({name: 'publishedAt', title: 'Published at', type: 'datetime'}),\n defineField({name: 'body', type: 'text'}),\n ],\n})\n";
|
|
8
|
+
export declare const FUNCTION_SET_SLUG = "import {createClient} from '@sanity/client'\nimport {documentEventHandler} from '@sanity/functions'\n\nconst slugify = (title: string) =>\n title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 96)\n\nexport const handler = documentEventHandler(async ({context, event}) => {\n const {_id, title, slug} = event.data\n if (!title || slug?.current) return\n\n const client = createClient({apiVersion: '2025-05-01', ...context.clientOptions})\n const current = slugify(title)\n await client.patch(_id).set({slug: {_type: 'slug', current}}).commit()\n\n console.log(`Set slug for ${_id}: ${current}`)\n})\n";
|
|
9
|
+
export declare const studioPackageJson: (name: string) => {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
private: boolean;
|
|
13
|
+
scripts: {
|
|
14
|
+
dev: string;
|
|
15
|
+
build: string;
|
|
16
|
+
deploy: string;
|
|
17
|
+
};
|
|
18
|
+
dependencies: {
|
|
19
|
+
react: string;
|
|
20
|
+
'react-dom': string;
|
|
21
|
+
sanity: string;
|
|
22
|
+
'styled-components': string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export declare const functionPackageJson: (name: string) => {
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
private: boolean;
|
|
29
|
+
type: string;
|
|
30
|
+
dependencies: {
|
|
31
|
+
'@sanity/client': string;
|
|
32
|
+
'@sanity/functions': string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { STARTER_DOCUMENT_TYPE, STARTER_FUNCTION_NAME } from './manifest.js';
|
|
2
|
+
const quote = (value) => `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
|
|
3
|
+
export function renderStudioConfig(options) {
|
|
4
|
+
return `import {defineConfig} from 'sanity'
|
|
5
|
+
import {structureTool} from 'sanity/structure'
|
|
6
|
+
import {schemaTypes} from './schemaTypes'
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
name: 'default',
|
|
10
|
+
title: ${quote(options.displayName)},
|
|
11
|
+
projectId: ${quote(options.projectId)},
|
|
12
|
+
dataset: ${quote(options.datasetName)},
|
|
13
|
+
plugins: [structureTool()],
|
|
14
|
+
schema: {types: schemaTypes},
|
|
15
|
+
})
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
export const STUDIO_SCHEMA_TYPES = `import {post} from './post'
|
|
19
|
+
|
|
20
|
+
export const schemaTypes = [post]
|
|
21
|
+
`;
|
|
22
|
+
export const STUDIO_POST_TYPE = `import {defineField, defineType} from 'sanity'
|
|
23
|
+
|
|
24
|
+
export const post = defineType({
|
|
25
|
+
name: '${STARTER_DOCUMENT_TYPE}',
|
|
26
|
+
title: 'Post',
|
|
27
|
+
type: 'document',
|
|
28
|
+
fields: [
|
|
29
|
+
defineField({name: 'title', type: 'string', validation: (rule) => rule.required()}),
|
|
30
|
+
// The ${STARTER_FUNCTION_NAME} function fills this when it is empty.
|
|
31
|
+
defineField({name: 'slug', type: 'slug', options: {source: 'title'}}),
|
|
32
|
+
defineField({name: 'publishedAt', title: 'Published at', type: 'datetime'}),
|
|
33
|
+
defineField({name: 'body', type: 'text'}),
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
`;
|
|
37
|
+
export const FUNCTION_SET_SLUG = `import {createClient} from '@sanity/client'
|
|
38
|
+
import {documentEventHandler} from '@sanity/functions'
|
|
39
|
+
|
|
40
|
+
const slugify = (title: string) =>
|
|
41
|
+
title
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
44
|
+
.replace(/^-+|-+$/g, '')
|
|
45
|
+
.slice(0, 96)
|
|
46
|
+
|
|
47
|
+
export const handler = documentEventHandler(async ({context, event}) => {
|
|
48
|
+
const {_id, title, slug} = event.data
|
|
49
|
+
if (!title || slug?.current) return
|
|
50
|
+
|
|
51
|
+
const client = createClient({apiVersion: '2025-05-01', ...context.clientOptions})
|
|
52
|
+
const current = slugify(title)
|
|
53
|
+
await client.patch(_id).set({slug: {_type: 'slug', current}}).commit()
|
|
54
|
+
|
|
55
|
+
console.log(\`Set slug for \${_id}: \${current}\`)
|
|
56
|
+
})
|
|
57
|
+
`;
|
|
58
|
+
export const studioPackageJson = (name) => ({
|
|
59
|
+
name,
|
|
60
|
+
version: '0.1.0',
|
|
61
|
+
private: true,
|
|
62
|
+
scripts: {
|
|
63
|
+
dev: 'sanity dev',
|
|
64
|
+
build: 'sanity build',
|
|
65
|
+
deploy: 'sanity deploy',
|
|
66
|
+
},
|
|
67
|
+
dependencies: {
|
|
68
|
+
react: '^19',
|
|
69
|
+
'react-dom': '^19',
|
|
70
|
+
sanity: '^4',
|
|
71
|
+
'styled-components': '^6',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
export const functionPackageJson = (name) => ({
|
|
75
|
+
name,
|
|
76
|
+
version: '0.1.0',
|
|
77
|
+
private: true,
|
|
78
|
+
type: 'module',
|
|
79
|
+
dependencies: {
|
|
80
|
+
'@sanity/client': '^7',
|
|
81
|
+
'@sanity/functions': '^1',
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface WorkspaceOptions {
|
|
2
|
+
dir: string;
|
|
3
|
+
projectId: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
datasetName: string;
|
|
6
|
+
stackName: string;
|
|
7
|
+
studioName: string;
|
|
8
|
+
blueprintsVersion: string;
|
|
9
|
+
}
|
|
10
|
+
export interface WorkspaceResult {
|
|
11
|
+
blueprintFilePath: string;
|
|
12
|
+
files: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function scaffoldWorkspace(options: WorkspaceOptions): WorkspaceResult;
|
|
15
|
+
export declare function adoptWorkspace(options: WorkspaceOptions): WorkspaceResult;
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function assertTargetsAvailable(dir, paths) {
|
|
54
|
+
const conflicts = paths.filter((path) => existsSync(join(dir, path)));
|
|
55
|
+
if (conflicts.length > 0) {
|
|
56
|
+
throw new Error(`Refusing to overwrite existing paths: ${conflicts.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function createWriter(dir, files) {
|
|
60
|
+
return (relativePath, content) => {
|
|
61
|
+
const filePath = join(dir, relativePath);
|
|
62
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
63
|
+
writeFileSync(filePath, content);
|
|
64
|
+
files.push(relativePath);
|
|
65
|
+
return filePath;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function writePreparedRootFiles(dir, packageJson, files) {
|
|
69
|
+
if (writeGitignore(dir))
|
|
70
|
+
files.push('.gitignore');
|
|
71
|
+
writeFileSync(join(dir, 'package.json'), formatJson(packageJson));
|
|
72
|
+
files.push('package.json');
|
|
73
|
+
}
|
|
74
|
+
function writeGitignore(dir) {
|
|
75
|
+
const filePath = join(dir, '.gitignore');
|
|
76
|
+
if (!existsSync(filePath)) {
|
|
77
|
+
writeFileSync(filePath, DEFAULT_GITIGNORE);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
81
|
+
if (existing.split(/\r?\n/).some((line) => line.trim() === '.sanity/'))
|
|
82
|
+
return false;
|
|
83
|
+
writeFileSync(filePath, `${existing.trimEnd()}\n\n${GITIGNORE_ENTRY}\n`);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
function preparePackageJson(options, requiredWorkspaces) {
|
|
87
|
+
const filePath = join(options.dir, 'package.json');
|
|
88
|
+
const parsed = existsSync(filePath)
|
|
89
|
+
? JSON.parse(readFileSync(filePath, 'utf8'))
|
|
90
|
+
: {
|
|
91
|
+
name: options.stackName,
|
|
92
|
+
version: '0.1.0',
|
|
93
|
+
private: true,
|
|
94
|
+
type: 'module',
|
|
95
|
+
};
|
|
96
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
97
|
+
throw new Error('The existing package.json must contain a JSON object.');
|
|
98
|
+
}
|
|
99
|
+
const packageJson = parsed;
|
|
100
|
+
addWorkspaces(packageJson, requiredWorkspaces);
|
|
101
|
+
addBlueprintsDependency(packageJson, options.blueprintsVersion);
|
|
102
|
+
return packageJson;
|
|
103
|
+
}
|
|
104
|
+
function addWorkspaces(packageJson, required) {
|
|
105
|
+
if (required.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
if (!packageJson.workspaces) {
|
|
108
|
+
packageJson.workspaces = required;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(packageJson.workspaces)) {
|
|
112
|
+
packageJson.workspaces = [...new Set([...packageJson.workspaces, ...required])];
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!packageJson.workspaces || typeof packageJson.workspaces !== 'object') {
|
|
116
|
+
throw new Error('Cannot add generated packages to the existing package.json workspaces.');
|
|
117
|
+
}
|
|
118
|
+
const workspaces = packageJson.workspaces;
|
|
119
|
+
if (Array.isArray(workspaces.packages)) {
|
|
120
|
+
workspaces.packages = [...new Set([...workspaces.packages, ...required])];
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
throw new Error('Cannot add generated packages to the existing package.json workspaces.');
|
|
124
|
+
}
|
|
125
|
+
function addBlueprintsDependency(packageJson, blueprintsVersion) {
|
|
126
|
+
const dependencies = recordOrEmpty(packageJson.dependencies);
|
|
127
|
+
const devDependencies = recordOrEmpty(packageJson.devDependencies);
|
|
128
|
+
if (dependencies['@sanity/blueprints'] || devDependencies['@sanity/blueprints'])
|
|
129
|
+
return;
|
|
130
|
+
packageJson.devDependencies = {
|
|
131
|
+
...devDependencies,
|
|
132
|
+
'@sanity/blueprints': blueprintsVersion,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function recordOrEmpty(value) {
|
|
136
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
137
|
+
? value
|
|
138
|
+
: {};
|
|
139
|
+
}
|
|
140
|
+
function formatJson(value) {
|
|
141
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
142
|
+
}
|
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/baseCommands.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare const pathFlagConfig: {
|
|
|
14
14
|
};
|
|
15
15
|
export declare const baseFlags: {
|
|
16
16
|
json: Interfaces.BooleanFlag<boolean>;
|
|
17
|
+
llms: Interfaces.BooleanFlag<boolean>;
|
|
17
18
|
path: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
18
19
|
trace: Interfaces.BooleanFlag<boolean>;
|
|
19
20
|
'validate-resources': Interfaces.BooleanFlag<boolean>;
|
|
@@ -42,6 +43,7 @@ export declare abstract class RuntimeCommand<T extends typeof Command> extends C
|
|
|
42
43
|
protected args: Args<T>;
|
|
43
44
|
static baseFlags: {
|
|
44
45
|
json: Interfaces.BooleanFlag<boolean>;
|
|
46
|
+
llms: Interfaces.BooleanFlag<boolean>;
|
|
45
47
|
path: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
46
48
|
trace: Interfaces.BooleanFlag<boolean>;
|
|
47
49
|
'validate-resources': Interfaces.BooleanFlag<boolean>;
|
|
@@ -113,6 +115,7 @@ export declare abstract class ResolvedCommand<T extends typeof Command> extends
|
|
|
113
115
|
static needs: readonly Need[];
|
|
114
116
|
static baseFlags: {
|
|
115
117
|
json: Interfaces.BooleanFlag<boolean>;
|
|
118
|
+
llms: Interfaces.BooleanFlag<boolean>;
|
|
116
119
|
path: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
117
120
|
trace: Interfaces.BooleanFlag<boolean>;
|
|
118
121
|
'validate-resources': Interfaces.BooleanFlag<boolean>;
|
package/dist/baseCommands.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resolveIds } from './actions/blueprints/resolve.js';
|
|
|
6
6
|
import { getStack, resolveStackIdByNameOrId } from './actions/blueprints/stacks.js';
|
|
7
7
|
import config from './config.js';
|
|
8
8
|
import { CI_SETUP_DOCS_URL } from './constants.js';
|
|
9
|
+
import { commandSkillFilename, readCommandSkill } from './utils/command-skills.js';
|
|
9
10
|
import { presentBlueprintParserErrors } from './utils/display/errors.js';
|
|
10
11
|
import { Logger } from './utils/logger.js';
|
|
11
12
|
import { styleText } from './utils/style-text.js';
|
|
@@ -30,6 +31,10 @@ export const baseFlags = {
|
|
|
30
31
|
// override defaults from oclif's built-in --json flag
|
|
31
32
|
description: 'Format output as json',
|
|
32
33
|
}),
|
|
34
|
+
llms: OclifFlags.boolean({
|
|
35
|
+
description: 'Show agent-oriented guidance for this command',
|
|
36
|
+
aliases: ['skill'],
|
|
37
|
+
}),
|
|
33
38
|
path: OclifFlags.string({
|
|
34
39
|
...pathFlagConfig,
|
|
35
40
|
hidden: true,
|
|
@@ -118,6 +123,18 @@ export class RuntimeCommand extends Command {
|
|
|
118
123
|
return commandHelp.generate();
|
|
119
124
|
}
|
|
120
125
|
async init() {
|
|
126
|
+
if (this.argv.some((arg) => arg === '--llms' || arg === '--skill')) {
|
|
127
|
+
const commandId = this.id ?? this.ctor.id;
|
|
128
|
+
const skill = await readCommandSkill(commandId);
|
|
129
|
+
if (!skill) {
|
|
130
|
+
const commandName = commandId.replaceAll(':', ' ');
|
|
131
|
+
this.error(`No agent guidance is available for \`${commandName}\`.`, {
|
|
132
|
+
suggestions: [`Add it at skills/${commandSkillFilename(commandId)}.`],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write(skill.endsWith('\n') ? skill : `${skill}\n`);
|
|
136
|
+
this.exit();
|
|
137
|
+
}
|
|
121
138
|
const { args, flags } = await this.parse({
|
|
122
139
|
flags: this.ctor.flags,
|
|
123
140
|
baseFlags: super.ctor.baseFlags,
|