@sanity/runtime-cli 8.0.3 → 8.1.0

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 CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @sanity/runtime-cli
20
20
  $ sanity-run COMMAND
21
21
  running command...
22
22
  $ sanity-run (--version)
23
- @sanity/runtime-cli/8.0.3 linux-x64 node-v22.16.0
23
+ @sanity/runtime-cli/8.1.0 linux-x64 node-v22.16.0
24
24
  $ sanity-run --help [COMMAND]
25
25
  USAGE
26
26
  $ sanity-run COMMAND
@@ -52,15 +52,16 @@ Add a Resource to a Blueprint
52
52
 
53
53
  ```
54
54
  USAGE
55
- $ sanity-run blueprints add TYPE [--fn-type document-publish -n <value>] [--fn-language ts|js] [--javascript]
56
- [--fn-helpers] [-i | --fn-installer skip|npm|pnpm|yarn]
55
+ $ sanity-run blueprints add TYPE [--example <value> | -n <value> | --fn-type document-publish | --fn-language
56
+ ts|js | --javascript | --fn-helpers | --fn-installer skip|npm|pnpm|yarn] [-i | ]
57
57
 
58
58
  ARGUMENTS
59
- TYPE (function) Type of resource to add (e.g. function)
59
+ TYPE (function) Type of Resource to add (e.g. function)
60
60
 
61
61
  FLAGS
62
62
  -i, --install Shortcut for --fn-installer npm
63
63
  -n, --name=<value> Name of the Resource to add
64
+ --example=<value> Example to use for the Resource
64
65
  --[no-]fn-helpers Add helpers to the new Function
65
66
  --fn-installer=<option> How to install the @sanity/functions helpers
66
67
  <options: skip|npm|pnpm|yarn>
@@ -85,7 +86,7 @@ EXAMPLES
85
86
  $ sanity-run blueprints add function --name my-function --fn-type document-publish --lang js
86
87
  ```
87
88
 
88
- _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/blueprints/add.ts)_
89
+ _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/blueprints/add.ts)_
89
90
 
90
91
  ## `sanity-run blueprints config`
91
92
 
@@ -116,7 +117,7 @@ EXAMPLES
116
117
  $ sanity-run blueprints config --edit --project-id <projectId> --stack-id <stackId>
117
118
  ```
118
119
 
119
- _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/blueprints/config.ts)_
120
+ _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/blueprints/config.ts)_
120
121
 
121
122
  ## `sanity-run blueprints deploy`
122
123
 
@@ -138,7 +139,7 @@ EXAMPLES
138
139
  $ sanity-run blueprints deploy --no-wait
139
140
  ```
140
141
 
141
- _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/blueprints/deploy.ts)_
142
+ _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/blueprints/deploy.ts)_
142
143
 
143
144
  ## `sanity-run blueprints destroy`
144
145
 
@@ -163,7 +164,7 @@ EXAMPLES
163
164
  $ sanity-run blueprints destroy --stack-id <stackId> --project-id <projectId> --force --no-wait
164
165
  ```
165
166
 
166
- _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/blueprints/destroy.ts)_
167
+ _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/blueprints/destroy.ts)_
167
168
 
168
169
  ## `sanity-run blueprints info`
169
170
 
@@ -185,7 +186,7 @@ EXAMPLES
185
186
  $ sanity-run blueprints info --stack-id <stackId>
186
187
  ```
187
188
 
188
- _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/blueprints/info.ts)_
189
+ _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/blueprints/info.ts)_
189
190
 
190
191
  ## `sanity-run blueprints init [DIR]`
191
192
 
@@ -193,8 +194,8 @@ Initialize a new Blueprint
193
194
 
194
195
  ```
195
196
  USAGE
196
- $ sanity-run blueprints init [DIR] [--dir <value>] [--blueprint-type json|js|ts] [--stack-name <value> |
197
- [--stack-id <value> --project-id <value>]]
197
+ $ sanity-run blueprints init [DIR] [--dir <value>] [--example <value> | --blueprint-type json|js|ts | --stack-id
198
+ <value> | --stack-name <value>] [--project-id <value>]
198
199
 
199
200
  ARGUMENTS
200
201
  DIR Directory to create the Blueprint in
@@ -203,6 +204,7 @@ FLAGS
203
204
  --blueprint-type=<option> Blueprint manifest type to use for the Blueprint
204
205
  <options: json|js|ts>
205
206
  --dir=<value> Directory to create the Blueprint in
207
+ --example=<value> Example to use for the Blueprint
206
208
  --project-id=<value> Sanity Project ID to use for the Blueprint
207
209
  --stack-id=<value> Existing Stack ID to use for the Blueprint
208
210
  --stack-name=<value> Name to use for a NEW Stack
@@ -222,7 +224,7 @@ EXAMPLES
222
224
  $ sanity-run blueprints init --blueprint-type <json|js|ts> --stack-name <stackName>
223
225
  ```
224
226
 
225
- _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/blueprints/init.ts)_
227
+ _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/blueprints/init.ts)_
226
228
 
227
229
  ## `sanity-run blueprints logs`
228
230
 
@@ -244,7 +246,7 @@ EXAMPLES
244
246
  $ sanity-run blueprints logs --watch
245
247
  ```
246
248
 
247
- _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/blueprints/logs.ts)_
249
+ _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/blueprints/logs.ts)_
248
250
 
249
251
  ## `sanity-run blueprints plan`
250
252
 
@@ -261,7 +263,7 @@ EXAMPLES
261
263
  $ sanity-run blueprints plan
262
264
  ```
263
265
 
264
- _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/blueprints/plan.ts)_
266
+ _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/blueprints/plan.ts)_
265
267
 
266
268
  ## `sanity-run blueprints stacks`
267
269
 
@@ -283,7 +285,7 @@ EXAMPLES
283
285
  $ sanity-run blueprints stacks --project-id <projectId>
284
286
  ```
285
287
 
286
- _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/blueprints/stacks.ts)_
288
+ _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/blueprints/stacks.ts)_
287
289
 
288
290
  ## `sanity-run functions dev`
289
291
 
@@ -303,7 +305,7 @@ EXAMPLES
303
305
  $ sanity-run functions dev --port 8974
304
306
  ```
305
307
 
306
- _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/functions/dev.ts)_
308
+ _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/functions/dev.ts)_
307
309
 
308
310
  ## `sanity-run functions env add NAME KEY VALUE`
309
311
 
@@ -325,7 +327,7 @@ EXAMPLES
325
327
  $ sanity-run functions env add MyFunction API_URL https://api.example.com/
326
328
  ```
327
329
 
328
- _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/functions/env/add.ts)_
330
+ _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/functions/env/add.ts)_
329
331
 
330
332
  ## `sanity-run functions env list NAME`
331
333
 
@@ -345,7 +347,7 @@ EXAMPLES
345
347
  $ sanity-run functions env list MyFunction
346
348
  ```
347
349
 
348
- _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/functions/env/list.ts)_
350
+ _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/functions/env/list.ts)_
349
351
 
350
352
  ## `sanity-run functions env remove NAME KEY`
351
353
 
@@ -366,7 +368,7 @@ EXAMPLES
366
368
  $ sanity-run functions env remove MyFunction API_URL
367
369
  ```
368
370
 
369
- _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/functions/env/remove.ts)_
371
+ _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/functions/env/remove.ts)_
370
372
 
371
373
  ## `sanity-run functions logs NAME`
372
374
 
@@ -400,7 +402,7 @@ EXAMPLES
400
402
  $ sanity-run functions logs <name> --delete
401
403
  ```
402
404
 
403
- _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/functions/logs.ts)_
405
+ _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/functions/logs.ts)_
404
406
 
405
407
  ## `sanity-run functions test NAME`
406
408
 
@@ -433,7 +435,7 @@ EXAMPLES
433
435
  $ sanity-run functions test <name> --data '{ "id": 1 }' --timeout 60
434
436
  ```
435
437
 
436
- _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v8.0.3/src/commands/functions/test.ts)_
438
+ _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v8.1.0/src/commands/functions/test.ts)_
437
439
 
438
440
  ## `sanity-run help [COMMAND]`
439
441
 
@@ -0,0 +1,26 @@
1
+ export declare const EXAMPLES_CACHE_DIR: string;
2
+ declare const EXAMPLE_TYPES: {
3
+ blueprint: string;
4
+ function: string;
5
+ };
6
+ export declare function verifyExampleExists({ type, name, }: {
7
+ type: keyof typeof EXAMPLE_TYPES;
8
+ name: string;
9
+ }): Promise<boolean>;
10
+ /**
11
+ * Downloads an example from the examples repo and writes it to disk.
12
+ * @sideEffect Creates the example directory and writes the example to disk.
13
+ * @returns The example files and directory and the function config if it exists.
14
+ */
15
+ export declare function writeExample({ ownerRepo, exampleType, exampleName, dir, }: {
16
+ ownerRepo?: string;
17
+ exampleType: keyof typeof EXAMPLE_TYPES;
18
+ exampleName: string;
19
+ dir?: string;
20
+ }): Promise<false | {
21
+ files: Record<string, string>;
22
+ dir: string;
23
+ instructions: string | null;
24
+ functionConfig: Record<string, unknown> | null;
25
+ }>;
26
+ export {};
@@ -0,0 +1,176 @@
1
+ import { createReadStream, createWriteStream, existsSync, mkdirSync, statSync, writeFileSync, } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { createGunzip } from 'node:zlib';
6
+ import { extract } from 'tar-stream';
7
+ import { gitHubRequest } from '../../utils/other/github.js';
8
+ export const EXAMPLES_CACHE_DIR = join(tmpdir(), 'sanity-examples');
9
+ const EXAMPLES_REPO = 'sanity-io/sanity';
10
+ const EXAMPLES_DIR = 'examples';
11
+ const BLUEPRINTS_DIR = `${EXAMPLES_DIR}/blueprints`;
12
+ const FUNCTIONS_DIR = `${EXAMPLES_DIR}/functions`;
13
+ const EXAMPLE_TYPES = {
14
+ blueprint: BLUEPRINTS_DIR,
15
+ function: FUNCTIONS_DIR,
16
+ };
17
+ export async function verifyExampleExists({ type, name, }) {
18
+ const examplePath = `${EXAMPLE_TYPES[type]}/${name}`;
19
+ const path = `/repos/${EXAMPLES_REPO}/contents/${examplePath}`;
20
+ const response = await gitHubRequest(path);
21
+ return response.ok;
22
+ }
23
+ async function downloadRepoArchive(ownerRepo, ref = 'main') {
24
+ // cache the archive in a temp directory
25
+ const cacheKey = `${ownerRepo.replace('/', '-')}-${ref}.tar.gz`;
26
+ const cacheDir = EXAMPLES_CACHE_DIR;
27
+ const cachePath = join(cacheDir, cacheKey);
28
+ if (existsSync(cachePath)) {
29
+ const stats = statSync(cachePath);
30
+ const ageMinutes = (Date.now() - stats.mtime.getTime()) / (1000 * 60);
31
+ if (ageMinutes < 5)
32
+ return createReadStream(cachePath);
33
+ }
34
+ const path = `/repos/${ownerRepo}/tarball/${ref}`;
35
+ try {
36
+ const response = await gitHubRequest(path);
37
+ if (!response.ok)
38
+ throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
39
+ if (!response.body)
40
+ throw new Error('No response body received');
41
+ mkdirSync(cacheDir, { recursive: true });
42
+ const cacheWriteStream = createWriteStream(cachePath);
43
+ try {
44
+ // converting web streams to node streams is awkward
45
+ await pipeline(response.body, cacheWriteStream);
46
+ }
47
+ catch (error) {
48
+ console.debug(error);
49
+ return response.body;
50
+ }
51
+ return createReadStream(cachePath);
52
+ }
53
+ catch (error) {
54
+ console.error('❌ Error:', error instanceof Error ? error.message : 'Unknown error');
55
+ return null;
56
+ }
57
+ }
58
+ async function extractExampleFromArchive({ archiveStream, exampleType, exampleName, }) {
59
+ const fullPath = `${EXAMPLE_TYPES[exampleType]}/${exampleName}`;
60
+ const files = {};
61
+ let repoPrefix = null;
62
+ const extractStream = extract();
63
+ extractStream.on('entry', (header, stream, next) => {
64
+ const { name, type } = header;
65
+ // GitHub tarballs have a top-level directory like "repo-name-commit-hash/"
66
+ // detect it and strip it
67
+ if (!repoPrefix && name.includes('/'))
68
+ repoPrefix = name.split('/')[0];
69
+ // remove the repo prefix to get the actual file path
70
+ const cleanPath = repoPrefix ? name.replace(`${repoPrefix}/`, '') : name;
71
+ // check if this file is in our target example directory
72
+ if (cleanPath.startsWith(`${fullPath}/`) || cleanPath === fullPath) {
73
+ if (type === 'file') {
74
+ const chunks = [];
75
+ stream.on('data', (chunk) => chunks.push(chunk));
76
+ stream.on('end', () => {
77
+ const content = Buffer.concat(chunks).toString('utf8');
78
+ files[cleanPath.replace(`${fullPath}/`, '')] = content;
79
+ next();
80
+ });
81
+ }
82
+ else {
83
+ stream.resume(); // skip directories
84
+ next();
85
+ }
86
+ }
87
+ else {
88
+ stream.resume();
89
+ next();
90
+ }
91
+ });
92
+ // pipes!
93
+ await pipeline(archiveStream, createGunzip(), extractStream);
94
+ if (Object.keys(files).length === 0)
95
+ return null;
96
+ return files;
97
+ }
98
+ function writeExampleToDisk(files, targetDir, exampleName) {
99
+ for (const [filePath, content] of Object.entries(files)) {
100
+ // Remove the example prefix from the file path for local storage
101
+ const localPath = filePath.startsWith(`${exampleName}/`)
102
+ ? filePath.replace(`${exampleName}/`, '')
103
+ : filePath;
104
+ const fullPath = join(targetDir, localPath);
105
+ const dir = dirname(fullPath);
106
+ // Create directory if it doesn't exist
107
+ mkdirSync(dir, { recursive: true });
108
+ // Write file
109
+ writeFileSync(fullPath, content);
110
+ }
111
+ }
112
+ /**
113
+ * @sideEffect Modifies the example files to remove the function config from the package.json.
114
+ * @returns The function config if it exists.
115
+ */
116
+ function extractFunctionConfig(exampleFiles) {
117
+ const packageJson = exampleFiles['package.json'];
118
+ if (!packageJson)
119
+ return null;
120
+ let functionConfig = null;
121
+ try {
122
+ const packageJsonContent = JSON.parse(packageJson);
123
+ functionConfig = packageJsonContent.blueprintResourceItem;
124
+ packageJsonContent.blueprintResourceItem = undefined;
125
+ exampleFiles['package.json'] = JSON.stringify(packageJsonContent, null, 2);
126
+ }
127
+ catch (error) {
128
+ return null;
129
+ }
130
+ return functionConfig;
131
+ }
132
+ /**
133
+ * @sideEffect Modifies the example files to remove the instructions from the package.json.
134
+ * @returns The instructions if they exist.
135
+ */
136
+ function extractInstructions(exampleFiles) {
137
+ const packageJson = exampleFiles['package.json'];
138
+ if (!packageJson)
139
+ return null;
140
+ let instructions = null;
141
+ try {
142
+ const packageJsonContent = JSON.parse(packageJson);
143
+ instructions = packageJsonContent.exampleInstructions;
144
+ packageJsonContent.exampleInstructions = undefined;
145
+ exampleFiles['package.json'] = JSON.stringify(packageJsonContent, null, 2);
146
+ }
147
+ catch (error) {
148
+ return null;
149
+ }
150
+ return instructions;
151
+ }
152
+ /**
153
+ * Downloads an example from the examples repo and writes it to disk.
154
+ * @sideEffect Creates the example directory and writes the example to disk.
155
+ * @returns The example files and directory and the function config if it exists.
156
+ */
157
+ export async function writeExample({ ownerRepo = EXAMPLES_REPO, exampleType, exampleName, dir = './tmp', }) {
158
+ const archiveStream = await downloadRepoArchive(ownerRepo);
159
+ if (!archiveStream)
160
+ return false;
161
+ const exampleFiles = await extractExampleFromArchive({
162
+ archiveStream,
163
+ exampleType,
164
+ exampleName,
165
+ });
166
+ if (!exampleFiles)
167
+ return false;
168
+ let instructions = null;
169
+ instructions = extractInstructions(exampleFiles);
170
+ let functionConfig = null;
171
+ if (exampleType === 'function')
172
+ functionConfig = extractFunctionConfig(exampleFiles);
173
+ mkdirSync(dir, { recursive: true });
174
+ writeExampleToDisk(exampleFiles, dir, exampleName);
175
+ return { files: exampleFiles, dir, instructions, functionConfig };
176
+ }
@@ -6,6 +6,7 @@ export default class AddCommand extends BlueprintCommand<typeof AddCommand> {
6
6
  type: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
8
  static flags: {
9
+ example: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  'fn-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  'fn-language': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -12,12 +12,17 @@ export default class AddCommand extends BlueprintCommand {
12
12
  ];
13
13
  static args = {
14
14
  type: Args.string({
15
- description: 'Type of resource to add (e.g. function)',
15
+ description: 'Type of Resource to add (e.g. function)',
16
16
  options: ['function'],
17
17
  required: true,
18
18
  }),
19
19
  };
20
20
  static flags = {
21
+ example: Flags.string({
22
+ description: 'Example to use for the Resource',
23
+ aliases: ['recipe'],
24
+ exclusive: ['name', 'fn-type', 'fn-language', 'javascript', 'fn-helpers', 'fn-installer'], // set automatically
25
+ }),
21
26
  name: Flags.string({
22
27
  description: 'Name of the Resource to add',
23
28
  char: 'n',
@@ -7,6 +7,7 @@ export default class InitCommand extends Command {
7
7
  };
8
8
  static flags: {
9
9
  dir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ example: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  'blueprint-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  'stack-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -19,6 +19,11 @@ export default class InitCommand extends Command {
19
19
  dir: Flags.string({
20
20
  description: 'Directory to create the Blueprint in',
21
21
  }),
22
+ example: Flags.string({
23
+ description: 'Example to use for the Blueprint',
24
+ aliases: ['recipe'],
25
+ exclusive: ['blueprint-type', 'stack-id', 'stack-name'], // set automatically
26
+ }),
22
27
  'blueprint-type': Flags.string({
23
28
  description: 'Blueprint manifest type to use for the Blueprint',
24
29
  options: ['json', 'js', 'ts'],
@@ -6,6 +6,7 @@ export interface BlueprintAddOptions extends CoreConfig {
6
6
  type: string;
7
7
  };
8
8
  flags: {
9
+ example?: string;
9
10
  name?: string;
10
11
  'fn-type'?: string;
11
12
  language?: string;
@@ -1,8 +1,12 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
1
3
  import { cwd } from 'node:process';
2
4
  import { highlight } from 'cardinal';
3
5
  import chalk from 'chalk';
4
6
  import inquirer from 'inquirer';
5
7
  import { createFunctionResource } from '../../actions/blueprints/resources.js';
8
+ import { verifyExampleExists, writeExample } from '../../actions/sanity/examples.js';
9
+ import { check, indent, warn } from '../../utils/display/presenters.js';
6
10
  import { validateFunctionName } from '../../utils/validate/resource.js';
7
11
  const FUNCTION_BLUEPRINT_RESOURCE_TEMPLATE = `
8
12
  export default defineBlueprint({
@@ -14,9 +18,11 @@ const FUNCTION_BLUEPRINT_RESOURCE_TEMPLATE = `
14
18
  })
15
19
  `;
16
20
  export async function blueprintAddCore(options) {
21
+ const root = cwd();
17
22
  const { bin = 'sanity', log, blueprint, args, flags } = options;
23
+ const { blueprintFilePath } = blueprint.fileInfo;
18
24
  const { type: resourceType } = args;
19
- const { name: flagResourceName, 'fn-type': flagFnType, javascript: flagJs, 'fn-helpers': flagFnHelpers, install: flagI, 'fn-installer': flagFnInstaller, // can be 'skip'!
25
+ const { example: flagExample, name: flagResourceName, 'fn-type': flagFnType, javascript: flagJs, 'fn-helpers': flagFnHelpers, install: flagI, 'fn-installer': flagFnInstaller, // can be 'skip'!
20
26
  } = flags;
21
27
  let { language: flagFnLang } = flags;
22
28
  flagFnLang = flagJs ? 'js' : flagFnLang;
@@ -26,6 +32,59 @@ export async function blueprintAddCore(options) {
26
32
  error: `Unsupported Resource type: ${resourceType}`,
27
33
  };
28
34
  }
35
+ if (flagExample) {
36
+ // ! short circuit for examples
37
+ log(warn(`Example feature is experimental. Setting up "${flagExample}"...`)); // we need to...
38
+ // * 1. verify example exists in the recipes repo
39
+ const exampleExists = await verifyExampleExists({ type: 'function', name: flagExample });
40
+ if (!exampleExists) {
41
+ return { success: false, error: `Function example "${flagExample}" does not exist.` };
42
+ }
43
+ // * 2. download and write example to disk
44
+ // TODO: revisit path string handling; differs for fs operations vs. display
45
+ const exampleDir = join(dirname(blueprintFilePath), 'functions', flagExample);
46
+ if (existsSync(exampleDir)) {
47
+ return {
48
+ success: false,
49
+ error: `Directory "${exampleDir.replace(root, '')}" already exists.`,
50
+ };
51
+ }
52
+ const addedExample = await writeExample({
53
+ exampleType: 'function',
54
+ exampleName: flagExample,
55
+ dir: exampleDir,
56
+ });
57
+ if (!addedExample) {
58
+ return { success: false, error: `Unable to download example "${flagExample}"` };
59
+ }
60
+ const { files, dir, instructions, functionConfig } = addedExample;
61
+ const newDir = dir.replace(root, '').replace(/^[/\\]+/, '');
62
+ for (const filePath of Object.keys(files)) {
63
+ log(check(`${chalk.bold('Created:')} ${newDir}/${filePath}`));
64
+ }
65
+ // * 3. print instructions
66
+ if (functionConfig) {
67
+ log('');
68
+ log(chalk.bold(`Add the following to ${blueprint.fileInfo.fileName}:`));
69
+ const configString = JSON.stringify(functionConfig, null, 2);
70
+ if (blueprint.fileInfo.extension === '.json') {
71
+ log(indent(highlight(configString)));
72
+ }
73
+ else {
74
+ // modify configString so it doesn't have quoted keys
75
+ const objectLiteral = configString.replace(/^(\s*)"([a-zA-Z_$][a-zA-Z0-9_$]*)":/gm, '$1$2:');
76
+ log(indent(highlight(`defineDocumentFunction(${objectLiteral})`)));
77
+ }
78
+ }
79
+ else {
80
+ log(warn('No Function config found in example.'));
81
+ }
82
+ if (instructions) {
83
+ log('');
84
+ log(instructions);
85
+ }
86
+ return { success: true };
87
+ }
29
88
  if (flagI) {
30
89
  if (flagFnInstaller) {
31
90
  return {
@@ -90,13 +149,14 @@ export async function blueprintAddCore(options) {
90
149
  if (installCommand)
91
150
  log(`${chalk.magenta('Installing')} with ${installCommand}...`);
92
151
  const { filePath, resourceAdded, resource } = await createFunctionResource({
152
+ blueprintFilePath,
93
153
  name: fnName,
94
154
  type: fnType,
95
155
  lang: fnLang,
96
156
  addHelpers,
97
157
  installCommand,
98
158
  });
99
- log(`\nCreated function: ${filePath.replace(cwd(), '')}`);
159
+ log(`\nCreated function: ${filePath.replace(root, '')}`);
100
160
  if (!resourceAdded) {
101
161
  // print the resource definition for manual addition
102
162
  log(`\n${chalk.bold('Add the Resource to your Blueprint:')}`);
@@ -6,6 +6,7 @@ export interface BlueprintInitOptions extends CoreConfig {
6
6
  };
7
7
  flags: {
8
8
  dir?: string;
9
+ example?: string;
9
10
  'blueprint-type'?: string;
10
11
  'project-id'?: string;
11
12
  'stack-id'?: string;
@@ -1,9 +1,10 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { join } from 'node:path';
2
- import { cwd } from 'node:process';
3
3
  import chalk from 'chalk';
4
4
  import inquirer from 'inquirer';
5
5
  import { BLUEPRINT_CONFIG_FILE, BLUEPRINT_DIR, findBlueprintFile, writeBlueprintToDisk, writeConfigFile, writeGitignoreFile, writeOrUpdateNodeDependency, } from '../../actions/blueprints/blueprint.js';
6
6
  import { createEmptyStack, getStack } from '../../actions/blueprints/stacks.js';
7
+ import { verifyExampleExists, writeExample } from '../../actions/sanity/examples.js';
7
8
  import { getProject } from '../../actions/sanity/projects.js';
8
9
  import { check, warn } from '../../utils/display/presenters.js';
9
10
  import { promptForProject, promptForStackId } from '../../utils/display/prompt.js';
@@ -11,15 +12,66 @@ const LAUNCH_LIMIT_STACK_PER_PROJECT = true;
11
12
  export async function blueprintInitCore(options) {
12
13
  const { bin = 'sanity', log, token, args, flags } = options;
13
14
  try {
14
- const { 'blueprint-type': flagBlueprintType, 'project-id': flagProjectId, 'stack-id': flagStackId, 'stack-name': flagStackName, dir: flagDir, } = flags;
15
+ const { dir: flagDir, example: flagExample, 'blueprint-type': flagBlueprintType, 'project-id': flagProjectId, 'stack-id': flagStackId, 'stack-name': flagStackName, } = flags;
15
16
  const { dir: argDir } = args;
16
17
  const providedDir = argDir || flagDir;
17
- const here = cwd();
18
- const dir = argDir || flagDir || here;
18
+ const dir = providedDir || '.';
19
19
  const existingBlueprint = findBlueprintFile(dir);
20
20
  if (existingBlueprint) {
21
21
  return { success: false, error: 'Existing Blueprint found.' };
22
22
  }
23
+ if (flagExample) {
24
+ // ! short circuit for examples
25
+ log(warn(`Example feature is experimental. Setting up "${flagExample}"...`));
26
+ // we need to...
27
+ // * 1. verify example exists in the recipes repo
28
+ const exampleExists = await verifyExampleExists({ type: 'blueprint', name: flagExample });
29
+ if (!exampleExists) {
30
+ return { success: false, error: `Blueprint example "${flagExample}" does not exist.` };
31
+ }
32
+ // * 2. get a projectId from the user
33
+ const projectId = flagProjectId || (await promptForProject({ token })).projectId;
34
+ // * 3. create empty stack with name from example name
35
+ const stack = await createEmptyStack({
36
+ token,
37
+ projectId,
38
+ name: `example-${flagExample}`,
39
+ projectBased: false,
40
+ });
41
+ // * 4. download and write example to disk
42
+ // take into account optional providedDir
43
+ const exampleDir = join(dir, providedDir ? '' : flagExample);
44
+ if (existsSync(exampleDir)) {
45
+ return { success: false, error: `Example directory "${exampleDir}" already exists.` };
46
+ }
47
+ const addedExample = await writeExample({
48
+ exampleType: 'blueprint',
49
+ exampleName: flagExample,
50
+ dir: exampleDir,
51
+ });
52
+ if (!addedExample) {
53
+ return { success: false, error: `Unable to download example "${flagExample}"` };
54
+ }
55
+ const { files, dir: newDir, instructions } = addedExample;
56
+ for (const filePath of Object.keys(files)) {
57
+ log(check(`${chalk.bold('Created:')} ${newDir}/${filePath}`));
58
+ }
59
+ const discoveredBlueprint = findBlueprintFile(exampleDir);
60
+ if (!discoveredBlueprint) {
61
+ return { success: false, error: 'Failed to find blueprint file.' };
62
+ }
63
+ const { blueprintFilePath } = discoveredBlueprint;
64
+ // * 5. write config file
65
+ writeConfigFile({ blueprintFilePath, projectId, stackId: stack.id });
66
+ log(check(`${chalk.bold('Configured:')} ${exampleDir}/${BLUEPRINT_DIR}/${BLUEPRINT_CONFIG_FILE}`));
67
+ // * 6. print next step
68
+ log(`\n Run "${chalk.bold.magenta(`cd ${exampleDir} && npm i`)}" and check out the README`);
69
+ if (instructions) {
70
+ log('');
71
+ log(instructions);
72
+ }
73
+ return { success: true };
74
+ }
23
75
  const blueprintExtension = flagBlueprintType || (await promptForBlueprintType());
24
76
  if (!blueprintExtension) {
25
77
  return { success: false, error: 'Blueprint type is required.' };
@@ -77,12 +129,11 @@ export async function blueprintInitCore(options) {
77
129
  }
78
130
  const nextStepParts = [];
79
131
  if (providedDir)
80
- nextStepParts.push(`cd ./${providedDir}`);
132
+ nextStepParts.push(`cd ${providedDir}`);
81
133
  if (blueprintExtension !== 'json')
82
134
  nextStepParts.push('npm install');
83
135
  nextStepParts.push(`${bin} blueprints --help`);
84
- const nextStep = `Run ${chalk.bold.magenta(nextStepParts.join(' && '))}`;
85
- log(`\n ${nextStep} to get started`);
136
+ log(`\n Run "${chalk.bold.magenta(nextStepParts.join(' && '))}" to get started`);
86
137
  return { success: true };
87
138
  }
88
139
  catch (error) {
@@ -19,6 +19,6 @@ export async function blueprintPlanCore(options) {
19
19
  log(chalk.dim('No changes detected to live deployment'));
20
20
  }
21
21
  }
22
- log(chalk.dim(`Run "${bin} blueprints deploy" to deploy these changes`));
22
+ log(`\n Run "${chalk.bold.magenta(`${bin} blueprints deploy`)}" to deploy these changes`);
23
23
  return { success: true };
24
24
  }
@@ -3,3 +3,4 @@ export declare function info(str: string): string;
3
3
  export declare function warn(str: string): string;
4
4
  export declare function severe(str: string): string;
5
5
  export declare function niceId(id: string | undefined): string;
6
+ export declare function indent(str: string, spaces?: number): string;
@@ -16,3 +16,10 @@ export function niceId(id) {
16
16
  return '';
17
17
  return `<${chalk.yellow(id)}>`;
18
18
  }
19
+ export function indent(str, spaces = 2) {
20
+ const pad = ' '.repeat(spaces);
21
+ return str
22
+ .split('\n')
23
+ .map((line) => (line.length > 0 ? pad + line : line))
24
+ .join('\n');
25
+ }
@@ -0,0 +1,2 @@
1
+ export declare const GITHUB_API_URL = "https://api.github.com";
2
+ export declare function gitHubRequest(path: string): Promise<Response>;
@@ -0,0 +1,30 @@
1
+ // ! Making requests to the GitHub API will be rate limited at 60 requests per hour per IP address
2
+ // https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-unauthenticated-users
3
+ export const GITHUB_API_URL = 'https://api.github.com';
4
+ export async function gitHubRequest(path) {
5
+ const response = await fetch(`${GITHUB_API_URL}${path}`, {
6
+ headers: { Accept: 'application/vnd.github.v3+json' },
7
+ });
8
+ if (response.ok) {
9
+ const remaining = Number(response.headers.get('X-RateLimit-Remaining'));
10
+ const limit = Number(response.headers.get('X-RateLimit-Limit'));
11
+ const reset = Number(response.headers.get('X-RateLimit-Reset'));
12
+ if (remaining && limit && reset) {
13
+ const percentRemaining = (remaining / limit) * 100;
14
+ const percentUsed = 100 - percentRemaining;
15
+ if (percentUsed > 85) {
16
+ // warn if near rate limit
17
+ console.warn(`Warning: You have used ${percentUsed.toFixed(2)}% of your GitHub API requests.`);
18
+ const resetTime = new Date(reset * 1000);
19
+ console.log(`Reset in ${Math.floor((resetTime.getTime() - Date.now()) / 60000)} minutes`);
20
+ }
21
+ }
22
+ }
23
+ else {
24
+ // check for rate limit error
25
+ if (response.status === 403 || response.status === 429) {
26
+ console.error('GitHub API rate limit exceeded');
27
+ }
28
+ }
29
+ return response; // always return the response
30
+ }
@@ -4,7 +4,7 @@
4
4
  "aliases": [],
5
5
  "args": {
6
6
  "type": {
7
- "description": "Type of resource to add (e.g. function)",
7
+ "description": "Type of Resource to add (e.g. function)",
8
8
  "name": "type",
9
9
  "options": [
10
10
  "function"
@@ -21,6 +21,24 @@
21
21
  "<%= config.bin %> <%= command.id %> function --name my-function --fn-type document-publish --lang js"
22
22
  ],
23
23
  "flags": {
24
+ "example": {
25
+ "aliases": [
26
+ "recipe"
27
+ ],
28
+ "description": "Example to use for the Resource",
29
+ "exclusive": [
30
+ "name",
31
+ "fn-type",
32
+ "fn-language",
33
+ "javascript",
34
+ "fn-helpers",
35
+ "fn-installer"
36
+ ],
37
+ "name": "example",
38
+ "hasDynamicHelp": false,
39
+ "multiple": false,
40
+ "type": "option"
41
+ },
24
42
  "name": {
25
43
  "char": "n",
26
44
  "description": "Name of the Resource to add",
@@ -351,6 +369,21 @@
351
369
  "multiple": false,
352
370
  "type": "option"
353
371
  },
372
+ "example": {
373
+ "aliases": [
374
+ "recipe"
375
+ ],
376
+ "description": "Example to use for the Blueprint",
377
+ "exclusive": [
378
+ "blueprint-type",
379
+ "stack-id",
380
+ "stack-name"
381
+ ],
382
+ "name": "example",
383
+ "hasDynamicHelp": false,
384
+ "multiple": false,
385
+ "type": "option"
386
+ },
354
387
  "blueprint-type": {
355
388
  "aliases": [
356
389
  "type"
@@ -838,5 +871,5 @@
838
871
  ]
839
872
  }
840
873
  },
841
- "version": "8.0.3"
874
+ "version": "8.1.0"
842
875
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanity/runtime-cli",
3
3
  "description": "Sanity's Runtime CLI for Blueprints and Functions",
4
- "version": "8.0.3",
4
+ "version": "8.1.0",
5
5
  "author": "Sanity Runtime Team",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -70,7 +70,8 @@
70
70
  "pretest": "cd test/integration && npm install",
71
71
  "test": "vitest run",
72
72
  "posttest": "npm run lint",
73
- "test:watch": "vitest"
73
+ "test:watch": "vitest",
74
+ "watch": "tsc --watch"
74
75
  },
75
76
  "dependencies": {
76
77
  "@oclif/core": "^4.3.0",
@@ -86,6 +87,7 @@
86
87
  "jiti": "^2.4.2",
87
88
  "mime-types": "^3.0.1",
88
89
  "ora": "^8.2.0",
90
+ "tar-stream": "^3.1.7",
89
91
  "vite": "^6.3.5",
90
92
  "vite-tsconfig-paths": "^5.1.4",
91
93
  "ws": "^8.18.2",
@@ -105,6 +107,7 @@
105
107
  "@types/cardinal": "^2.1.1",
106
108
  "@types/mime-types": "^2.1.4",
107
109
  "@types/node": "20",
110
+ "@types/tar-stream": "^3.1.4",
108
111
  "@types/ws": "^8.18.1",
109
112
  "codemirror": "^6.0.1",
110
113
  "mentoss": "^0.11.0",