@sanity/runtime-cli 14.13.3 → 15.0.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 +215 -118
- package/dist/actions/blueprints/blueprint.js +13 -11
- package/dist/actions/blueprints/logs.d.ts +2 -1
- package/dist/actions/blueprints/logs.js +4 -5
- package/dist/actions/blueprints/resources.js +1 -0
- package/dist/actions/blueprints/stacks.d.ts +3 -1
- package/dist/actions/blueprints/stacks.js +11 -2
- package/dist/actions/functions/test.js +1 -1
- package/dist/actions/sanity/access.d.ts +38 -0
- package/dist/actions/sanity/access.js +23 -0
- package/dist/actions/sanity/projects.d.ts +1 -1
- package/dist/baseCommands.d.ts +2 -0
- package/dist/baseCommands.js +8 -5
- package/dist/commands/blueprints/add.js +1 -1
- package/dist/commands/blueprints/deploy.d.ts +2 -0
- package/dist/commands/blueprints/deploy.js +6 -2
- package/dist/commands/blueprints/destroy.js +0 -2
- package/dist/commands/blueprints/info.d.ts +1 -0
- package/dist/commands/blueprints/info.js +3 -1
- package/dist/commands/blueprints/init.js +2 -0
- package/dist/commands/blueprints/logs.d.ts +5 -0
- package/dist/commands/blueprints/logs.js +26 -3
- package/dist/commands/blueprints/mint-deploy-token.d.ts +14 -0
- package/dist/commands/blueprints/mint-deploy-token.js +47 -0
- package/dist/commands/blueprints/plan.d.ts +2 -0
- package/dist/commands/blueprints/plan.js +8 -2
- package/dist/commands/blueprints/promote.d.ts +2 -1
- package/dist/commands/blueprints/promote.js +7 -2
- package/dist/commands/blueprints/stacks.js +1 -1
- package/dist/commands/functions/add.js +1 -1
- package/dist/cores/blueprints/config.js +34 -34
- package/dist/cores/blueprints/doctor.js +7 -7
- package/dist/cores/blueprints/init.js +99 -76
- package/dist/cores/blueprints/logs.d.ts +3 -0
- package/dist/cores/blueprints/logs.js +15 -9
- package/dist/cores/blueprints/mint-deploy-token.d.ts +15 -0
- package/dist/cores/blueprints/mint-deploy-token.js +111 -0
- package/dist/cores/blueprints/promote.d.ts +1 -0
- package/dist/cores/blueprints/promote.js +25 -4
- package/dist/cores/functions/add.js +4 -5
- package/dist/cores/index.d.ts +1 -2
- package/dist/cores/index.js +1 -3
- package/dist/index.d.ts +0 -18
- package/dist/index.js +0 -20
- package/dist/utils/clipboard.d.ts +14 -0
- package/dist/utils/clipboard.js +73 -0
- package/dist/utils/display/errors.d.ts +5 -1
- package/dist/utils/display/prompt.d.ts +55 -15
- package/dist/utils/display/prompt.js +271 -45
- package/oclif.manifest.json +320 -18
- package/package.json +21 -67
- package/dist/actions/blueprints/index.d.ts +0 -16
- package/dist/actions/blueprints/index.js +0 -10
- package/dist/actions/functions/index.d.ts +0 -4
- package/dist/actions/functions/index.js +0 -4
- package/dist/actions/sanity/index.d.ts +0 -1
- package/dist/actions/sanity/index.js +0 -1
- package/dist/cores/blueprints/index.d.ts +0 -20
- package/dist/cores/blueprints/index.js +0 -10
- package/dist/cores/functions/index.d.ts +0 -16
- package/dist/cores/functions/index.js +0 -8
- package/dist/utils/display/index.d.ts +0 -5
- package/dist/utils/display/index.js +0 -5
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/index.js +0 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { ResolvedCommand } from '../../baseCommands.js';
|
|
2
|
+
import { projectIdFlagConfig, ResolvedCommand } from '../../baseCommands.js';
|
|
3
3
|
import { blueprintPromoteCore } from '../../cores/blueprints/promote.js';
|
|
4
4
|
import { Logger } from '../../utils/logger.js';
|
|
5
5
|
export default class PromoteCommand extends ResolvedCommand {
|
|
@@ -8,20 +8,25 @@ export default class PromoteCommand extends ResolvedCommand {
|
|
|
8
8
|
static description = `Promotes a deployed Stack to organization scope, enabling management of org-level resources. Promotion cannot be reversed.
|
|
9
9
|
|
|
10
10
|
Your local Blueprint configuration will be updated to reflect the new scope.`;
|
|
11
|
-
static hidden = true;
|
|
12
11
|
static examples = [
|
|
13
12
|
'<%= config.bin %> <%= command.id %>',
|
|
14
13
|
'<%= config.bin %> <%= command.id %> --stack <name-or-id>',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --project-id <projectId> --stack <name-or-id>',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --new-stack-name <new-name>',
|
|
15
16
|
];
|
|
16
17
|
static flags = {
|
|
17
18
|
stack: Flags.string({
|
|
18
19
|
description: 'Stack name or ID to promote',
|
|
19
20
|
aliases: ['id'],
|
|
20
21
|
}),
|
|
22
|
+
'project-id': Flags.string({ ...projectIdFlagConfig }),
|
|
21
23
|
force: Flags.boolean({
|
|
22
24
|
description: 'Skip confirmation prompt',
|
|
23
25
|
default: false,
|
|
24
26
|
}),
|
|
27
|
+
'new-stack-name': Flags.string({
|
|
28
|
+
description: 'Set a new name for the Stack while promoting',
|
|
29
|
+
}),
|
|
25
30
|
};
|
|
26
31
|
async run() {
|
|
27
32
|
const result = await blueprintPromoteCore({
|
|
@@ -21,7 +21,7 @@ Use --include-projects with --organization-id to also list Stacks from all proje
|
|
|
21
21
|
...projectIdFlagConfig,
|
|
22
22
|
exclusive: ['organization-id', 'include-projects'],
|
|
23
23
|
}),
|
|
24
|
-
'organization-id': Flags.string({ ...organizationIdFlagConfig
|
|
24
|
+
'organization-id': Flags.string({ ...organizationIdFlagConfig }),
|
|
25
25
|
'include-projects': Flags.boolean({
|
|
26
26
|
description: 'Include Stacks from all projects within the organization. Requires --organization-id.',
|
|
27
27
|
dependsOn: ['organization-id'],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { ResolvedCommand } from '../../baseCommands.js';
|
|
3
3
|
import { FUNCTION_TYPES } from '../../constants.js';
|
|
4
|
-
import { functionAddCore } from '../../cores/functions/
|
|
4
|
+
import { functionAddCore } from '../../cores/functions/add.js';
|
|
5
5
|
import { Logger } from '../../utils/logger.js';
|
|
6
6
|
import { INSTALLER_OPTIONS } from '../../utils/types.js';
|
|
7
7
|
export default class AddCommand extends ResolvedCommand {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { patchConfigFile, writeConfigFile, } from '../../actions/blueprints/config.js';
|
|
2
2
|
import { resolveStackIdByNameOrId } from '../../actions/blueprints/stacks.js';
|
|
3
3
|
import { filePathRelativeToCwd, labeledId, warn } from '../../utils/display/presenters.js';
|
|
4
|
-
import {
|
|
4
|
+
import { runScopeAndStackWizard } from '../../utils/display/prompt.js';
|
|
5
5
|
import { styleText } from '../../utils/style-text.js';
|
|
6
6
|
export async function blueprintConfigCore(options) {
|
|
7
7
|
const { bin = 'sanity', blueprint, log, token, flags } = options;
|
|
@@ -12,10 +12,10 @@ export async function blueprintConfigCore(options) {
|
|
|
12
12
|
const hasConfigFile = !!blueprintConfig;
|
|
13
13
|
if (!configStackId && !configScopeType && !configScopeId) {
|
|
14
14
|
if (hasConfigFile) {
|
|
15
|
-
log(warn('
|
|
15
|
+
log(warn('Blueprint config (.sanity/blueprint.config.json) is incomplete.'));
|
|
16
16
|
}
|
|
17
17
|
else {
|
|
18
|
-
log('No
|
|
18
|
+
log('No Blueprint config found at .sanity/blueprint.config.json.');
|
|
19
19
|
}
|
|
20
20
|
if (!editConfig) {
|
|
21
21
|
log(`Run \`npx ${bin} blueprints doctor\` for diagnostics.`);
|
|
@@ -26,7 +26,7 @@ export async function blueprintConfigCore(options) {
|
|
|
26
26
|
printConfig({ configLabel: 'Current', log, config: blueprintConfig });
|
|
27
27
|
// passing new config without --edit flag is not allowed
|
|
28
28
|
if (providedConfigFlag && !editConfig) {
|
|
29
|
-
log('To update the
|
|
29
|
+
log('To update the Blueprint config, use the --edit flag.');
|
|
30
30
|
return { success: true, json: blueprintConfig ? { config: blueprintConfig } : undefined };
|
|
31
31
|
}
|
|
32
32
|
if (!editConfig) {
|
|
@@ -70,44 +70,44 @@ export async function blueprintConfigCore(options) {
|
|
|
70
70
|
return { success: true, json: { config: newConfig } };
|
|
71
71
|
}
|
|
72
72
|
catch {
|
|
73
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
error: 'Could not update Blueprint config (.sanity/blueprint.config.json).',
|
|
76
|
+
};
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
|
-
// prompt for values interactively
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return { success: false, error: 'Project ID is required.' };
|
|
90
|
-
let updatedStackId;
|
|
91
|
-
if (flagStack) {
|
|
92
|
-
updatedStackId = await resolveStackIdByNameOrId(flagStack, { token, scopeType: 'project', scopeId: updatedProjectId }, log);
|
|
93
|
-
}
|
|
94
|
-
if (!updatedStackId) {
|
|
95
|
-
const pickedStack = await promptForStack({ projectId: updatedProjectId, token, logger: log });
|
|
96
|
-
updatedStackId = pickedStack.stackId;
|
|
97
|
-
}
|
|
98
|
-
if (!updatedStackId)
|
|
99
|
-
return { success: false, error: 'Stack is required.' };
|
|
80
|
+
// prompt for values interactively via wizard: org -> scope -> project (if needed) -> stack
|
|
81
|
+
log('');
|
|
82
|
+
const knownOrganizationId = configScopeType === 'organization' ? configScopeId : undefined;
|
|
83
|
+
const knownProjectId = configScopeType === 'project' ? configScopeId : undefined;
|
|
84
|
+
const wizardResult = await runScopeAndStackWizard({
|
|
85
|
+
token,
|
|
86
|
+
knownOrganizationId,
|
|
87
|
+
knownProjectId,
|
|
88
|
+
logger: log,
|
|
89
|
+
});
|
|
90
|
+
if (!wizardResult.scopeId)
|
|
91
|
+
return { success: false, error: 'Scope selection is required.' };
|
|
100
92
|
try {
|
|
101
|
-
// update or create config JSON
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
93
|
+
// update or create config JSON with correct scope fields
|
|
94
|
+
const configUpdate = { stackId: wizardResult.stackId };
|
|
95
|
+
if (wizardResult.scopeType === 'organization') {
|
|
96
|
+
configUpdate.organizationId = wizardResult.scopeId;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
configUpdate.projectId = wizardResult.scopeId;
|
|
100
|
+
}
|
|
101
|
+
const newConfig = writeConfigFile(blueprintFilePath, configUpdate);
|
|
102
|
+
log('');
|
|
106
103
|
printConfig({ configLabel: hasConfigFile ? 'Updated' : 'New', log, config: newConfig });
|
|
107
104
|
return { success: true, json: { config: newConfig } };
|
|
108
105
|
}
|
|
109
106
|
catch {
|
|
110
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: 'Could not update Blueprint config (.sanity/blueprint.config.json).',
|
|
110
|
+
};
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -15,7 +15,7 @@ import { blueprintConfigCore } from './config.js';
|
|
|
15
15
|
const diagLookup = {
|
|
16
16
|
online: 'Host online',
|
|
17
17
|
tokenValid: 'Authenticated',
|
|
18
|
-
blueprintValid: '
|
|
18
|
+
blueprintValid: 'Manifest valid',
|
|
19
19
|
stackReady: 'Stack ready',
|
|
20
20
|
userHasAccess: 'User has access',
|
|
21
21
|
};
|
|
@@ -24,9 +24,9 @@ function sourceLabel(source) {
|
|
|
24
24
|
case 'env':
|
|
25
25
|
return 'environment';
|
|
26
26
|
case 'module':
|
|
27
|
-
return '
|
|
27
|
+
return 'manifest module';
|
|
28
28
|
case 'config':
|
|
29
|
-
return 'config
|
|
29
|
+
return '.sanity/blueprint.config.json';
|
|
30
30
|
case 'inferred':
|
|
31
31
|
return 'inferred';
|
|
32
32
|
default:
|
|
@@ -91,7 +91,7 @@ export async function blueprintDoctorCore(options) {
|
|
|
91
91
|
blueprintConfig,
|
|
92
92
|
...resolved,
|
|
93
93
|
};
|
|
94
|
-
envRows.push(['
|
|
94
|
+
envRows.push(['Manifest', filePathRelativeToCwd(blueprint.fileInfo.blueprintFilePath)]);
|
|
95
95
|
if (blueprint.errors.length === 0) {
|
|
96
96
|
diagnostics.blueprintValid = { status: true };
|
|
97
97
|
}
|
|
@@ -129,7 +129,7 @@ export async function blueprintDoctorCore(options) {
|
|
|
129
129
|
configRows.push(['Source', filePathRelativeToCwd(blueprintConfig.configPath)]);
|
|
130
130
|
}
|
|
131
131
|
else if (!blueprintConfig) {
|
|
132
|
-
configRows.push(['Source', styleText('dim', 'no config
|
|
132
|
+
configRows.push(['Source', styleText('dim', 'no .sanity/blueprint.config.json')]);
|
|
133
133
|
}
|
|
134
134
|
if (blueprintConfig?.updatedAt) {
|
|
135
135
|
configRows.push(['Updated', new Date(blueprintConfig.updatedAt).toLocaleString('sv-SE')]);
|
|
@@ -315,8 +315,8 @@ export async function blueprintDoctorCore(options) {
|
|
|
315
315
|
if (!localBlueprint) {
|
|
316
316
|
return {
|
|
317
317
|
success: false,
|
|
318
|
-
error: `${errorMessage}. Unable to fix: Blueprint is missing or invalid`,
|
|
319
|
-
suggestions: [`Run \`npx ${bin} blueprints init\` to create a new Blueprint.`],
|
|
318
|
+
error: `${errorMessage}. Unable to fix: Blueprint manifest is missing or invalid`,
|
|
319
|
+
suggestions: [`Run \`npx ${bin} blueprints init\` to create a new Blueprint manifest.`],
|
|
320
320
|
data: { diagnostics: flatDiagnostics },
|
|
321
321
|
};
|
|
322
322
|
}
|
|
@@ -3,18 +3,24 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { confirm } from '@inquirer/prompts';
|
|
4
4
|
import { findBlueprintFile, readLocalBlueprint, writeBlueprintToDisk, } from '../../actions/blueprints/blueprint.js';
|
|
5
5
|
import { writeConfigFile } from '../../actions/blueprints/config.js';
|
|
6
|
-
import { createEmptyStack } from '../../actions/blueprints/stacks.js';
|
|
6
|
+
import { createEmptyStack, getStack } from '../../actions/blueprints/stacks.js';
|
|
7
7
|
import { writeGitignoreFile } from '../../actions/git.js';
|
|
8
8
|
import { writeOrUpdateNodeDependency } from '../../actions/node.js';
|
|
9
9
|
import { verifyExampleExists, writeExample } from '../../actions/sanity/examples.js';
|
|
10
10
|
import { getProject } from '../../actions/sanity/projects.js';
|
|
11
11
|
import { BLUEPRINT_CONFIG_DIR, BLUEPRINT_CONFIG_FILE } from '../../config.js';
|
|
12
12
|
import { check, filePathRelativeToCwd, labeledId, warn } from '../../utils/display/presenters.js';
|
|
13
|
-
import { promptForBlueprintType,
|
|
13
|
+
import { promptForBlueprintType, promptForOrganization, promptForProjectInOrg, promptForStack, runScopeAndStackWizard, } from '../../utils/display/prompt.js';
|
|
14
14
|
import { styleText } from '../../utils/style-text.js';
|
|
15
15
|
import { blueprintConfigCore } from './config.js';
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/** Derive scope type and ID from mutually exclusive flag values. */
|
|
17
|
+
function resolveScopeFromFlags(projectId, organizationId) {
|
|
18
|
+
if (projectId)
|
|
19
|
+
return { scopeType: 'project', scopeId: projectId };
|
|
20
|
+
if (organizationId)
|
|
21
|
+
return { scopeType: 'organization', scopeId: organizationId };
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
18
24
|
export async function blueprintInitCore(options) {
|
|
19
25
|
const { bin = 'sanity', log, token, knownProjectId, args, flags, validateResources } = options;
|
|
20
26
|
const { dir: flagDir, example: flagExample, 'blueprint-type': flagBlueprintType, 'project-id': flagProjectId, 'organization-id': flagOrganizationId, 'stack-id': flagStackId, 'stack-name': flagStackName, verbose = false, } = flags;
|
|
@@ -87,23 +93,15 @@ async function handleExistingBlueprint(options) {
|
|
|
87
93
|
error: 'Flag --example cannot be used with an existing Blueprint.',
|
|
88
94
|
};
|
|
89
95
|
}
|
|
90
|
-
//
|
|
91
|
-
// --stack-name means they want a new stack created
|
|
96
|
+
// --stack-name with a scope flag means create a new stack before configuring
|
|
92
97
|
let resolvedStackId = flagStackId;
|
|
93
98
|
if (!resolvedStackId && flagStackName) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
scopeType = SCOPE_PROJECT;
|
|
98
|
-
else if (flagOrganizationId)
|
|
99
|
-
scopeType = SCOPE_ORGANIZATION;
|
|
100
|
-
if (scopeType && scopeId) {
|
|
101
|
-
// have what we need to create a new stack
|
|
102
|
-
log(`\nCreating new Stack "${flagStackName}" scoped to ${labeledId(scopeType, scopeId)}`);
|
|
99
|
+
const scope = resolveScopeFromFlags(flagProjectId, flagOrganizationId);
|
|
100
|
+
if (scope) {
|
|
101
|
+
log(`\nCreating new Stack "${flagStackName}" scoped to ${labeledId(scope.scopeType, scope.scopeId)}`);
|
|
103
102
|
const stack = await createEmptyStack({
|
|
104
103
|
token,
|
|
105
|
-
|
|
106
|
-
scopeId,
|
|
104
|
+
...scope,
|
|
107
105
|
name: flagStackName,
|
|
108
106
|
logger: log,
|
|
109
107
|
});
|
|
@@ -168,8 +166,11 @@ export function validateFlags(flags) {
|
|
|
168
166
|
if (stackId && stackName) {
|
|
169
167
|
return { success: false, error: 'Cannot specify both --stack-id and --stack-name' };
|
|
170
168
|
}
|
|
171
|
-
if (organizationId && projectId) {
|
|
172
|
-
return {
|
|
169
|
+
if ((stackId || stackName) && !organizationId && !projectId) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: `${stackId ? '--stack-id' : '--stack-name'} requires --project-id or --organization-id`,
|
|
173
|
+
};
|
|
173
174
|
}
|
|
174
175
|
return null;
|
|
175
176
|
}
|
|
@@ -184,10 +185,20 @@ async function handleExampleInitialization(options) {
|
|
|
184
185
|
if (!exampleExists) {
|
|
185
186
|
return { success: false, error: `Blueprint example "${exampleName}" does not exist.` };
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
+
let resolvedProjectId = projectId;
|
|
189
|
+
if (!resolvedProjectId) {
|
|
190
|
+
// examples are project-scoped; use the composable prompts to pick an org then a project
|
|
191
|
+
const orgResult = await promptForOrganization({ token, logger: log });
|
|
192
|
+
if (typeof orgResult === 'symbol')
|
|
193
|
+
throw new Error('Unexpected back navigation');
|
|
194
|
+
const picked = await promptForProjectInOrg({ projects: orgResult.projects, logger: log });
|
|
195
|
+
if (typeof picked === 'symbol')
|
|
196
|
+
throw new Error('Unexpected back navigation');
|
|
197
|
+
resolvedProjectId = picked.projectId;
|
|
198
|
+
}
|
|
188
199
|
const stack = await createEmptyStack({
|
|
189
200
|
token,
|
|
190
|
-
scopeType:
|
|
201
|
+
scopeType: 'project',
|
|
191
202
|
scopeId: resolvedProjectId,
|
|
192
203
|
name: `example-${exampleName}`,
|
|
193
204
|
logger: log,
|
|
@@ -225,69 +236,81 @@ async function handleExampleInitialization(options) {
|
|
|
225
236
|
}
|
|
226
237
|
export async function resolveScopeAndStack(params) {
|
|
227
238
|
const { projectId, organizationId, stackId, stackName, knownProjectId, log, token } = params;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
scopeType =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
const flagScope = resolveScopeFromFlags(projectId, organizationId);
|
|
240
|
+
// --- Branch 1: Scope provided via flags ---
|
|
241
|
+
if (flagScope) {
|
|
242
|
+
const { scopeType, scopeId } = flagScope;
|
|
243
|
+
let resolvedStackId = stackId;
|
|
244
|
+
// --stack-name: create a new stack
|
|
245
|
+
if (!resolvedStackId && stackName) {
|
|
246
|
+
log(`\nCreating new Stack "${stackName}" scoped to ${labeledId(scopeType, scopeId)}`);
|
|
247
|
+
const stack = await createEmptyStack({
|
|
248
|
+
token,
|
|
249
|
+
scopeType,
|
|
250
|
+
scopeId,
|
|
251
|
+
name: stackName,
|
|
252
|
+
logger: log,
|
|
253
|
+
});
|
|
254
|
+
resolvedStackId = stack.id;
|
|
255
|
+
}
|
|
256
|
+
// --stack-id: validate it exists
|
|
257
|
+
if (resolvedStackId && resolvedStackId === stackId) {
|
|
258
|
+
const spinner = log.ora('Validating Stack...').start();
|
|
259
|
+
const { ok, error } = await getStack({
|
|
260
|
+
stackId: resolvedStackId,
|
|
261
|
+
auth: { token, scopeType, scopeId },
|
|
262
|
+
logger: log,
|
|
263
|
+
});
|
|
264
|
+
spinner.stop();
|
|
265
|
+
if (!ok) {
|
|
266
|
+
throw new Error(error || `Stack "${resolvedStackId}" not found for ${scopeType} "${scopeId}".`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// No stack flag: prompt for stack selection
|
|
270
|
+
if (!resolvedStackId) {
|
|
271
|
+
log('\nBlueprints are deployed to a "Stack".');
|
|
272
|
+
const stackResult = await promptForStack({ scopeType, scopeId, token, logger: log });
|
|
273
|
+
if (typeof stackResult === 'symbol')
|
|
274
|
+
throw new Error('Unexpected back navigation');
|
|
275
|
+
resolvedStackId = stackResult.stackId;
|
|
276
|
+
}
|
|
277
|
+
return { scopeType, scopeId, stackId: resolvedStackId };
|
|
237
278
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
279
|
+
// --- Branch 2: No scope flags -- interactive resolution ---
|
|
280
|
+
log('\nBlueprints are scoped to an organization or project.');
|
|
281
|
+
log(styleText('dim', 'Scope determines which resources your Blueprint can manage.\n' +
|
|
282
|
+
'Organization scope covers all projects; project scope is limited to one.'));
|
|
283
|
+
// Offer the CLI's configured project as a shortcut
|
|
284
|
+
if (knownProjectId) {
|
|
285
|
+
const { ok, project } = await getProject({
|
|
244
286
|
token,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
name: stackName,
|
|
287
|
+
scopeId: knownProjectId,
|
|
288
|
+
scopeType: 'project',
|
|
248
289
|
logger: log,
|
|
249
290
|
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// If we have a CLI project ID, offer it as a suggestion
|
|
255
|
-
if (knownProjectId) {
|
|
256
|
-
const { ok, project } = await getProject({
|
|
257
|
-
token,
|
|
258
|
-
scopeId: knownProjectId,
|
|
259
|
-
scopeType: 'project',
|
|
260
|
-
logger: log,
|
|
291
|
+
if (ok && project) {
|
|
292
|
+
const useCliProject = await confirm({
|
|
293
|
+
message: `The CLI is configured to use "${project.displayName}" (${knownProjectId}). Use this for the blueprint?`,
|
|
294
|
+
default: true,
|
|
261
295
|
});
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
296
|
+
if (useCliProject) {
|
|
297
|
+
// Scope locked to project; just need a stack
|
|
298
|
+
log('\nBlueprints are deployed to a "Stack".');
|
|
299
|
+
const stackResult = await promptForStack({
|
|
300
|
+
scopeType: 'project',
|
|
301
|
+
scopeId: knownProjectId,
|
|
302
|
+
token,
|
|
303
|
+
logger: log,
|
|
266
304
|
});
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
305
|
+
if (typeof stackResult === 'symbol')
|
|
306
|
+
throw new Error('Unexpected back navigation');
|
|
307
|
+
return { scopeType: 'project', scopeId: knownProjectId, stackId: stackResult.stackId };
|
|
271
308
|
}
|
|
272
309
|
}
|
|
273
|
-
// If still no scope (no knownProjectId, lookup failed, or user declined), prompt for selection
|
|
274
|
-
if (!scopeId) {
|
|
275
|
-
log('Select a project:');
|
|
276
|
-
const pickedProject = await promptForProject({ token, logger: log });
|
|
277
|
-
scopeType = SCOPE_PROJECT;
|
|
278
|
-
scopeId = pickedProject.projectId;
|
|
279
|
-
}
|
|
280
310
|
}
|
|
281
|
-
if
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
resolvedStackId = stackId;
|
|
285
|
-
}
|
|
286
|
-
return {
|
|
287
|
-
scopeType,
|
|
288
|
-
scopeId,
|
|
289
|
-
stackId: resolvedStackId,
|
|
290
|
-
};
|
|
311
|
+
// Full wizard: org -> scope -> project (if needed) -> stack
|
|
312
|
+
const wizard = await runScopeAndStackWizard({ token, logger: log });
|
|
313
|
+
return { scopeType: wizard.scopeType, scopeId: wizard.scopeId, stackId: wizard.stackId };
|
|
291
314
|
}
|
|
292
315
|
export async function determineBlueprintExtension(params) {
|
|
293
316
|
const { requestedType, blueprintDir, log } = params;
|
|
@@ -329,7 +352,7 @@ export async function createBlueprintFiles(params) {
|
|
|
329
352
|
log(check(`${styleText('bold', 'Created Blueprint:')} ${displayPath}/${blueprintFileName}`));
|
|
330
353
|
writeConfigFile(blueprintFilePath, {
|
|
331
354
|
stackId,
|
|
332
|
-
...(scopeType ===
|
|
355
|
+
...(scopeType === 'organization' ? { organizationId: scopeId } : { projectId: scopeId }),
|
|
333
356
|
});
|
|
334
357
|
log(check(`${styleText('bold', 'Added configuration:')} ${displayPath}/${BLUEPRINT_CONFIG_DIR}/${BLUEPRINT_CONFIG_FILE}`));
|
|
335
358
|
const gitignoreResult = writeGitignoreFile(blueprintFilePath);
|
|
@@ -7,6 +7,9 @@ export interface BlueprintLogsOptions extends CoreConfig {
|
|
|
7
7
|
flags: {
|
|
8
8
|
watch?: boolean;
|
|
9
9
|
verbose?: boolean;
|
|
10
|
+
limit?: number;
|
|
11
|
+
since?: string;
|
|
12
|
+
before?: string;
|
|
10
13
|
};
|
|
11
14
|
}
|
|
12
15
|
export declare function blueprintLogsCore(options: BlueprintLogsOptions): Promise<CoreResult>;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { getLogs
|
|
1
|
+
import { getLogs } from '../../actions/blueprints/logs.js';
|
|
2
2
|
import { setupLogPolling } from '../../actions/blueprints/logs-polling.js';
|
|
3
3
|
import { formatTitle } from '../../utils/display/blueprints-formatting.js';
|
|
4
4
|
import { formatLogEntry, formatLogs } from '../../utils/display/logs-formatting.js';
|
|
5
5
|
import { niceId } from '../../utils/display/presenters.js';
|
|
6
6
|
import { styleText } from '../../utils/style-text.js';
|
|
7
|
+
/** Number of recent logs to show before entering watch mode */
|
|
8
|
+
const WATCH_PREFETCH_LIMIT = 10;
|
|
7
9
|
export async function blueprintLogsCore(options) {
|
|
8
10
|
const { log, auth, stackId, deployedStack, flags } = options;
|
|
9
|
-
const { watch = false, verbose = false } = flags;
|
|
11
|
+
const { watch = false, verbose = false, limit, since, before } = flags;
|
|
10
12
|
const spinner = log.ora(`Fetching recent logs for Stack deployment ${niceId(stackId)}`).start();
|
|
11
13
|
try {
|
|
12
14
|
if (watch) {
|
|
13
|
-
const { ok, logs, error } = await getLogs({ stackId }, auth, log);
|
|
15
|
+
const { ok, logs, error } = await getLogs({ stackId, limit: WATCH_PREFETCH_LIMIT }, auth, log);
|
|
14
16
|
if (!ok) {
|
|
15
17
|
spinner.fail(`${styleText('red', 'Failed')} to retrieve logs`);
|
|
16
18
|
log.error(`Error: ${error || 'Unknown error'}`);
|
|
@@ -20,9 +22,10 @@ export async function blueprintLogsCore(options) {
|
|
|
20
22
|
log(`${formatTitle('Blueprint', deployedStack.name)} ${niceId(stackId)} logs`);
|
|
21
23
|
if (logs.length > 0) {
|
|
22
24
|
log('\nMost recent logs:');
|
|
23
|
-
|
|
25
|
+
// API returns newest first; display in chronological order
|
|
26
|
+
const orderedLogs = [...logs].reverse();
|
|
24
27
|
let previousLog;
|
|
25
|
-
for (const logEntry of
|
|
28
|
+
for (const logEntry of orderedLogs) {
|
|
26
29
|
log(formatLogEntry(logEntry, verbose, previousLog));
|
|
27
30
|
previousLog = logEntry;
|
|
28
31
|
}
|
|
@@ -39,14 +42,14 @@ export async function blueprintLogsCore(options) {
|
|
|
39
42
|
showBanner: true,
|
|
40
43
|
verbose,
|
|
41
44
|
});
|
|
42
|
-
// Return a
|
|
45
|
+
// Return a never-resolving promise so the polling loop keeps running
|
|
43
46
|
return {
|
|
44
47
|
success: true,
|
|
45
48
|
streaming: new Promise(() => { }),
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
|
-
//
|
|
49
|
-
const { ok, logs, error } = await getLogs({ stackId }, auth, log);
|
|
51
|
+
// One-shot fetch (no watch)
|
|
52
|
+
const { ok, logs, error, hasMore } = await getLogs({ stackId, limit, before, after: since }, auth, log);
|
|
50
53
|
if (!ok) {
|
|
51
54
|
spinner.fail(`${styleText('red', 'Failed')} to retrieve Stack deployment logs`);
|
|
52
55
|
log.error(`Error: ${error || 'Unknown error'}`);
|
|
@@ -59,7 +62,10 @@ export async function blueprintLogsCore(options) {
|
|
|
59
62
|
spinner.succeed(`${formatTitle('Blueprint', deployedStack.name)} Logs`);
|
|
60
63
|
log(`Found ${styleText('bold', logs.length.toString())} log entries for Stack deployment ${niceId(stackId)}\n`);
|
|
61
64
|
log(formatLogs(logs, verbose));
|
|
62
|
-
|
|
65
|
+
if (hasMore) {
|
|
66
|
+
log(`\n${styleText('dim', 'More logs available. Narrow the range with --before <timestamp> or raise --limit (max 500).')}`);
|
|
67
|
+
}
|
|
68
|
+
return { success: true, json: { logs, hasMore } };
|
|
63
69
|
}
|
|
64
70
|
catch (err) {
|
|
65
71
|
spinner.fail('Failed to retrieve Stack deployment logs');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuthParams, ScopeType } from '../../utils/types.js';
|
|
2
|
+
import type { CoreConfig, CoreResult } from '../index.js';
|
|
3
|
+
export interface BlueprintMintDeployTokenOptions extends CoreConfig {
|
|
4
|
+
auth: AuthParams;
|
|
5
|
+
scopeType: ScopeType;
|
|
6
|
+
scopeId: string;
|
|
7
|
+
flags: {
|
|
8
|
+
label?: string;
|
|
9
|
+
print?: boolean;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
verbose?: boolean;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function defaultLabel(scopeType: ScopeType): string;
|
|
15
|
+
export declare function blueprintMintDeployTokenCore(options: BlueprintMintDeployTokenOptions): Promise<CoreResult>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
3
|
+
import { createRobotToken } from '../../actions/sanity/access.js';
|
|
4
|
+
import { ClipboardUnavailableError, write as clipboardWrite } from '../../utils/clipboard.js';
|
|
5
|
+
import { styleText } from '../../utils/style-text.js';
|
|
6
|
+
const ROLE_BY_SCOPE = {
|
|
7
|
+
project: 'blueprints-deployer',
|
|
8
|
+
organization: 'blueprints-deployer-robot',
|
|
9
|
+
};
|
|
10
|
+
export function defaultLabel(scopeType) {
|
|
11
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
12
|
+
let host = 'unknown-host';
|
|
13
|
+
try {
|
|
14
|
+
host = hostname();
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
return `blueprints-deployer @ ${host} ${date} (${scopeType})`;
|
|
18
|
+
}
|
|
19
|
+
export async function blueprintMintDeployTokenCore(options) {
|
|
20
|
+
const { log, auth, scopeType, scopeId, flags } = options;
|
|
21
|
+
const label = flags.label ?? defaultLabel(scopeType);
|
|
22
|
+
const roleName = ROLE_BY_SCOPE[scopeType];
|
|
23
|
+
const spinner = log.ora('Minting deploy token...').start();
|
|
24
|
+
const { ok, error, robot } = await createRobotToken({
|
|
25
|
+
auth,
|
|
26
|
+
resourceType: scopeType,
|
|
27
|
+
resourceId: scopeId,
|
|
28
|
+
body: {
|
|
29
|
+
label,
|
|
30
|
+
memberships: [{ resourceType: scopeType, resourceId: scopeId, roleNames: [roleName] }],
|
|
31
|
+
},
|
|
32
|
+
logger: log,
|
|
33
|
+
});
|
|
34
|
+
if (!ok || !robot) {
|
|
35
|
+
spinner.fail('Failed to mint deploy token');
|
|
36
|
+
const suggestions = [];
|
|
37
|
+
if (error && /access|permission|forbidden|role/i.test(error)) {
|
|
38
|
+
suggestions.push(scopeType === 'organization'
|
|
39
|
+
? 'Verify you have organization admin access.'
|
|
40
|
+
: 'Verify you have project admin access.');
|
|
41
|
+
}
|
|
42
|
+
return { success: false, error: error || 'Failed to mint deploy token', suggestions };
|
|
43
|
+
}
|
|
44
|
+
spinner.stop().clear();
|
|
45
|
+
if (flags.print) {
|
|
46
|
+
log(robot.token);
|
|
47
|
+
return { success: true, json: serializeRobot(robot) };
|
|
48
|
+
}
|
|
49
|
+
if (flags.json) {
|
|
50
|
+
return { success: true, json: serializeRobot(robot) };
|
|
51
|
+
}
|
|
52
|
+
log(styleText('green', `Minted "${robot.label}" (${robot.id})`));
|
|
53
|
+
if (robot.expiresAt) {
|
|
54
|
+
log(styleText('dim', `Expires: ${robot.expiresAt}`));
|
|
55
|
+
}
|
|
56
|
+
const action = await select({
|
|
57
|
+
message: 'What would you like to do with the token?',
|
|
58
|
+
choices: [
|
|
59
|
+
{ name: 'Copy to clipboard', value: 'copy' },
|
|
60
|
+
{ name: 'Print to terminal', value: 'print' },
|
|
61
|
+
{ name: 'Exit (token will be lost)', value: 'exit' },
|
|
62
|
+
],
|
|
63
|
+
default: 'copy',
|
|
64
|
+
});
|
|
65
|
+
if (action === 'copy') {
|
|
66
|
+
try {
|
|
67
|
+
await clipboardWrite(robot.token);
|
|
68
|
+
log(styleText('green', 'Token copied to clipboard.'));
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const msg = err instanceof ClipboardUnavailableError
|
|
72
|
+
? err.message
|
|
73
|
+
: err instanceof Error
|
|
74
|
+
? err.message
|
|
75
|
+
: String(err);
|
|
76
|
+
log.warn(styleText('yellow', `Could not copy to clipboard: ${msg}`));
|
|
77
|
+
const printInstead = await confirm({
|
|
78
|
+
message: 'Print the token to the terminal instead?',
|
|
79
|
+
default: false,
|
|
80
|
+
});
|
|
81
|
+
if (printInstead) {
|
|
82
|
+
await printAndConfirm(log, robot.token);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
log(styleText('yellow', 'Token discarded. You can mint a new one anytime.'));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (action === 'print') {
|
|
90
|
+
await printAndConfirm(log, robot.token);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
log(styleText('yellow', 'Token discarded. You can mint a new one anytime.'));
|
|
94
|
+
}
|
|
95
|
+
return { success: true, json: serializeRobot(robot) };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Show the token inside an inquirer prompt and rely on `clearPromptOnDone`
|
|
99
|
+
* so inquirer wipes the entire rendered block (token included) on Enter.
|
|
100
|
+
*/
|
|
101
|
+
async function printAndConfirm(log, token) {
|
|
102
|
+
await input({
|
|
103
|
+
message: `\n${token}\n\nPress Enter once you have copied the token (it will be erased):`,
|
|
104
|
+
default: '',
|
|
105
|
+
theme: { prefix: '' },
|
|
106
|
+
}, { clearPromptOnDone: true });
|
|
107
|
+
log(styleText('dim', 'Token erased.'));
|
|
108
|
+
}
|
|
109
|
+
function serializeRobot(robot) {
|
|
110
|
+
return robot;
|
|
111
|
+
}
|