@sanity/runtime-cli 14.0.0 → 14.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 +21 -19
- package/dist/actions/blueprints/config.d.ts +3 -7
- package/dist/actions/blueprints/config.js +1 -1
- package/dist/actions/blueprints/logs-streaming.d.ts +1 -0
- package/dist/actions/blueprints/logs-streaming.js +1 -0
- package/dist/actions/blueprints/stacks.d.ts +51 -0
- package/dist/actions/blueprints/stacks.js +18 -0
- package/dist/commands/blueprints/deploy.js +3 -1
- package/dist/commands/blueprints/doctor.js +5 -1
- package/dist/cores/blueprints/config.js +10 -1
- package/dist/cores/blueprints/deploy.js +36 -3
- package/dist/cores/blueprints/destroy.js +15 -5
- package/dist/cores/blueprints/doctor.js +184 -90
- package/dist/cores/blueprints/plan.js +56 -21
- package/dist/utils/display/blueprints-formatting.d.ts +3 -1
- package/dist/utils/display/blueprints-formatting.js +137 -23
- package/dist/utils/other/npmjs.js +4 -1
- package/oclif.manifest.json +4 -5
- package/package.json +1 -1
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/14.
|
|
23
|
+
@sanity/runtime-cli/14.1.0 linux-x64 node-v24.13.1
|
|
24
24
|
$ sanity-run --help [COMMAND]
|
|
25
25
|
USAGE
|
|
26
26
|
$ sanity-run COMMAND
|
|
@@ -98,7 +98,7 @@ EXAMPLES
|
|
|
98
98
|
$ sanity-run blueprints add function --name my-function --fn-type document-create --fn-type document-update --lang js
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
_See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
101
|
+
_See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/add.ts)_
|
|
102
102
|
|
|
103
103
|
## `sanity-run blueprints config`
|
|
104
104
|
|
|
@@ -133,7 +133,7 @@ EXAMPLES
|
|
|
133
133
|
$ sanity-run blueprints config --edit --project-id <projectId> --stack <name-or-id>
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
_See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
136
|
+
_See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/config.ts)_
|
|
137
137
|
|
|
138
138
|
## `sanity-run blueprints deploy`
|
|
139
139
|
|
|
@@ -158,13 +158,15 @@ DESCRIPTION
|
|
|
158
158
|
|
|
159
159
|
Use --no-wait to queue the deployment and return immediately without waiting for completion.
|
|
160
160
|
|
|
161
|
+
Set SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.
|
|
162
|
+
|
|
161
163
|
EXAMPLES
|
|
162
164
|
$ sanity-run blueprints deploy
|
|
163
165
|
|
|
164
166
|
$ sanity-run blueprints deploy --no-wait
|
|
165
167
|
```
|
|
166
168
|
|
|
167
|
-
_See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
169
|
+
_See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/deploy.ts)_
|
|
168
170
|
|
|
169
171
|
## `sanity-run blueprints destroy`
|
|
170
172
|
|
|
@@ -196,7 +198,7 @@ EXAMPLES
|
|
|
196
198
|
$ sanity-run blueprints destroy --stack <name-or-id> --project-id <projectId> --force --no-wait
|
|
197
199
|
```
|
|
198
200
|
|
|
199
|
-
_See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
201
|
+
_See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/destroy.ts)_
|
|
200
202
|
|
|
201
203
|
## `sanity-run blueprints doctor`
|
|
202
204
|
|
|
@@ -210,7 +212,7 @@ FLAGS
|
|
|
210
212
|
-p, --path=<value> [env: SANITY_BLUEPRINT_PATH] Path to a Blueprint file or directory containing one
|
|
211
213
|
--fix Interactively fix configuration issues
|
|
212
214
|
--json Format output as json.
|
|
213
|
-
--verbose
|
|
215
|
+
--[no-]verbose Verbose output; defaults to true
|
|
214
216
|
|
|
215
217
|
DESCRIPTION
|
|
216
218
|
Diagnose potential issues with local Blueprint and remote Stack configuration
|
|
@@ -222,7 +224,7 @@ DESCRIPTION
|
|
|
222
224
|
issues.
|
|
223
225
|
```
|
|
224
226
|
|
|
225
|
-
_See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
227
|
+
_See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/doctor.ts)_
|
|
226
228
|
|
|
227
229
|
## `sanity-run blueprints info`
|
|
228
230
|
|
|
@@ -252,7 +254,7 @@ EXAMPLES
|
|
|
252
254
|
$ sanity-run blueprints info --stack <name-or-id>
|
|
253
255
|
```
|
|
254
256
|
|
|
255
|
-
_See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
257
|
+
_See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/info.ts)_
|
|
256
258
|
|
|
257
259
|
## `sanity-run blueprints init [DIR]`
|
|
258
260
|
|
|
@@ -302,7 +304,7 @@ EXAMPLES
|
|
|
302
304
|
$ sanity-run blueprints init --blueprint-type <json|js|ts> --stack-name <stackName>
|
|
303
305
|
```
|
|
304
306
|
|
|
305
|
-
_See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
307
|
+
_See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/init.ts)_
|
|
306
308
|
|
|
307
309
|
## `sanity-run blueprints logs`
|
|
308
310
|
|
|
@@ -331,7 +333,7 @@ EXAMPLES
|
|
|
331
333
|
$ sanity-run blueprints logs --watch
|
|
332
334
|
```
|
|
333
335
|
|
|
334
|
-
_See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
336
|
+
_See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/logs.ts)_
|
|
335
337
|
|
|
336
338
|
## `sanity-run blueprints plan`
|
|
337
339
|
|
|
@@ -357,7 +359,7 @@ EXAMPLES
|
|
|
357
359
|
$ sanity-run blueprints plan
|
|
358
360
|
```
|
|
359
361
|
|
|
360
|
-
_See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
362
|
+
_See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/plan.ts)_
|
|
361
363
|
|
|
362
364
|
## `sanity-run blueprints stacks`
|
|
363
365
|
|
|
@@ -386,7 +388,7 @@ EXAMPLES
|
|
|
386
388
|
$ sanity-run blueprints stacks --organization-id <organizationId>
|
|
387
389
|
```
|
|
388
390
|
|
|
389
|
-
_See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
391
|
+
_See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/blueprints/stacks.ts)_
|
|
390
392
|
|
|
391
393
|
## `sanity-run functions add`
|
|
392
394
|
|
|
@@ -435,7 +437,7 @@ EXAMPLES
|
|
|
435
437
|
$ sanity-run functions add --name my-function --type document-create --type document-update --lang js
|
|
436
438
|
```
|
|
437
439
|
|
|
438
|
-
_See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
440
|
+
_See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/functions/add.ts)_
|
|
439
441
|
|
|
440
442
|
## `sanity-run functions dev`
|
|
441
443
|
|
|
@@ -469,7 +471,7 @@ EXAMPLES
|
|
|
469
471
|
$ sanity-run functions dev --timeout 60
|
|
470
472
|
```
|
|
471
473
|
|
|
472
|
-
_See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
474
|
+
_See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/functions/dev.ts)_
|
|
473
475
|
|
|
474
476
|
## `sanity-run functions env add NAME KEY VALUE`
|
|
475
477
|
|
|
@@ -496,7 +498,7 @@ EXAMPLES
|
|
|
496
498
|
$ sanity-run functions env add MyFunction API_URL https://api.example.com/
|
|
497
499
|
```
|
|
498
500
|
|
|
499
|
-
_See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
501
|
+
_See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/functions/env/add.ts)_
|
|
500
502
|
|
|
501
503
|
## `sanity-run functions env list NAME`
|
|
502
504
|
|
|
@@ -520,7 +522,7 @@ EXAMPLES
|
|
|
520
522
|
$ sanity-run functions env list MyFunction
|
|
521
523
|
```
|
|
522
524
|
|
|
523
|
-
_See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
525
|
+
_See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/functions/env/list.ts)_
|
|
524
526
|
|
|
525
527
|
## `sanity-run functions env remove NAME KEY`
|
|
526
528
|
|
|
@@ -546,7 +548,7 @@ EXAMPLES
|
|
|
546
548
|
$ sanity-run functions env remove MyFunction API_URL
|
|
547
549
|
```
|
|
548
550
|
|
|
549
|
-
_See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
551
|
+
_See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/functions/env/remove.ts)_
|
|
550
552
|
|
|
551
553
|
## `sanity-run functions logs [NAME]`
|
|
552
554
|
|
|
@@ -586,7 +588,7 @@ EXAMPLES
|
|
|
586
588
|
$ sanity-run functions logs <name> --delete
|
|
587
589
|
```
|
|
588
590
|
|
|
589
|
-
_See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
591
|
+
_See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/functions/logs.ts)_
|
|
590
592
|
|
|
591
593
|
## `sanity-run functions test [NAME]`
|
|
592
594
|
|
|
@@ -640,7 +642,7 @@ EXAMPLES
|
|
|
640
642
|
$ sanity-run functions test <name> --event update --data-before '{ "title": "before" }' --data-after '{ "title": "after" }'
|
|
641
643
|
```
|
|
642
644
|
|
|
643
|
-
_See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v14.
|
|
645
|
+
_See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v14.1.0/src/commands/functions/test.ts)_
|
|
644
646
|
|
|
645
647
|
## `sanity-run help [COMMAND]`
|
|
646
648
|
|
|
@@ -16,7 +16,7 @@ export interface LocatedBlueprintsConfig extends BlueprintsConfig {
|
|
|
16
16
|
export declare function readConfigFile(blueprintFilePath?: string): LocatedBlueprintsConfig | null;
|
|
17
17
|
/**
|
|
18
18
|
* Create or update the config file to disk.
|
|
19
|
-
* One of organizationId or projectId must be provided.
|
|
19
|
+
* One of organizationId or projectId must be provided.
|
|
20
20
|
* Blueprint config version and updatedAt timestamp are set automatically.
|
|
21
21
|
* @param options - the options to write the config file
|
|
22
22
|
* @param options.blueprintFilePath - the path to the blueprint file
|
|
@@ -26,13 +26,9 @@ export declare function readConfigFile(blueprintFilePath?: string): LocatedBluep
|
|
|
26
26
|
*/
|
|
27
27
|
export declare function writeConfigFile(blueprintFilePath: string, options: {
|
|
28
28
|
stackId?: string;
|
|
29
|
-
} & ({
|
|
30
|
-
organizationId: string;
|
|
31
|
-
projectId?: string;
|
|
32
|
-
} | {
|
|
33
|
-
projectId: string;
|
|
34
29
|
organizationId?: string;
|
|
35
|
-
|
|
30
|
+
projectId?: string;
|
|
31
|
+
}): BlueprintsConfig;
|
|
36
32
|
/**
|
|
37
33
|
* Update the config file with the given properties.
|
|
38
34
|
* Config file must already exist.
|
|
@@ -20,7 +20,7 @@ export function readConfigFile(blueprintFilePath) {
|
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* Create or update the config file to disk.
|
|
23
|
-
* One of organizationId or projectId must be provided.
|
|
23
|
+
* One of organizationId or projectId must be provided.
|
|
24
24
|
* Blueprint config version and updatedAt timestamp are set automatically.
|
|
25
25
|
* @param options - the options to write the config file
|
|
26
26
|
* @param options.blueprintFilePath - the path to the blueprint file
|
|
@@ -79,6 +79,7 @@ export async function setupLogStreaming(config) {
|
|
|
79
79
|
if (!isNewerLog(logEntry, newestTimestamp))
|
|
80
80
|
return;
|
|
81
81
|
newestTimestamp = new Date(logEntry.timestamp).getTime();
|
|
82
|
+
config.onActivity?.();
|
|
82
83
|
log(formatLogEntry(logEntry, verbose, previousLog));
|
|
83
84
|
previousLog = logEntry;
|
|
84
85
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Blueprint } from '@sanity/blueprints-parser';
|
|
1
2
|
import type { Logger } from '../../utils/logger.js';
|
|
2
3
|
import type { AuthParams, ScopeType, Stack, StackMutation } from '../../utils/types.js';
|
|
3
4
|
export declare const stacksUrl: string;
|
|
@@ -50,6 +51,56 @@ export declare function updateStack({ stackId, stackMutation, auth, logger, }: {
|
|
|
50
51
|
auth: AuthParams;
|
|
51
52
|
logger: ReturnType<typeof Logger>;
|
|
52
53
|
}): Promise<UpdateStackResponse>;
|
|
54
|
+
interface PlanResourceSnapshot {
|
|
55
|
+
name: string;
|
|
56
|
+
type: string;
|
|
57
|
+
parameters: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
export interface PlanActionDetails {
|
|
60
|
+
name: string;
|
|
61
|
+
type: string;
|
|
62
|
+
parameters?: Record<string, unknown>;
|
|
63
|
+
existingResource?: PlanResourceSnapshot & {
|
|
64
|
+
id: string;
|
|
65
|
+
externalId?: string;
|
|
66
|
+
};
|
|
67
|
+
updatedResource?: PlanResourceSnapshot;
|
|
68
|
+
}
|
|
69
|
+
export interface DeploymentPlanAction {
|
|
70
|
+
type: 'create' | 'update' | 'destroy' | 'skip' | 'attach' | 'detach';
|
|
71
|
+
resourceType: string;
|
|
72
|
+
details: PlanActionDetails | null;
|
|
73
|
+
}
|
|
74
|
+
export interface DeploymentPlan {
|
|
75
|
+
plan: DeploymentPlanAction[];
|
|
76
|
+
summary: {
|
|
77
|
+
createCount: number;
|
|
78
|
+
updateCount: number;
|
|
79
|
+
destroyCount: number;
|
|
80
|
+
skipCount: number;
|
|
81
|
+
attachCount: number;
|
|
82
|
+
detachCount: number;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
type PlanStackResponse = {
|
|
86
|
+
ok: true;
|
|
87
|
+
error: null;
|
|
88
|
+
problems: null;
|
|
89
|
+
deploymentPlan: DeploymentPlan;
|
|
90
|
+
} | {
|
|
91
|
+
ok: false;
|
|
92
|
+
error: string;
|
|
93
|
+
problems: {
|
|
94
|
+
message: string;
|
|
95
|
+
}[] | null;
|
|
96
|
+
deploymentPlan: null;
|
|
97
|
+
};
|
|
98
|
+
export declare function planStack({ stackId, document, auth, logger, }: {
|
|
99
|
+
stackId: string;
|
|
100
|
+
document: Blueprint;
|
|
101
|
+
auth: AuthParams;
|
|
102
|
+
logger: ReturnType<typeof Logger>;
|
|
103
|
+
}): Promise<PlanStackResponse>;
|
|
53
104
|
interface DestroyStackResponse {
|
|
54
105
|
ok: boolean;
|
|
55
106
|
error: string | null;
|
|
@@ -75,6 +75,24 @@ export async function updateStack({ stackId, stackMutation, auth, logger, }) {
|
|
|
75
75
|
stack: data,
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
+
export async function planStack({ stackId, document, auth, logger, }) {
|
|
79
|
+
const fetchFn = createTracedFetch(logger);
|
|
80
|
+
const response = await fetchFn(`${stacksUrl}/${stackId}/plan`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: getHeaders(auth),
|
|
83
|
+
body: JSON.stringify({ document }),
|
|
84
|
+
});
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: data.message || 'Failed to retrieve deployment plan',
|
|
90
|
+
problems: response.status === 400 && Array.isArray(data.problems) ? data.problems : null,
|
|
91
|
+
deploymentPlan: null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, error: null, problems: null, deploymentPlan: data };
|
|
95
|
+
}
|
|
78
96
|
export async function resolveStackIdByNameOrId(value, auth, logger) {
|
|
79
97
|
if (value.startsWith('ST-') && value.length === 13)
|
|
80
98
|
return value;
|
|
@@ -8,7 +8,9 @@ export default class DeployCommand extends DeployedStackCommand {
|
|
|
8
8
|
|
|
9
9
|
Before deploying, run 'blueprints plan' to preview changes. After deployment, use 'blueprints info' to verify Stack status or 'blueprints logs' to monitor activity.
|
|
10
10
|
|
|
11
|
-
Use --no-wait to queue the deployment and return immediately without waiting for completion
|
|
11
|
+
Use --no-wait to queue the deployment and return immediately without waiting for completion.
|
|
12
|
+
|
|
13
|
+
Set SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.`;
|
|
12
14
|
static examples = [
|
|
13
15
|
'<%= config.bin %> <%= command.id %>',
|
|
14
16
|
'<%= config.bin %> <%= command.id %> --no-wait',
|
|
@@ -17,7 +17,11 @@ Run this command when encountering errors with other Blueprint commands. Use --f
|
|
|
17
17
|
default: false,
|
|
18
18
|
}),
|
|
19
19
|
json: unhide(baseFlags.json),
|
|
20
|
-
verbose:
|
|
20
|
+
verbose: Flags.boolean({
|
|
21
|
+
description: 'Verbose output; defaults to true',
|
|
22
|
+
default: true,
|
|
23
|
+
allowNo: true,
|
|
24
|
+
}),
|
|
21
25
|
};
|
|
22
26
|
async run() {
|
|
23
27
|
const { token } = config;
|
|
@@ -53,7 +53,16 @@ export async function blueprintConfigCore(options) {
|
|
|
53
53
|
return { success: true };
|
|
54
54
|
}
|
|
55
55
|
catch {
|
|
56
|
-
|
|
56
|
+
// fallback to writeConfigFile if patchConfigFile fails
|
|
57
|
+
// creates a new config file with the given properties
|
|
58
|
+
try {
|
|
59
|
+
const newConfig = writeConfigFile(blueprintFilePath, configUpdate);
|
|
60
|
+
printConfig({ configLabel: 'Updated', log, config: newConfig });
|
|
61
|
+
return { success: true };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { success: false, error: 'Unable to update configuration.' };
|
|
65
|
+
}
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
68
|
// prompt for values interactively
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { setTimeout } from 'node:timers/promises';
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
2
|
import { stashAsset } from '../../actions/blueprints/assets.js';
|
|
3
3
|
import { setupLogStreaming } from '../../actions/blueprints/logs-streaming.js';
|
|
4
4
|
import { getStack, updateStack } from '../../actions/blueprints/stacks.js';
|
|
5
5
|
import { niceId } from '../../utils/display/presenters.js';
|
|
6
6
|
import { styleText } from '../../utils/style-text.js';
|
|
7
7
|
import { isLocalFunctionCollection, isLocalFunctionResource } from '../../utils/types.js';
|
|
8
|
+
const DEFAULT_ASSET_TIMEOUT = 60;
|
|
9
|
+
const assetTimeoutS = Number(process.env.SANITY_ASSET_TIMEOUT) || DEFAULT_ASSET_TIMEOUT;
|
|
10
|
+
const assetTimeoutMs = assetTimeoutS * 1000;
|
|
11
|
+
const warnTimeoutMs = assetTimeoutMs / 2;
|
|
8
12
|
export async function blueprintDeployCore(options) {
|
|
9
13
|
const { bin = 'sanity', log, auth, stackId, scopeType, scopeId, deployedStack, blueprint, flags, } = options;
|
|
10
14
|
const { verbose } = flags;
|
|
@@ -33,7 +37,26 @@ export async function blueprintDeployCore(options) {
|
|
|
33
37
|
log('Processing function assets...');
|
|
34
38
|
for (const resource of allFunctionResources) {
|
|
35
39
|
const fnSpinner = log.ora({ text: `Processing ${resource.name}...`, prefixText: ' ' }).start();
|
|
36
|
-
const
|
|
40
|
+
const warnTimer = setTimeout(() => {
|
|
41
|
+
fnSpinner.text = `Still processing ${resource.name}, this can take a moment...`;
|
|
42
|
+
}, warnTimeoutMs);
|
|
43
|
+
let result;
|
|
44
|
+
try {
|
|
45
|
+
result = await Promise.race([
|
|
46
|
+
stashAsset({ resource, auth, logger: log }),
|
|
47
|
+
sleep(assetTimeoutMs).then(() => {
|
|
48
|
+
throw new Error(`Processing ${resource.name} timed out after ${assetTimeoutS}s`);
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54
|
+
fnSpinner.fail(msg);
|
|
55
|
+
return { success: false, error: msg };
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
clearTimeout(warnTimer);
|
|
59
|
+
}
|
|
37
60
|
if (result.success && result.assetId) {
|
|
38
61
|
resource.src = result.assetId;
|
|
39
62
|
if (isLocalFunctionCollection(resource)) {
|
|
@@ -102,12 +125,17 @@ export async function blueprintDeployCore(options) {
|
|
|
102
125
|
log(styleText('dim', 'Stack deployment progress:'));
|
|
103
126
|
let logStreamCleanup = null;
|
|
104
127
|
try {
|
|
128
|
+
let lastLogAt = Date.now();
|
|
129
|
+
let idleMessageShown = false;
|
|
105
130
|
logStreamCleanup = await setupLogStreaming({
|
|
106
131
|
stackId: stack.id,
|
|
107
132
|
after: isoNow,
|
|
108
133
|
auth,
|
|
109
134
|
log,
|
|
110
135
|
verbose,
|
|
136
|
+
onActivity: () => {
|
|
137
|
+
lastLogAt = Date.now();
|
|
138
|
+
},
|
|
111
139
|
});
|
|
112
140
|
while (true) {
|
|
113
141
|
const { ok, stack: currentStack } = await getStack({ stackId: stack.id, auth, logger: log });
|
|
@@ -133,7 +161,12 @@ export async function blueprintDeployCore(options) {
|
|
|
133
161
|
log(styleText(['bold', 'green'], 'Stack deployment completed!'));
|
|
134
162
|
return { success: true, data: { resources } };
|
|
135
163
|
}
|
|
136
|
-
|
|
164
|
+
if (!idleMessageShown && Date.now() - lastLogAt > 60_000) {
|
|
165
|
+
log(`No new activity for 60 seconds. The deployment is still running on Sanity servers.`);
|
|
166
|
+
log(`You can safely exit and check status later with \`${bin} blueprints info\`.`);
|
|
167
|
+
idleMessageShown = true;
|
|
168
|
+
}
|
|
169
|
+
await sleep(1500);
|
|
137
170
|
}
|
|
138
171
|
}
|
|
139
172
|
catch (error) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { setTimeout } from 'node:timers/promises';
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
2
|
import { confirm } from '@inquirer/prompts';
|
|
3
3
|
import { setupLogStreaming } from '../../actions/blueprints/logs-streaming.js';
|
|
4
4
|
import { destroyStack, getStack, resolveStackIdByNameOrId } from '../../actions/blueprints/stacks.js';
|
|
5
5
|
import { niceId } from '../../utils/display/presenters.js';
|
|
6
6
|
import { styleText } from '../../utils/style-text.js';
|
|
7
7
|
export async function blueprintDestroyCore(options) {
|
|
8
|
-
const { log, token, blueprint, flags } = options;
|
|
8
|
+
const { bin = 'sanity', log, token, blueprint, flags } = options;
|
|
9
9
|
const { force = false, 'project-id': flagProjectId, 'organization-id': flagOrganizationId, stack: flagStack, 'no-wait': noWait = false, verbose: _verbose = false, } = flags;
|
|
10
10
|
// 3-flag combo: just destroy it
|
|
11
11
|
if ((flagProjectId || flagOrganizationId) && flagStack && force) {
|
|
@@ -75,10 +75,10 @@ export async function blueprintDestroyCore(options) {
|
|
|
75
75
|
let i = 5;
|
|
76
76
|
while (i >= 0) {
|
|
77
77
|
destroySpinner.text = `Destroying Stack deployment in ${styleText('bold', (i--).toString())} seconds...`;
|
|
78
|
-
await
|
|
78
|
+
await sleep(1000);
|
|
79
79
|
}
|
|
80
80
|
destroySpinner.text = 'Destroying Stack deployment 💥';
|
|
81
|
-
await
|
|
81
|
+
await sleep(500);
|
|
82
82
|
}
|
|
83
83
|
else {
|
|
84
84
|
destroySpinner.start();
|
|
@@ -97,11 +97,16 @@ export async function blueprintDestroyCore(options) {
|
|
|
97
97
|
log(styleText('dim', 'Stack destruction progress:'));
|
|
98
98
|
let logStreamCleanup = null;
|
|
99
99
|
try {
|
|
100
|
+
let lastLogAt = Date.now();
|
|
101
|
+
let idleMessageShown = false;
|
|
100
102
|
logStreamCleanup = await setupLogStreaming({
|
|
101
103
|
stackId: stack.id,
|
|
102
104
|
after: isoNow,
|
|
103
105
|
auth,
|
|
104
106
|
log,
|
|
107
|
+
onActivity: () => {
|
|
108
|
+
lastLogAt = Date.now();
|
|
109
|
+
},
|
|
105
110
|
});
|
|
106
111
|
while (true) {
|
|
107
112
|
const { ok, stack: currentStack } = await getStack({ stackId: stack.id, auth, logger: log });
|
|
@@ -119,7 +124,12 @@ export async function blueprintDestroyCore(options) {
|
|
|
119
124
|
logStreamCleanup();
|
|
120
125
|
return { success: false, error: 'Stack destruction failed' };
|
|
121
126
|
}
|
|
122
|
-
|
|
127
|
+
if (!idleMessageShown && Date.now() - lastLogAt > 60_000) {
|
|
128
|
+
log(`No new activity for 60 seconds. The destruction is still running on Sanity servers.`);
|
|
129
|
+
log(`You can safely exit and check status later with \`${bin} blueprints info\`.`);
|
|
130
|
+
idleMessageShown = true;
|
|
131
|
+
}
|
|
132
|
+
await sleep(1500);
|
|
123
133
|
}
|
|
124
134
|
}
|
|
125
135
|
catch (error) {
|
|
@@ -1,202 +1,295 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { arch, cwd, version as nodeVersion, platform } from 'node:process';
|
|
3
|
+
import * as resolve from 'empathic/resolve';
|
|
4
|
+
import ora from 'ora';
|
|
2
5
|
import { readLocalBlueprint, } from '../../actions/blueprints/blueprint.js';
|
|
3
6
|
import { getStack } from '../../actions/blueprints/stacks.js';
|
|
4
|
-
import config from '../../config.js';
|
|
5
|
-
import {
|
|
7
|
+
import config, { RUNTIME_CLI_VERSION } from '../../config.js';
|
|
8
|
+
import { check, filePathRelativeToCwd, niceId, severe, unsure, } from '../../utils/display/presenters.js';
|
|
6
9
|
import { styleText } from '../../utils/style-text.js';
|
|
7
10
|
import { createTracedFetch } from '../../utils/traced-fetch.js';
|
|
8
11
|
import { validTokenOrErrorMessage } from '../../utils/validated-token.js';
|
|
9
12
|
import { blueprintConfigCore } from './config.js';
|
|
10
13
|
const diagLookup = {
|
|
11
|
-
online: '
|
|
14
|
+
online: 'Host online',
|
|
12
15
|
tokenValid: 'Authenticated',
|
|
13
16
|
blueprintValid: 'Blueprint valid',
|
|
14
17
|
stackReady: 'Stack ready',
|
|
15
18
|
userHasAccess: 'User has access',
|
|
16
19
|
};
|
|
20
|
+
function sourceLabel(source) {
|
|
21
|
+
switch (source) {
|
|
22
|
+
case 'env':
|
|
23
|
+
return 'environment';
|
|
24
|
+
case 'module':
|
|
25
|
+
return 'blueprint module';
|
|
26
|
+
case 'config':
|
|
27
|
+
return 'config file';
|
|
28
|
+
case 'inferred':
|
|
29
|
+
return 'inferred';
|
|
30
|
+
default:
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function renderSection(emit, title, rows) {
|
|
35
|
+
const pad = Math.max(...rows.map(([l]) => l.length)) + 3;
|
|
36
|
+
emit(styleText('bold', title));
|
|
37
|
+
for (const [label, value] of rows) {
|
|
38
|
+
emit(` ${styleText('dim', label.padEnd(pad))}${value}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
17
41
|
export async function blueprintDoctorCore(options) {
|
|
18
42
|
const { bin, log, token, validateResources, flags: { verbose: v, path: p, fix }, } = options;
|
|
19
|
-
const
|
|
20
|
-
log.error(styleText(['bgRedBright', 'whiteBright', 'bold'], ` ${s} `));
|
|
21
|
-
};
|
|
22
|
-
const here = cwd();
|
|
23
|
-
const path = p || here;
|
|
43
|
+
const path = p || cwd();
|
|
24
44
|
let tokenOrError;
|
|
25
|
-
log.verbose(`Checking ${filePathRelativeToCwd(path)}`);
|
|
26
|
-
// 3 states: null == unknown, true == good, false == bad
|
|
27
45
|
const diagnostics = {};
|
|
28
46
|
for (const key in diagLookup) {
|
|
29
|
-
diagnostics[key] = null;
|
|
47
|
+
diagnostics[key] = { status: null };
|
|
30
48
|
}
|
|
31
|
-
|
|
49
|
+
const envRows = [['Directory', p ? filePathRelativeToCwd(path) : path]];
|
|
50
|
+
const configRows = [];
|
|
51
|
+
const stackRows = [];
|
|
52
|
+
const spinner = ora('Running diagnostics...').start();
|
|
53
|
+
// --- ONLINE ---
|
|
32
54
|
const fetchFn = createTracedFetch(log);
|
|
33
55
|
try {
|
|
34
56
|
const res = await fetchFn(config.apiUrl);
|
|
35
57
|
if (res.ok) {
|
|
36
|
-
|
|
37
|
-
diagnostics.online = res.ok;
|
|
58
|
+
diagnostics.online = { status: true };
|
|
38
59
|
}
|
|
39
60
|
else {
|
|
40
|
-
|
|
41
|
-
diagnostics.online = false;
|
|
61
|
+
diagnostics.online = { status: false, detail: `${res.status} ${res.statusText}` };
|
|
42
62
|
}
|
|
43
63
|
}
|
|
44
|
-
catch {
|
|
45
|
-
|
|
64
|
+
catch (err) {
|
|
65
|
+
const reason = err instanceof Error ? err.message : 'unknown error';
|
|
66
|
+
diagnostics.online = { status: false, detail: reason };
|
|
46
67
|
}
|
|
47
|
-
// TOKEN
|
|
68
|
+
// --- TOKEN ---
|
|
48
69
|
if (token) {
|
|
49
70
|
tokenOrError = await validTokenOrErrorMessage(log, token);
|
|
50
71
|
if (tokenOrError.ok) {
|
|
51
|
-
diagnostics.tokenValid = true;
|
|
72
|
+
diagnostics.tokenValid = { status: true };
|
|
52
73
|
}
|
|
53
74
|
else {
|
|
54
|
-
|
|
55
|
-
diagnostics.tokenValid = false;
|
|
75
|
+
diagnostics.tokenValid = { status: false, detail: tokenOrError.error.message };
|
|
56
76
|
}
|
|
57
77
|
}
|
|
58
78
|
else {
|
|
59
|
-
|
|
60
|
-
diagnostics.tokenValid = false;
|
|
79
|
+
diagnostics.tokenValid = { status: false, detail: 'no token found' };
|
|
61
80
|
}
|
|
62
|
-
// BLUEPRINT
|
|
81
|
+
// --- BLUEPRINT ---
|
|
63
82
|
let localBlueprint;
|
|
64
83
|
try {
|
|
65
84
|
localBlueprint = await readLocalBlueprint(log, { resources: options.validateResources || false }, path);
|
|
66
|
-
|
|
85
|
+
envRows.push(['Blueprint', filePathRelativeToCwd(localBlueprint.fileInfo.blueprintFilePath)]);
|
|
67
86
|
if (localBlueprint.errors.length === 0) {
|
|
68
|
-
|
|
69
|
-
diagnostics.blueprintValid = true;
|
|
87
|
+
diagnostics.blueprintValid = { status: true };
|
|
70
88
|
}
|
|
71
89
|
else {
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
diagnostics.blueprintValid = {
|
|
91
|
+
status: false,
|
|
92
|
+
detail: `${localBlueprint.errors.length} error(s)`,
|
|
93
|
+
};
|
|
74
94
|
}
|
|
75
95
|
}
|
|
76
96
|
catch {
|
|
77
|
-
|
|
78
|
-
diagnostics.blueprintValid = false;
|
|
97
|
+
diagnostics.blueprintValid = { status: false, detail: 'unable to read file' };
|
|
79
98
|
}
|
|
99
|
+
envRows.push(['API URL', config.apiUrl]);
|
|
100
|
+
envRows.push(['Runtime', `Node.js ${nodeVersion} (${platform}-${arch})`]);
|
|
101
|
+
if (RUNTIME_CLI_VERSION) {
|
|
102
|
+
envRows.push(['Internals', `v${RUNTIME_CLI_VERSION}`]);
|
|
103
|
+
}
|
|
104
|
+
const sanityCliPkgPath = resolve.from(path, '@sanity/cli/package.json', true);
|
|
105
|
+
if (sanityCliPkgPath) {
|
|
106
|
+
try {
|
|
107
|
+
const sanityCliPkg = JSON.parse(readFileSync(sanityCliPkgPath, 'utf8'));
|
|
108
|
+
envRows.push(['Sanity CLI', sanityCliPkg.version]);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
}
|
|
112
|
+
// --- CONFIGURATION ---
|
|
80
113
|
if (localBlueprint) {
|
|
81
|
-
const { scopeType, scopeId, stackId, sources } = localBlueprint;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return 'inferred';
|
|
92
|
-
default:
|
|
93
|
-
return 'unknown';
|
|
94
|
-
}
|
|
95
|
-
};
|
|
114
|
+
const { scopeType, scopeId, stackId, sources, blueprintConfig } = localBlueprint;
|
|
115
|
+
if (blueprintConfig && 'configPath' in blueprintConfig && blueprintConfig.configPath) {
|
|
116
|
+
configRows.push(['Source', filePathRelativeToCwd(blueprintConfig.configPath)]);
|
|
117
|
+
}
|
|
118
|
+
else if (!blueprintConfig) {
|
|
119
|
+
configRows.push(['Source', styleText('dim', 'no config file')]);
|
|
120
|
+
}
|
|
121
|
+
if (blueprintConfig?.updatedAt) {
|
|
122
|
+
configRows.push(['Updated', new Date(blueprintConfig.updatedAt).toLocaleString('sv-SE')]);
|
|
123
|
+
}
|
|
96
124
|
if (scopeType && scopeId) {
|
|
97
125
|
const scopeSourceKey = scopeType === 'project' ? 'projectId' : 'organizationId';
|
|
98
|
-
|
|
126
|
+
const label = scopeType === 'project' ? 'Project' : 'Organization';
|
|
127
|
+
configRows.push([
|
|
128
|
+
label,
|
|
129
|
+
`${niceId(scopeId)} ${styleText('dim', sourceLabel(sources?.[scopeSourceKey]))}`,
|
|
130
|
+
]);
|
|
99
131
|
}
|
|
100
132
|
if (stackId) {
|
|
101
|
-
|
|
133
|
+
configRows.push([
|
|
134
|
+
'Stack',
|
|
135
|
+
`${niceId(stackId)} ${styleText('dim', sourceLabel(sources?.stackId))}`,
|
|
136
|
+
]);
|
|
102
137
|
}
|
|
103
|
-
// STACK + ACCESS
|
|
104
|
-
if (diagnostics.online
|
|
138
|
+
// --- STACK + ACCESS ---
|
|
139
|
+
if (diagnostics.online.status &&
|
|
140
|
+
diagnostics.tokenValid.status &&
|
|
141
|
+
token &&
|
|
142
|
+
stackId &&
|
|
143
|
+
scopeType &&
|
|
144
|
+
scopeId) {
|
|
105
145
|
const stackResponse = await getStack({
|
|
106
146
|
auth: { token, scopeType, scopeId },
|
|
107
147
|
stackId,
|
|
108
148
|
logger: log,
|
|
109
149
|
});
|
|
110
150
|
if (stackResponse.ok) {
|
|
111
|
-
|
|
112
|
-
diagnostics.
|
|
113
|
-
|
|
151
|
+
diagnostics.stackReady = { status: true };
|
|
152
|
+
diagnostics.userHasAccess = { status: true };
|
|
153
|
+
const stack = stackResponse.stack;
|
|
154
|
+
if (stack) {
|
|
155
|
+
const label = stack.name ? `"${stack.name}" ${niceId(stackId)}` : niceId(stackId);
|
|
156
|
+
stackRows.push(['Stack', label]);
|
|
157
|
+
if (stack.recentOperation) {
|
|
158
|
+
const op = stack.recentOperation;
|
|
159
|
+
const time = op.completedAt || op.createdAt;
|
|
160
|
+
const timestamp = time
|
|
161
|
+
? ` ${styleText('dim', new Date(time).toLocaleString('sv-SE'))}`
|
|
162
|
+
: '';
|
|
163
|
+
stackRows.push(['Operation', `${op.status}${timestamp}`]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
114
166
|
}
|
|
115
167
|
else if (stackResponse.response?.status === 404) {
|
|
116
|
-
|
|
117
|
-
|
|
168
|
+
diagnostics.stackReady = {
|
|
169
|
+
status: false,
|
|
170
|
+
detail: `Stack ${niceId(stackId)} not found (404)`,
|
|
171
|
+
};
|
|
118
172
|
}
|
|
119
173
|
else if (stackResponse.response?.status === 403 || stackResponse.response?.status === 401) {
|
|
120
|
-
|
|
121
|
-
|
|
174
|
+
diagnostics.userHasAccess = {
|
|
175
|
+
status: false,
|
|
176
|
+
detail: `no access to Stack ${niceId(stackId)} (${stackResponse.response.status})`,
|
|
177
|
+
};
|
|
122
178
|
}
|
|
123
179
|
else {
|
|
124
|
-
|
|
180
|
+
const statusSuffix = stackResponse.response?.status
|
|
181
|
+
? ` (${stackResponse.response.status})`
|
|
182
|
+
: '';
|
|
183
|
+
diagnostics.stackReady = {
|
|
184
|
+
status: null,
|
|
185
|
+
detail: (stackResponse.error || 'unknown error') + statusSuffix,
|
|
186
|
+
};
|
|
187
|
+
diagnostics.userHasAccess = { status: null, detail: `unknown error${statusSuffix}` };
|
|
125
188
|
}
|
|
126
189
|
}
|
|
190
|
+
else if (stackId && scopeType && scopeId) {
|
|
191
|
+
diagnostics.stackReady = { status: null, detail: 'requires online + authenticated' };
|
|
192
|
+
diagnostics.userHasAccess = { status: null, detail: 'requires online + authenticated' };
|
|
193
|
+
}
|
|
194
|
+
else if (!stackId && !scopeType && !scopeId) {
|
|
195
|
+
diagnostics.stackReady = { status: null, detail: 'missing configuration' };
|
|
196
|
+
diagnostics.userHasAccess = { status: null, detail: 'missing configuration' };
|
|
197
|
+
}
|
|
127
198
|
else {
|
|
199
|
+
const missing = [];
|
|
128
200
|
if (!stackId)
|
|
129
|
-
|
|
130
|
-
if (scopeType
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!scopeType)
|
|
140
|
-
yikes(`Blueprints configuration is missing a Scope Type`);
|
|
141
|
-
if (!scopeId)
|
|
142
|
-
yikes(`Blueprints configuration is missing a Scope ID`);
|
|
201
|
+
missing.push('Stack ID');
|
|
202
|
+
if (!scopeType)
|
|
203
|
+
missing.push('Scope Type');
|
|
204
|
+
if (!scopeId) {
|
|
205
|
+
const scopeLabel = scopeType === 'project'
|
|
206
|
+
? 'Project ID'
|
|
207
|
+
: scopeType === 'organization'
|
|
208
|
+
? 'Organization ID'
|
|
209
|
+
: 'Scope ID';
|
|
210
|
+
missing.push(scopeLabel);
|
|
143
211
|
}
|
|
212
|
+
const detail = `missing ${missing.join(', ')}`;
|
|
213
|
+
diagnostics.stackReady = { status: null, detail };
|
|
214
|
+
diagnostics.userHasAccess = { status: null, detail };
|
|
144
215
|
}
|
|
145
216
|
}
|
|
146
|
-
|
|
217
|
+
// --- RENDER REPORT ---
|
|
218
|
+
spinner.stop();
|
|
219
|
+
// Environment (verbose)
|
|
220
|
+
renderSection((msg) => log.verbose(msg), 'Environment', envRows);
|
|
221
|
+
log.verbose('');
|
|
222
|
+
// Configuration (verbose)
|
|
223
|
+
if (configRows.length > 0) {
|
|
224
|
+
renderSection((msg) => log.verbose(msg), 'Configuration', configRows);
|
|
225
|
+
log.verbose('');
|
|
226
|
+
}
|
|
227
|
+
// Deployment (verbose)
|
|
228
|
+
if (stackRows.length > 0) {
|
|
229
|
+
renderSection((msg) => log.verbose(msg), 'Deployment', stackRows);
|
|
230
|
+
log.verbose('');
|
|
231
|
+
}
|
|
232
|
+
// Checks (always visible)
|
|
233
|
+
const maxLabel = Math.max(...Object.values(diagLookup).map((l) => l.length));
|
|
234
|
+
log(styleText('bold', 'Checks'));
|
|
147
235
|
let allGood = true;
|
|
148
|
-
for (const [key,
|
|
149
|
-
|
|
236
|
+
for (const [key, entry] of Object.entries(diagnostics)) {
|
|
237
|
+
const label = diagLookup[key].padEnd(maxLabel);
|
|
238
|
+
const detail = entry.detail ? ` ${styleText('dim', entry.detail)}` : '';
|
|
239
|
+
switch (entry.status) {
|
|
150
240
|
case true:
|
|
151
|
-
log(check(
|
|
241
|
+
log(` ${check(label)}${detail}`);
|
|
152
242
|
break;
|
|
153
243
|
case false:
|
|
154
244
|
allGood = false;
|
|
155
|
-
log(severe(
|
|
245
|
+
log(` ${severe(label)}${detail}`);
|
|
156
246
|
break;
|
|
157
247
|
case null:
|
|
158
248
|
allGood = false;
|
|
159
|
-
log(unsure(
|
|
249
|
+
log(` ${unsure(label)}${detail}`);
|
|
160
250
|
break;
|
|
161
|
-
default:
|
|
162
|
-
allGood = false;
|
|
163
|
-
log(severe(`${key} is ${value}`));
|
|
164
251
|
}
|
|
165
252
|
}
|
|
253
|
+
// Result
|
|
254
|
+
const flatDiagnostics = {};
|
|
255
|
+
for (const [key, entry] of Object.entries(diagnostics)) {
|
|
256
|
+
flatDiagnostics[key] = entry.status;
|
|
257
|
+
}
|
|
258
|
+
log('');
|
|
166
259
|
const errorMessage = 'One or more checks failed';
|
|
167
260
|
if (allGood) {
|
|
168
261
|
log(styleText(['bold', 'green'], 'All checks passed'));
|
|
169
262
|
if (fix)
|
|
170
263
|
log(styleText(['bold', 'yellow'], 'Nothing to fix; --fix flag is ignored'));
|
|
171
|
-
return { success: true, data: { diagnostics } };
|
|
264
|
+
return { success: true, data: { diagnostics: flatDiagnostics } };
|
|
172
265
|
}
|
|
173
|
-
|
|
266
|
+
if (fix) {
|
|
174
267
|
if (p) {
|
|
175
268
|
return {
|
|
176
269
|
success: false,
|
|
177
270
|
error: `${errorMessage}. --fix cannot be used with --path`,
|
|
178
|
-
data: { diagnostics },
|
|
271
|
+
data: { diagnostics: flatDiagnostics },
|
|
179
272
|
};
|
|
180
273
|
}
|
|
181
274
|
if (!tokenOrError) {
|
|
182
275
|
return {
|
|
183
276
|
success: false,
|
|
184
277
|
error: `${errorMessage}. Unable to fix: Missing authentication token`,
|
|
185
|
-
data: { diagnostics },
|
|
278
|
+
data: { diagnostics: flatDiagnostics },
|
|
186
279
|
};
|
|
187
280
|
}
|
|
188
281
|
if (tokenOrError?.ok === false) {
|
|
189
282
|
return {
|
|
190
283
|
success: false,
|
|
191
284
|
error: `${errorMessage}. Unable to fix: ${tokenOrError.error.message}`,
|
|
192
|
-
data: { diagnostics },
|
|
285
|
+
data: { diagnostics: flatDiagnostics },
|
|
193
286
|
};
|
|
194
287
|
}
|
|
195
288
|
if (!localBlueprint) {
|
|
196
289
|
return {
|
|
197
290
|
success: false,
|
|
198
291
|
error: `${errorMessage}. Unable to fix: Blueprint is missing or invalid`,
|
|
199
|
-
data: { diagnostics },
|
|
292
|
+
data: { diagnostics: flatDiagnostics },
|
|
200
293
|
};
|
|
201
294
|
}
|
|
202
295
|
return blueprintConfigCore({
|
|
@@ -208,5 +301,6 @@ export async function blueprintDoctorCore(options) {
|
|
|
208
301
|
flags: { edit: true, verbose: v },
|
|
209
302
|
});
|
|
210
303
|
}
|
|
211
|
-
|
|
304
|
+
log(styleText('dim', ` Run \`${bin} blueprints doctor --fix\` to resolve configuration issues.`));
|
|
305
|
+
return { success: false, error: errorMessage, data: { diagnostics: flatDiagnostics } };
|
|
212
306
|
}
|
|
@@ -1,32 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { formatResourceTree,
|
|
1
|
+
import { planStack, resolveStackIdByNameOrId } from '../../actions/blueprints/stacks.js';
|
|
2
|
+
import { formatDeploymentPlan, formatResourceTree, hasActionableChanges, } from '../../utils/display/blueprints-formatting.js';
|
|
3
3
|
import { styleText } from '../../utils/style-text.js';
|
|
4
4
|
export async function blueprintPlanCore(options) {
|
|
5
5
|
const { bin = 'sanity', log, blueprint, token, flags } = options;
|
|
6
6
|
const { verbose: _verbose = false } = flags;
|
|
7
7
|
const { scopeType, scopeId, stackId: blueprintStackId, parsedBlueprint, fileInfo } = blueprint;
|
|
8
|
-
log(`${styleText(['bold', 'blueBright'], 'Blueprint
|
|
8
|
+
log(`${styleText(['bold', 'blueBright'], 'Local Blueprint')} ${styleText('dim', `(${fileInfo.fileName})`)}`);
|
|
9
9
|
log(formatResourceTree(parsedBlueprint.resources));
|
|
10
|
-
if (token
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
10
|
+
if (!token || !scopeType || !scopeId) {
|
|
11
|
+
log(styleText('dim', 'Unable to retrieve live Stack deployment for comparison'));
|
|
12
|
+
const errorMessage = !token
|
|
13
|
+
? `Missing authentication token. Run \`${bin} login\` to authenticate.`
|
|
14
|
+
: `Missing Blueprint configuration. Run \`${bin} blueprints doctor --fix\` to repair.`;
|
|
15
|
+
return {
|
|
16
|
+
success: false,
|
|
17
|
+
error: errorMessage,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const stackId = flags.stack
|
|
21
|
+
? await resolveStackIdByNameOrId(flags.stack, { token, scopeType, scopeId }, log)
|
|
22
|
+
: blueprintStackId;
|
|
23
|
+
if (!stackId) {
|
|
24
|
+
log(styleText('dim', 'Unable to retrieve live Stack deployment for comparison'));
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: `Missing Stack deployment configuration. Run \`${bin} blueprints doctor --fix\` to repair.`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const auth = { token, scopeType, scopeId };
|
|
31
|
+
const spinner = log.ora('Generating deployment plan...').start();
|
|
32
|
+
const planResponse = await planStack({
|
|
33
|
+
stackId,
|
|
34
|
+
document: parsedBlueprint,
|
|
35
|
+
auth,
|
|
36
|
+
logger: log,
|
|
37
|
+
});
|
|
38
|
+
spinner.stop().clear();
|
|
39
|
+
if (!planResponse.ok) {
|
|
40
|
+
if (planResponse.problems) {
|
|
41
|
+
log('');
|
|
42
|
+
for (const problem of planResponse.problems) {
|
|
43
|
+
const messages = problem.message
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((s) => s.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
for (const msg of messages) {
|
|
48
|
+
log(` ${styleText(['bold', 'red'], '✘')} ${msg}`);
|
|
49
|
+
}
|
|
27
50
|
}
|
|
51
|
+
log(`\n Fix the issues above before running "${styleText(['bold', 'magenta'], `${bin} blueprints deploy`)}"`);
|
|
28
52
|
}
|
|
53
|
+
else {
|
|
54
|
+
log(styleText('dim', '\nUnable to retrieve deployment plan from server'));
|
|
55
|
+
}
|
|
56
|
+
return { success: false, error: 'Deployment plan has problems' };
|
|
57
|
+
}
|
|
58
|
+
log('');
|
|
59
|
+
log(formatDeploymentPlan(planResponse.deploymentPlan));
|
|
60
|
+
if (hasActionableChanges(planResponse.deploymentPlan)) {
|
|
61
|
+
log(`\n Run "${styleText(['bold', 'magenta'], `${bin} blueprints deploy`)}" to apply these Stack changes`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
log(`\n ${styleText('dim', `No significant changes to deploy. Run \`${bin} blueprints deploy\` to apply.`)}`);
|
|
29
65
|
}
|
|
30
|
-
log(`\n Run "${styleText(['bold', 'magenta'], `${bin} blueprints deploy`)}" to deploy these Stack changes`);
|
|
31
66
|
return { success: true };
|
|
32
67
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Blueprint, Resource } from '@sanity/blueprints-parser';
|
|
2
|
+
import type { DeploymentPlan } from '../../actions/blueprints/stacks.js';
|
|
2
3
|
import { type BlueprintResourceRecord, type Stack } from '../types.js';
|
|
3
4
|
export declare function formatTitle(title: string, name: string): string;
|
|
4
5
|
export declare function formatDeployedResourceTree(resources: BlueprintResourceRecord[] | undefined, verbose?: boolean): string;
|
|
5
6
|
export declare function formatResourceTree(resources: Resource[] | undefined, verbose?: boolean): string;
|
|
6
7
|
export declare function formatStackInfo(stack: Stack | Blueprint, isCurrentStack?: boolean): string;
|
|
7
8
|
export declare function formatStacksListing(stacks: Stack[], currentStackId?: string): string;
|
|
8
|
-
export declare function
|
|
9
|
+
export declare function hasActionableChanges(deploymentPlan: DeploymentPlan): boolean;
|
|
10
|
+
export declare function formatDeploymentPlan(deploymentPlan: DeploymentPlan): string;
|
|
@@ -208,29 +208,143 @@ export function formatStacksListing(stacks, currentStackId) {
|
|
|
208
208
|
}
|
|
209
209
|
return output.join('\n');
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
211
|
+
const IGNORED_PARAMS = {
|
|
212
|
+
[SANITY_FUNCTION_DOCUMENT]: new Set(['src']),
|
|
213
|
+
[SANITY_FUNCTION_MEDIA_LIBRARY_ASSET]: new Set(['src']),
|
|
214
|
+
[SANITY_FUNCTION_SCHEDULE]: new Set(['src']),
|
|
215
|
+
};
|
|
216
|
+
const ASSET_RESOURCE_TYPES = new Set([
|
|
217
|
+
SANITY_FUNCTION_DOCUMENT,
|
|
218
|
+
SANITY_FUNCTION_MEDIA_LIBRARY_ASSET,
|
|
219
|
+
SANITY_FUNCTION_SCHEDULE,
|
|
220
|
+
]);
|
|
221
|
+
function stringifyUnknown(val) {
|
|
222
|
+
if (val === null || val === undefined)
|
|
223
|
+
return JSON.stringify(val);
|
|
224
|
+
if (Array.isArray(val))
|
|
225
|
+
return `[${val.map(stringifyUnknown).join(',')}]`;
|
|
226
|
+
if (typeof val === 'object') {
|
|
227
|
+
const obj = val;
|
|
228
|
+
const sorted = Object.keys(obj)
|
|
229
|
+
.sort()
|
|
230
|
+
.map((k) => `${JSON.stringify(k)}:${stringifyUnknown(obj[k])}`);
|
|
231
|
+
return `{${sorted.join(',')}}`;
|
|
231
232
|
}
|
|
232
|
-
|
|
233
|
-
|
|
233
|
+
return JSON.stringify(val);
|
|
234
|
+
}
|
|
235
|
+
function detectChangedParams(details) {
|
|
236
|
+
const existing = details.existingResource?.parameters;
|
|
237
|
+
const updated = details.updatedResource?.parameters;
|
|
238
|
+
if (!existing || !updated)
|
|
239
|
+
return [];
|
|
240
|
+
const ignored = IGNORED_PARAMS[details.type];
|
|
241
|
+
const keys = new Set([...Object.keys(existing), ...Object.keys(updated)]);
|
|
242
|
+
const changed = [];
|
|
243
|
+
for (const key of keys) {
|
|
244
|
+
if (ignored?.has(key))
|
|
245
|
+
continue;
|
|
246
|
+
if (stringifyUnknown(existing[key]) !== stringifyUnknown(updated[key])) {
|
|
247
|
+
changed.push(key);
|
|
248
|
+
}
|
|
234
249
|
}
|
|
235
|
-
return
|
|
250
|
+
return changed;
|
|
251
|
+
}
|
|
252
|
+
function getAssetSrc(details) {
|
|
253
|
+
const src = details.updatedResource?.parameters?.src ?? details.parameters?.src;
|
|
254
|
+
return typeof src === 'string' && !src.startsWith('AS-') ? src : undefined;
|
|
255
|
+
}
|
|
256
|
+
function actionStyle(type, isUnchanged = false) {
|
|
257
|
+
if (isUnchanged)
|
|
258
|
+
return { icon: '=', color: 'dim' };
|
|
259
|
+
switch (type) {
|
|
260
|
+
case 'create':
|
|
261
|
+
return { icon: '+', color: 'green' };
|
|
262
|
+
case 'update':
|
|
263
|
+
return { icon: '~', color: 'yellow' };
|
|
264
|
+
case 'destroy':
|
|
265
|
+
return { icon: '-', color: 'red' };
|
|
266
|
+
case 'attach':
|
|
267
|
+
return { icon: '←', color: 'blue' };
|
|
268
|
+
case 'detach':
|
|
269
|
+
return { icon: '→', color: 'gray' };
|
|
270
|
+
// case 'skip':
|
|
271
|
+
default:
|
|
272
|
+
return { icon: '=', color: 'dim' };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export function hasActionableChanges(deploymentPlan) {
|
|
276
|
+
const { summary } = deploymentPlan;
|
|
277
|
+
if (summary.createCount || summary.destroyCount || summary.attachCount || summary.detachCount) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
return deploymentPlan.plan.some((action) => action.type === 'update' &&
|
|
281
|
+
action.details &&
|
|
282
|
+
(detectChangedParams(action.details).length > 0 ||
|
|
283
|
+
(ASSET_RESOURCE_TYPES.has(action.resourceType) &&
|
|
284
|
+
getAssetSrc(action.details) !== undefined)));
|
|
285
|
+
}
|
|
286
|
+
export function formatDeploymentPlan(deploymentPlan) {
|
|
287
|
+
const lines = [];
|
|
288
|
+
lines.push(styleText('bold', 'Deployment Plan'));
|
|
289
|
+
if (deploymentPlan.plan.length === 0) {
|
|
290
|
+
lines.push(styleText('dim', ' No changes'));
|
|
291
|
+
return lines.join('\n');
|
|
292
|
+
}
|
|
293
|
+
const typePad = Math.max(...deploymentPlan.plan.map((a) => a.type.length));
|
|
294
|
+
const names = deploymentPlan.plan.map((a) => a.details?.name ?? '');
|
|
295
|
+
const namePad = Math.max(...names.map((n) => n.length));
|
|
296
|
+
const enrichedPlan = deploymentPlan.plan.map((action) => {
|
|
297
|
+
const changedParams = action.type === 'update' && action.details ? detectChangedParams(action.details) : [];
|
|
298
|
+
// always represent asset updates as a change; hard to know if the asset has changed
|
|
299
|
+
const assetSrc = action.details && ASSET_RESOURCE_TYPES.has(action.resourceType)
|
|
300
|
+
? getAssetSrc(action.details)
|
|
301
|
+
: undefined;
|
|
302
|
+
const isUnchanged = action.type === 'update' && changedParams.length === 0 && !assetSrc;
|
|
303
|
+
return { action, changedParams, isUnchanged };
|
|
304
|
+
});
|
|
305
|
+
let unchangedCount = 0;
|
|
306
|
+
for (const { action, changedParams, isUnchanged } of enrichedPlan) {
|
|
307
|
+
const name = action.details?.name ?? '';
|
|
308
|
+
if (isUnchanged)
|
|
309
|
+
unchangedCount++;
|
|
310
|
+
const style = actionStyle(action.type, isUnchanged);
|
|
311
|
+
const icon = styleText(['bold', style.color], style.icon);
|
|
312
|
+
const typeLabel = styleText(style.color, action.type.padEnd(typePad));
|
|
313
|
+
const nameLabel = isUnchanged
|
|
314
|
+
? styleText('dim', name.padEnd(namePad))
|
|
315
|
+
: styleText('bold', name.padEnd(namePad));
|
|
316
|
+
const resourceType = styleText('dim', action.resourceType);
|
|
317
|
+
let suffix = '';
|
|
318
|
+
if (changedParams.length > 0) {
|
|
319
|
+
suffix = ` ${styleText('yellow', changedParams.join(', '))}`;
|
|
320
|
+
}
|
|
321
|
+
lines.push(` ${icon} ${typeLabel} ${nameLabel} ${resourceType}${suffix}`);
|
|
322
|
+
}
|
|
323
|
+
const { summary } = deploymentPlan;
|
|
324
|
+
const parts = [];
|
|
325
|
+
if (summary.createCount)
|
|
326
|
+
parts.push(styleText('green', `${summary.createCount} create`));
|
|
327
|
+
if (summary.updateCount) {
|
|
328
|
+
let label = `${summary.updateCount} update`;
|
|
329
|
+
if (unchangedCount > 0 && unchangedCount < summary.updateCount) {
|
|
330
|
+
label += ` (${styleText('yellow', `${summary.updateCount - unchangedCount} changed`)})`;
|
|
331
|
+
}
|
|
332
|
+
else if (unchangedCount === summary.updateCount) {
|
|
333
|
+
label += ` ${styleText('dim', '(no changes)')}`;
|
|
334
|
+
}
|
|
335
|
+
parts.push(label);
|
|
336
|
+
}
|
|
337
|
+
if (summary.destroyCount)
|
|
338
|
+
parts.push(styleText('red', `${summary.destroyCount} destroy`));
|
|
339
|
+
if (summary.skipCount)
|
|
340
|
+
parts.push(styleText('dim', `${summary.skipCount} skip`));
|
|
341
|
+
if (summary.attachCount)
|
|
342
|
+
parts.push(styleText('blue', `${summary.attachCount} attach`));
|
|
343
|
+
if (summary.detachCount)
|
|
344
|
+
parts.push(styleText('dim', `${summary.detachCount} detach`));
|
|
345
|
+
if (parts.length > 0) {
|
|
346
|
+
lines.push('');
|
|
347
|
+
lines.push(` ${parts.join(', ')}`);
|
|
348
|
+
}
|
|
349
|
+
return lines.join('\n');
|
|
236
350
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { env } from 'node:process';
|
|
1
2
|
import { createTracedFetch } from '../traced-fetch.js';
|
|
2
3
|
export async function getLatestNpmVersion(pkg, logger) {
|
|
3
4
|
const fetchFn = createTracedFetch(logger);
|
|
4
|
-
|
|
5
|
+
// allow for registry override; helpful for testing
|
|
6
|
+
const registry = env.SANITY_INTERNAL_NPM_REGISTRY_URL || 'https://registry.npmjs.org';
|
|
7
|
+
const url = `${registry}/${pkg}/latest`;
|
|
5
8
|
try {
|
|
6
9
|
const res = await fetchFn(url);
|
|
7
10
|
if (!res.ok)
|
package/oclif.manifest.json
CHANGED
|
@@ -318,7 +318,7 @@
|
|
|
318
318
|
"blueprints:deploy": {
|
|
319
319
|
"aliases": [],
|
|
320
320
|
"args": {},
|
|
321
|
-
"description": "Pushes your local Blueprint configuration to the remote Stack; provisioning, updating, or destroying resources as needed. This is the primary command for applying infrastructure changes.\n\nBefore deploying, run 'blueprints plan' to preview changes. After deployment, use 'blueprints info' to verify Stack status or 'blueprints logs' to monitor activity.\n\nUse --no-wait to queue the deployment and return immediately without waiting for completion.",
|
|
321
|
+
"description": "Pushes your local Blueprint configuration to the remote Stack; provisioning, updating, or destroying resources as needed. This is the primary command for applying infrastructure changes.\n\nBefore deploying, run 'blueprints plan' to preview changes. After deployment, use 'blueprints info' to verify Stack status or 'blueprints logs' to monitor activity.\n\nUse --no-wait to queue the deployment and return immediately without waiting for completion.\n\nSet SANITY_ASSET_TIMEOUT (seconds) to override the 60-second timeout for processing resource assets.",
|
|
322
322
|
"examples": [
|
|
323
323
|
"<%= config.bin %> <%= command.id %>",
|
|
324
324
|
"<%= config.bin %> <%= command.id %> --no-wait"
|
|
@@ -578,10 +578,9 @@
|
|
|
578
578
|
"type": "boolean"
|
|
579
579
|
},
|
|
580
580
|
"verbose": {
|
|
581
|
-
"description": "Verbose output",
|
|
582
|
-
"hidden": false,
|
|
581
|
+
"description": "Verbose output; defaults to true",
|
|
583
582
|
"name": "verbose",
|
|
584
|
-
"allowNo":
|
|
583
|
+
"allowNo": true,
|
|
585
584
|
"type": "boolean"
|
|
586
585
|
},
|
|
587
586
|
"fix": {
|
|
@@ -2084,5 +2083,5 @@
|
|
|
2084
2083
|
]
|
|
2085
2084
|
}
|
|
2086
2085
|
},
|
|
2087
|
-
"version": "14.
|
|
2086
|
+
"version": "14.1.0"
|
|
2088
2087
|
}
|