@sanity/runtime-cli 17.1.0-next.gcfbd613 → 17.2.0-blueprinit.gd1553d6
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 +35 -26
- package/dist/actions/blueprinit/manifest.d.ts +13 -0
- package/dist/actions/blueprinit/manifest.js +57 -0
- package/dist/actions/blueprinit/studio.d.ts +6 -0
- package/dist/actions/blueprinit/studio.js +43 -0
- package/dist/actions/blueprinit/templates.d.ts +34 -0
- package/dist/actions/blueprinit/templates.js +83 -0
- package/dist/actions/blueprinit/workspace.d.ts +15 -0
- package/dist/actions/blueprinit/workspace.js +142 -0
- package/dist/actions/blueprints/assets.js +15 -24
- package/dist/actions/blueprints/pre-deploy.d.ts +29 -0
- package/dist/actions/blueprints/pre-deploy.js +164 -0
- package/dist/actions/sanity/datasets.d.ts +15 -0
- package/dist/actions/sanity/datasets.js +26 -0
- package/dist/commands/blueprinit.d.ts +13 -0
- package/dist/commands/blueprinit.js +44 -0
- package/dist/commands/functions/env/add.d.ts +3 -0
- package/dist/commands/functions/env/add.js +6 -2
- package/dist/commands/functions/env/list.d.ts +3 -0
- package/dist/commands/functions/env/list.js +9 -3
- package/dist/commands/functions/env/remove.d.ts +3 -0
- package/dist/commands/functions/env/remove.js +9 -3
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/cores/blueprinit/adopt.d.ts +6 -0
- package/dist/cores/blueprinit/adopt.js +90 -0
- package/dist/cores/blueprinit/finish.d.ts +14 -0
- package/dist/cores/blueprinit/finish.js +36 -0
- package/dist/cores/blueprinit/fresh.d.ts +3 -0
- package/dist/cores/blueprinit/fresh.js +88 -0
- package/dist/cores/blueprinit/prompts.d.ts +4 -0
- package/dist/cores/blueprinit/prompts.js +14 -0
- package/dist/cores/blueprinit/selection.d.ts +15 -0
- package/dist/cores/blueprinit/selection.js +63 -0
- package/dist/cores/blueprinit/types.d.ts +13 -0
- package/dist/cores/blueprinit/types.js +1 -0
- package/dist/cores/blueprinit.d.ts +7 -0
- package/dist/cores/blueprinit.js +39 -0
- package/dist/cores/blueprints/deploy.js +16 -64
- package/dist/utils/functions/child-process-wrapper.js +13 -171
- package/dist/utils/functions/child-runtime/console.d.ts +1 -0
- package/dist/utils/functions/child-runtime/console.js +50 -0
- package/dist/utils/functions/child-runtime/invoke.d.ts +4 -0
- package/dist/utils/functions/child-runtime/invoke.js +47 -0
- package/dist/utils/functions/child-runtime/resources.d.ts +4 -0
- package/dist/utils/functions/child-runtime/resources.js +59 -0
- package/dist/utils/studios/prepare-asset.d.ts +6 -0
- package/dist/utils/studios/prepare-asset.js +139 -0
- package/dist/utils/types.d.ts +4 -7
- package/dist/utils/types.js +2 -3
- package/oclif.manifest.json +104 -7
- package/package.json +9 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { STARTER_FUNCTION_NAME, slugify } from '../../actions/blueprinit/manifest.js';
|
|
2
|
+
import { scaffoldWorkspace } from '../../actions/blueprinit/workspace.js';
|
|
3
|
+
import { listDatasets } from '../../actions/sanity/datasets.js';
|
|
4
|
+
import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
|
|
5
|
+
import { check } from '../../utils/display/presenters.js';
|
|
6
|
+
import { getLatestNpmVersion } from '../../utils/external/npmjs.js';
|
|
7
|
+
import { isInteractive } from '../../utils/interactive.js';
|
|
8
|
+
import { alignSteps, createStackAndFinish } from './finish.js';
|
|
9
|
+
import { confirmUnlessSkipped, promptForStudioName } from './prompts.js';
|
|
10
|
+
import { chooseDataset, chooseOrganization, chooseProject } from './selection.js';
|
|
11
|
+
export async function freshFlow(options) {
|
|
12
|
+
const { bin, dir, flags, log, token } = options;
|
|
13
|
+
const grouped = await groupProjectsByOrganization({ token, logger: log });
|
|
14
|
+
if (!grouped.ok) {
|
|
15
|
+
return { success: false, error: `Could not list organizations: ${grouped.error}` };
|
|
16
|
+
}
|
|
17
|
+
const organizationResult = await chooseOrganization(grouped.organizations, flags.organizationId, isInteractive);
|
|
18
|
+
if (!organizationResult.ok) {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
error: organizationResult.error.message,
|
|
22
|
+
suggestions: organizationResult.error.suggestions,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const { organization, projects = [] } = organizationResult.value;
|
|
26
|
+
const projectResult = await chooseProject(projects, flags.projectId, organization.name, isInteractive);
|
|
27
|
+
if (!projectResult.ok) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: projectResult.error.message,
|
|
31
|
+
suggestions: projectResult.error.suggestions,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const project = projectResult.value;
|
|
35
|
+
const datasets = await listDatasets({ token, projectId: project.id, logger: log });
|
|
36
|
+
if (!datasets.ok) {
|
|
37
|
+
return { success: false, error: `Could not list datasets: ${datasets.error}` };
|
|
38
|
+
}
|
|
39
|
+
const dataset = chooseDataset(datasets.datasets);
|
|
40
|
+
if (!dataset)
|
|
41
|
+
return { success: false, error: `No datasets found in ${project.displayName}.` };
|
|
42
|
+
log(check(`Org: ${organization.name} (${organization.id})`));
|
|
43
|
+
log(check(`Project: ${project.displayName} (${project.id}), dataset: ${dataset.name}`));
|
|
44
|
+
log('');
|
|
45
|
+
const confirmed = await confirmUnlessSkipped(flags, isInteractive, `Set up this directory as the workspace for "${project.displayName}"?`);
|
|
46
|
+
if (!confirmed)
|
|
47
|
+
return { success: false, error: 'Initialization cancelled.' };
|
|
48
|
+
const stackName = slugify(project.displayName) || project.id;
|
|
49
|
+
const studioName = await promptForStudioName(flags, isInteractive);
|
|
50
|
+
log('Files to write:');
|
|
51
|
+
log(' + sanity.blueprint.ts project + dataset (attached), studio, function');
|
|
52
|
+
log(` + ${`studios/${studioName}/`.padEnd(32)}Studio with a "post" type`);
|
|
53
|
+
log(` + ${`functions/${STARTER_FUNCTION_NAME}/`.padEnd(32)}sets a post slug from its title`);
|
|
54
|
+
log(` + .sanity/blueprint.config.json org scope, stack ${stackName}`);
|
|
55
|
+
log(' + .gitignore');
|
|
56
|
+
log(' + package.json');
|
|
57
|
+
log('');
|
|
58
|
+
const scaffold = scaffoldWorkspace({
|
|
59
|
+
dir,
|
|
60
|
+
projectId: project.id,
|
|
61
|
+
displayName: project.displayName,
|
|
62
|
+
datasetName: dataset.name,
|
|
63
|
+
stackName,
|
|
64
|
+
studioName,
|
|
65
|
+
blueprintsVersion: await getLatestNpmVersion('@sanity/blueprints', log),
|
|
66
|
+
});
|
|
67
|
+
return await createStackAndFinish({
|
|
68
|
+
...options,
|
|
69
|
+
organizationId: organization.id,
|
|
70
|
+
projectId: project.id,
|
|
71
|
+
datasetName: dataset.name,
|
|
72
|
+
stackName,
|
|
73
|
+
blueprintFilePath: scaffold.blueprintFilePath,
|
|
74
|
+
files: scaffold.files,
|
|
75
|
+
nextSteps: [
|
|
76
|
+
...alignSteps([
|
|
77
|
+
['npm install'],
|
|
78
|
+
[`npm run dev -w studios/${studioName}`, 'open the Studio and add a post'],
|
|
79
|
+
[`npx ${bin} blueprints plan`, 'preview the deployment'],
|
|
80
|
+
[`npx ${bin} blueprints deploy`, 'deploy the resources'],
|
|
81
|
+
]),
|
|
82
|
+
'',
|
|
83
|
+
'Test the function on that post:',
|
|
84
|
+
` npx ${bin} functions test ${STARTER_FUNCTION_NAME} --document-id <id>` +
|
|
85
|
+
` --project-id ${project.id} --dataset ${dataset.name} --with-user-token`,
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BlueprinitFlags } from './types.js';
|
|
2
|
+
export declare const DEFAULT_STUDIO_NAME = "studio";
|
|
3
|
+
export declare function confirmUnlessSkipped(flags: BlueprinitFlags, interactive: boolean, message: string): Promise<boolean>;
|
|
4
|
+
export declare function promptForStudioName(flags: BlueprinitFlags, interactive: boolean): Promise<string>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
2
|
+
import { slugify } from '../../actions/blueprinit/manifest.js';
|
|
3
|
+
export const DEFAULT_STUDIO_NAME = 'studio';
|
|
4
|
+
export async function confirmUnlessSkipped(flags, interactive, message) {
|
|
5
|
+
if (flags.yes || !interactive)
|
|
6
|
+
return true;
|
|
7
|
+
return await confirm({ message, default: true });
|
|
8
|
+
}
|
|
9
|
+
export async function promptForStudioName(flags, interactive) {
|
|
10
|
+
if (flags.yes || !interactive)
|
|
11
|
+
return DEFAULT_STUDIO_NAME;
|
|
12
|
+
const answer = await input({ message: 'Name your studio?', default: DEFAULT_STUDIO_NAME });
|
|
13
|
+
return slugify(answer) || DEFAULT_STUDIO_NAME;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Dataset } from '../../actions/sanity/datasets.js';
|
|
2
|
+
import type { GroupedProjects, Project } from '../../actions/sanity/projects.js';
|
|
3
|
+
import type { Result } from '../../utils/types.js';
|
|
4
|
+
interface SelectionError {
|
|
5
|
+
message: string;
|
|
6
|
+
suggestions?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function chooseOrganization(groups: GroupedProjects[], organizationId: string | undefined, interactive: boolean): Promise<Result<GroupedProjects, SelectionError>>;
|
|
9
|
+
export declare function chooseProject(projects: Project[], projectId: string | undefined, organizationName: string, interactive: boolean): Promise<Result<Project, SelectionError>>;
|
|
10
|
+
export declare function chooseDataset(datasets: Dataset[]): Dataset | undefined;
|
|
11
|
+
export declare function findProject(groups: GroupedProjects[], projectId: string): {
|
|
12
|
+
organization: GroupedProjects['organization'];
|
|
13
|
+
project: Project;
|
|
14
|
+
} | undefined;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
export async function chooseOrganization(groups, organizationId, interactive) {
|
|
3
|
+
if (groups.length === 0) {
|
|
4
|
+
return failure('No organizations found for this account.');
|
|
5
|
+
}
|
|
6
|
+
if (organizationId) {
|
|
7
|
+
const organization = groups.find((group) => group.organization.id === organizationId);
|
|
8
|
+
return organization
|
|
9
|
+
? { ok: true, value: organization }
|
|
10
|
+
: failure(`No organization "${organizationId}" found for this account.`);
|
|
11
|
+
}
|
|
12
|
+
if (groups.length === 1)
|
|
13
|
+
return { ok: true, value: groups[0] };
|
|
14
|
+
if (!interactive) {
|
|
15
|
+
return failure('Multiple organizations found.', ['Pass --organization-id <id> to choose one.']);
|
|
16
|
+
}
|
|
17
|
+
const selectedId = await select({
|
|
18
|
+
message: 'Choose a Sanity organization:',
|
|
19
|
+
choices: groups.map(({ organization, projects }) => ({
|
|
20
|
+
name: organization.name,
|
|
21
|
+
value: organization.id,
|
|
22
|
+
description: `${projects?.length ?? 0} projects`,
|
|
23
|
+
})),
|
|
24
|
+
});
|
|
25
|
+
const selected = groups.find(({ organization }) => organization.id === selectedId);
|
|
26
|
+
return selected ? { ok: true, value: selected } : failure('Organization not found.');
|
|
27
|
+
}
|
|
28
|
+
export async function chooseProject(projects, projectId, organizationName, interactive) {
|
|
29
|
+
if (projects.length === 0) {
|
|
30
|
+
return failure(`No projects found in ${organizationName}.`);
|
|
31
|
+
}
|
|
32
|
+
if (projectId) {
|
|
33
|
+
const project = projects.find(({ id }) => id === projectId);
|
|
34
|
+
return project
|
|
35
|
+
? { ok: true, value: project }
|
|
36
|
+
: failure(`No project "${projectId}" found in ${organizationName}.`);
|
|
37
|
+
}
|
|
38
|
+
if (projects.length === 1)
|
|
39
|
+
return { ok: true, value: projects[0] };
|
|
40
|
+
if (!interactive) {
|
|
41
|
+
return failure('Multiple projects found.', ['Pass --project-id <id> to choose one.']);
|
|
42
|
+
}
|
|
43
|
+
const selectedId = await select({
|
|
44
|
+
message: 'Choose a Sanity project:',
|
|
45
|
+
choices: projects.map((project) => ({ name: project.displayName, value: project.id })),
|
|
46
|
+
});
|
|
47
|
+
const selected = projects.find(({ id }) => id === selectedId);
|
|
48
|
+
return selected ? { ok: true, value: selected } : failure('Project not found.');
|
|
49
|
+
}
|
|
50
|
+
export function chooseDataset(datasets) {
|
|
51
|
+
return datasets.find(({ name }) => name === 'production') ?? datasets[0];
|
|
52
|
+
}
|
|
53
|
+
export function findProject(groups, projectId) {
|
|
54
|
+
for (const { organization, projects = [] } of groups) {
|
|
55
|
+
const project = projects.find(({ id }) => id === projectId);
|
|
56
|
+
if (project)
|
|
57
|
+
return { organization, project };
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function failure(message, suggestions) {
|
|
62
|
+
return { ok: false, error: { message, suggestions } };
|
|
63
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Logger } from '../../utils/logger.js';
|
|
2
|
+
export interface BlueprinitFlags {
|
|
3
|
+
yes?: boolean;
|
|
4
|
+
organizationId?: string;
|
|
5
|
+
projectId?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BlueprinitFlowOptions {
|
|
8
|
+
bin: string;
|
|
9
|
+
dir: string;
|
|
10
|
+
flags: BlueprinitFlags;
|
|
11
|
+
log: Logger;
|
|
12
|
+
token: string;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BlueprinitFlags } from './blueprinit/types.js';
|
|
2
|
+
import type { CoreConfig, CoreResult } from './index.js';
|
|
3
|
+
export interface BlueprinitOptions extends CoreConfig {
|
|
4
|
+
dir?: string;
|
|
5
|
+
flags?: BlueprinitFlags;
|
|
6
|
+
}
|
|
7
|
+
export declare function blueprinitCore(options: BlueprinitOptions): Promise<CoreResult>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { cwd } from 'node:process';
|
|
2
|
+
import { detectStudio } from '../actions/blueprinit/studio.js';
|
|
3
|
+
import { findBlueprintFile } from '../actions/blueprints/blueprint.js';
|
|
4
|
+
import { validTokenOrErrorMessage } from '../utils/validated-token.js';
|
|
5
|
+
import { adoptFlow } from './blueprinit/adopt.js';
|
|
6
|
+
import { freshFlow } from './blueprinit/fresh.js';
|
|
7
|
+
export async function blueprinitCore(options) {
|
|
8
|
+
const { bin, log, flags = {}, dir = cwd() } = options;
|
|
9
|
+
try {
|
|
10
|
+
const existingBlueprint = findBlueprintFile(dir);
|
|
11
|
+
if (existingBlueprint) {
|
|
12
|
+
log(`A blueprint already exists here (${existingBlueprint.fileName}).`);
|
|
13
|
+
log('');
|
|
14
|
+
log('Use these instead:');
|
|
15
|
+
log(` npx ${bin} blueprints config --edit`);
|
|
16
|
+
log(` npx ${bin} blueprints doctor`);
|
|
17
|
+
return {
|
|
18
|
+
success: true,
|
|
19
|
+
json: { alreadyInitialized: true, blueprintFile: existingBlueprint.fileName },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const tokenCheck = await validTokenOrErrorMessage(log);
|
|
23
|
+
if (!tokenCheck.ok) {
|
|
24
|
+
return {
|
|
25
|
+
success: false,
|
|
26
|
+
error: 'No Sanity session found.',
|
|
27
|
+
suggestions: ['Sign in first: npx sanity login', 'Then run blueprinit again.'],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
log('Setting up your Sanity workspace.');
|
|
31
|
+
log('');
|
|
32
|
+
const flowOptions = { bin, dir, flags, log, token: tokenCheck.value };
|
|
33
|
+
const studio = detectStudio(dir);
|
|
34
|
+
return studio ? await adoptFlow({ ...flowOptions, studio }) : await freshFlow(flowOptions);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { preDeploy } from '../../actions/blueprints/pre-deploy.js';
|
|
3
3
|
import { getStack, updateStack } from '../../actions/blueprints/stacks.js';
|
|
4
4
|
import { waitForOperation } from '../../actions/blueprints/wait-for-operation.js';
|
|
5
5
|
import { checkUserPermission } from '../../actions/sanity/access.js';
|
|
@@ -7,12 +7,7 @@ import { createHintCollector } from '../../utils/blueprints/hints.js';
|
|
|
7
7
|
import { niceId } from '../../utils/display/presenters.js';
|
|
8
8
|
import { CODE_OPERATION_UNCONFIRMED, EXIT_OPERATION_UNCONFIRMED } from '../../utils/exit-codes.js';
|
|
9
9
|
import { styleText } from '../../utils/style-text.js';
|
|
10
|
-
import { isAssetResource } from '../../utils/types.js';
|
|
11
10
|
import { getWarnings } from '../../utils/warnings.js';
|
|
12
|
-
const DEFAULT_ASSET_TIMEOUT = 60;
|
|
13
|
-
const assetTimeoutS = Number(process.env.SANITY_ASSET_TIMEOUT) || DEFAULT_ASSET_TIMEOUT;
|
|
14
|
-
const assetTimeoutMs = assetTimeoutS * 1000;
|
|
15
|
-
const warnTimeoutMs = assetTimeoutMs / 2;
|
|
16
11
|
export async function blueprintDeployCore(options) {
|
|
17
12
|
const { bin = 'sanity', log, auth, stackId, scopeType, scopeId, deployedStack, blueprint, flags, } = options;
|
|
18
13
|
const { verbose } = flags;
|
|
@@ -39,17 +34,21 @@ export async function blueprintDeployCore(options) {
|
|
|
39
34
|
};
|
|
40
35
|
}
|
|
41
36
|
const resources = [...blueprint.resources];
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
37
|
+
const preDeployResult = await preDeploy({
|
|
38
|
+
scopeType,
|
|
39
|
+
scopeId,
|
|
40
|
+
resources,
|
|
41
|
+
stackId,
|
|
42
|
+
stackName,
|
|
43
|
+
auth,
|
|
44
|
+
blueprintFileDir,
|
|
45
|
+
installer,
|
|
46
|
+
log,
|
|
47
|
+
bin,
|
|
48
|
+
verbose,
|
|
49
|
+
});
|
|
50
|
+
if (!preDeployResult?.success) {
|
|
51
|
+
return preDeployResult;
|
|
53
52
|
}
|
|
54
53
|
const spinner = log.ora('Deploying...').start();
|
|
55
54
|
const { ok: deployOk, stack, error: deployError, } = await updateStack({
|
|
@@ -152,53 +151,6 @@ export async function blueprintDeployCore(options) {
|
|
|
152
151
|
return { success: false, error: errorMessage };
|
|
153
152
|
}
|
|
154
153
|
}
|
|
155
|
-
async function preDeploy(resource, options) {
|
|
156
|
-
const { auth, blueprintFileDir, installer, log } = options;
|
|
157
|
-
const fnSpinner = log.ora({ text: `Processing ${resource.name}...`, prefixText: ' ' }).start();
|
|
158
|
-
const warnTimer = setTimeout(() => {
|
|
159
|
-
fnSpinner.text = `Still processing ${resource.name}, this can take a moment...`;
|
|
160
|
-
}, warnTimeoutMs);
|
|
161
|
-
let assetTimeoutTimer;
|
|
162
|
-
let result;
|
|
163
|
-
try {
|
|
164
|
-
result = await Promise.race([
|
|
165
|
-
stashAsset({ resource, blueprintFileDir, auth, logger: log, installer }),
|
|
166
|
-
new Promise((_, reject) => {
|
|
167
|
-
assetTimeoutTimer = setTimeout(() => {
|
|
168
|
-
reject(new Error(`Processing ${resource.name} timed out after ${assetTimeoutS}s`));
|
|
169
|
-
}, assetTimeoutMs);
|
|
170
|
-
}),
|
|
171
|
-
]);
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
-
fnSpinner.fail(msg);
|
|
176
|
-
return { success: false, error: msg };
|
|
177
|
-
}
|
|
178
|
-
finally {
|
|
179
|
-
clearTimeout(warnTimer);
|
|
180
|
-
clearTimeout(assetTimeoutTimer);
|
|
181
|
-
}
|
|
182
|
-
if (result.success && result.assetId) {
|
|
183
|
-
const updatedResource = { ...resource, src: result.assetId };
|
|
184
|
-
fnSpinner.succeed(`${updatedResource.name} ${niceId(result.assetId)}`);
|
|
185
|
-
log(` Source: ${updatedResource.src}`);
|
|
186
|
-
if (result.hash) {
|
|
187
|
-
if (result.hash.length > 24) {
|
|
188
|
-
log.verbose(` Hash: ${result.hash.slice(0, 8)}...${result.hash.slice(-12)}`);
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
log.verbose(` Hash: ${result.hash}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (result.exists)
|
|
195
|
-
log.verbose(' Asset unchanged');
|
|
196
|
-
return { success: true, resource: updatedResource };
|
|
197
|
-
}
|
|
198
|
-
const errorMsg = `Failed uploading ${resource.name} asset, deploy has stopped`;
|
|
199
|
-
fnSpinner.fail(errorMsg);
|
|
200
|
-
return { success: false, error: result.error || 'Failed to process asset' };
|
|
201
|
-
}
|
|
202
154
|
const CHECK_LEGACY_DEPLOY_PERMISSION = false;
|
|
203
155
|
const BLUEPRINTS_PERMISSION_ENFORCEMENT_DATE = 'July 1, 2026';
|
|
204
156
|
async function checkLegacyDeployPermission({ auth, scopeType, scopeId, log, }) {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {table as renderTable} from 'node:console'
|
|
2
1
|
import {existsSync, statSync} from 'node:fs'
|
|
3
2
|
import {isAbsolute, join} from 'node:path'
|
|
4
3
|
import process from 'node:process'
|
|
5
4
|
import {pathToFileURL} from 'node:url'
|
|
6
|
-
import {
|
|
5
|
+
import {installConsole} from './child-runtime/console.js'
|
|
6
|
+
import {createInvoker} from './child-runtime/invoke.js'
|
|
7
|
+
import {createResourcesApi} from './child-runtime/resources.js'
|
|
8
|
+
|
|
9
|
+
installConsole()
|
|
10
|
+
const invoker = createInvoker()
|
|
7
11
|
|
|
8
12
|
export function getFunctionSource(src) {
|
|
9
13
|
const pathToCheck = isAbsolute(src) ? src : join(process.cwd(), src)
|
|
@@ -18,91 +22,6 @@ export function getFunctionSource(src) {
|
|
|
18
22
|
return pathToFileURL(pathToCheck).href
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
// Monkey patch console menthods to have logs match server log format
|
|
22
|
-
function logPrefix(level, ...args) {
|
|
23
|
-
const date = new Date()
|
|
24
|
-
const shouldUseColors = process.env.FORCE_COLOR === '1'
|
|
25
|
-
const message = args
|
|
26
|
-
.map((arg) =>
|
|
27
|
-
typeof arg === 'string' ? arg : inspect(arg, {depth: null, colors: shouldUseColors}),
|
|
28
|
-
)
|
|
29
|
-
.join(' ')
|
|
30
|
-
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()} ${level.toUpperCase()} ${message}`
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
console.log = (...args) => {
|
|
34
|
-
process.stdout.write(`${logPrefix('info', ...args)}\n`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
console.info = (...args) => {
|
|
38
|
-
process.stdout.write(`${logPrefix('info', ...args)}\n`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.dir = (obj, options) => {
|
|
42
|
-
const shouldUseColors = process.env.FORCE_COLOR === '1'
|
|
43
|
-
const inspectOptions = {...options, colors: shouldUseColors}
|
|
44
|
-
process.stdout.write(`${logPrefix('info', inspect(obj, inspectOptions))}\n`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
console.table = (data, columns) => {
|
|
48
|
-
const prefix = logPrefix('info')
|
|
49
|
-
|
|
50
|
-
// Capture table output
|
|
51
|
-
const originalWrite = process.stdout.write.bind(process.stdout)
|
|
52
|
-
let buffer = ''
|
|
53
|
-
|
|
54
|
-
process.stdout.write = (chunk, _encoding, callback) => {
|
|
55
|
-
buffer += chunk
|
|
56
|
-
if (typeof callback === 'function') callback()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
renderTable(data, columns)
|
|
60
|
-
|
|
61
|
-
// Restore and print
|
|
62
|
-
process.stdout.write = originalWrite
|
|
63
|
-
for (const line of buffer.split('\n')) {
|
|
64
|
-
if (line.trim()) {
|
|
65
|
-
const message = `${!line.startsWith(prefix) ? `${prefix} ${line}` : line}`.trim()
|
|
66
|
-
process.stdout.write(`${message}\n`)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
console.warn = (...args) => {
|
|
72
|
-
process.stdout.write(`${logPrefix('warn', ...args)}\n`)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
console.error = (...args) => {
|
|
76
|
-
process.stdout.write(`${logPrefix('error', ...args)}\n`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Pending nested `context.invoke` calls, keyed by correlation id. Each entry
|
|
81
|
-
* holds the resolve/reject of the Promise returned to the user's handler; it is
|
|
82
|
-
* settled when the matching `invoke-response` arrives from the parent.
|
|
83
|
-
* @type {Map<string, {resolve: (value: unknown) => void, reject: (reason: Error) => void}>}
|
|
84
|
-
*/
|
|
85
|
-
const pendingInvokes = new Map()
|
|
86
|
-
let invokeSeq = 0
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* `context.invoke(name, payload)` — ask the parent to execute another blueprint
|
|
90
|
-
* function and resolve with its return value. Sent over IPC and correlated by id.
|
|
91
|
-
* @param {string} name
|
|
92
|
-
* @param {Record<string, unknown>} [payload]
|
|
93
|
-
* @returns {Promise<unknown>}
|
|
94
|
-
*/
|
|
95
|
-
function invokeFunction(name, payload = {}) {
|
|
96
|
-
if (typeof name !== 'string' || name.length === 0) {
|
|
97
|
-
return Promise.reject(new Error('context.invoke requires a function name'))
|
|
98
|
-
}
|
|
99
|
-
const id = String(++invokeSeq)
|
|
100
|
-
return new Promise((resolve, reject) => {
|
|
101
|
-
pendingInvokes.set(id, {resolve, reject})
|
|
102
|
-
process.send(JSON.stringify({type: 'invoke-request', id, name, payload}))
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
25
|
// Listen for IPC messages from the parent process.
|
|
107
26
|
process.on('message', async (data) => {
|
|
108
27
|
let jsonData = null
|
|
@@ -115,18 +34,7 @@ process.on('message', async (data) => {
|
|
|
115
34
|
|
|
116
35
|
// Response to a nested context.invoke() — route to the awaiting Promise and stop.
|
|
117
36
|
if (jsonData?.type === 'invoke-response') {
|
|
118
|
-
|
|
119
|
-
if (!pending) return
|
|
120
|
-
pendingInvokes.delete(jsonData.id)
|
|
121
|
-
if (jsonData.ok) {
|
|
122
|
-
pending.resolve(jsonData.result?.json)
|
|
123
|
-
} else {
|
|
124
|
-
const {message, stack, name} = jsonData.error ?? {}
|
|
125
|
-
const error = new Error(message ?? 'Nested invoke failed')
|
|
126
|
-
if (stack) error.stack = stack
|
|
127
|
-
if (name) error.name = name
|
|
128
|
-
pending.reject(error)
|
|
129
|
-
}
|
|
37
|
+
invoker.handleResponse(jsonData)
|
|
130
38
|
return
|
|
131
39
|
}
|
|
132
40
|
|
|
@@ -160,25 +68,20 @@ process.on('message', async (data) => {
|
|
|
160
68
|
try {
|
|
161
69
|
// Import the function code
|
|
162
70
|
const entry = await import(getFunctionSource(srcPath))
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
throw new Error(
|
|
167
|
-
'No valid handler found. Please provide a default export or a named export, "handler"',
|
|
168
|
-
)
|
|
71
|
+
const handler = entry.handler || entry.default
|
|
72
|
+
if (typeof handler !== 'function') {
|
|
73
|
+
throw new Error('No valid handler found. Provide a default export or a named "handler".')
|
|
169
74
|
}
|
|
170
75
|
|
|
171
76
|
// Replace resources JSON with Resources API
|
|
172
77
|
context.resources = createResourcesApi(context.resources)
|
|
173
78
|
|
|
174
79
|
// Allow the handler to invoke other blueprint functions by name.
|
|
175
|
-
context.invoke =
|
|
176
|
-
|
|
177
|
-
json = await eventHandler({
|
|
80
|
+
context.invoke = invoker.invoke
|
|
81
|
+
json = await handler({
|
|
178
82
|
context,
|
|
179
83
|
event,
|
|
180
|
-
|
|
181
|
-
done: async (_tags) => new Response(null, {status: 204}),
|
|
84
|
+
done: async () => new Response(null, {status: 204}),
|
|
182
85
|
})
|
|
183
86
|
|
|
184
87
|
// Restore streams
|
|
@@ -191,7 +94,6 @@ process.on('message', async (data) => {
|
|
|
191
94
|
// Restore streams on error
|
|
192
95
|
process.stdout.write = originalStdoutWrite
|
|
193
96
|
process.stderr.write = originalStderrWrite
|
|
194
|
-
|
|
195
97
|
const errorInfo = {
|
|
196
98
|
type: 'result',
|
|
197
99
|
json: null,
|
|
@@ -207,63 +109,3 @@ process.on('message', async (data) => {
|
|
|
207
109
|
process.exit(1)
|
|
208
110
|
}
|
|
209
111
|
})
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* The returned proxy wraps a function so the API itself is callable.
|
|
213
|
-
* @param {Record<string, ResourceEntry[]>} resources
|
|
214
|
-
* @returns {ResourcesApi}
|
|
215
|
-
*/
|
|
216
|
-
export function createResourcesApi(resources) {
|
|
217
|
-
const findByName = (name) => {
|
|
218
|
-
for (const group of Object.values(resources)) {
|
|
219
|
-
const found = group.find((r) => r.name === name)
|
|
220
|
-
if (found) return found
|
|
221
|
-
}
|
|
222
|
-
return undefined
|
|
223
|
-
}
|
|
224
|
-
let allCache
|
|
225
|
-
const all = () => (allCache ??= Object.values(resources).flat())
|
|
226
|
-
|
|
227
|
-
return /** @type {ResourcesApi} */ (
|
|
228
|
-
/** @type {unknown} */ (
|
|
229
|
-
/*
|
|
230
|
-
* The Proxy target is `findByName`, so invoking the API as a function —
|
|
231
|
-
* `context.resources('my-proj')` — bypasses the `get` trap entirely and
|
|
232
|
-
* calls `findByName` directly for a cross-type lookup by name.
|
|
233
|
-
*/
|
|
234
|
-
new Proxy(findByName, {
|
|
235
|
-
get: (_target, prop) => {
|
|
236
|
-
// context.resources.all() — flat array of every resource across types
|
|
237
|
-
if (prop === 'all') return all
|
|
238
|
-
// [...context.resources] / for (const r of context.resources) — iterate all
|
|
239
|
-
if (prop === Symbol.iterator) {
|
|
240
|
-
return function* () {
|
|
241
|
-
yield* all()
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Symbol property access (other than iterator above) and `then`
|
|
247
|
-
* lookups return undefined. The `then` guard prevents the API from
|
|
248
|
-
* looking like a thenable, so `await context.resources` resolves to
|
|
249
|
-
* the proxy itself rather than hanging or unwrapping unexpectedly.
|
|
250
|
-
*/
|
|
251
|
-
if (typeof prop !== 'string' || prop === 'then') {
|
|
252
|
-
return undefined
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* context.resources.<type>(name) — per-type lookup,
|
|
257
|
-
* e.g. context.resources.project('my-proj').
|
|
258
|
-
* Returns a function so the caller supplies the name;
|
|
259
|
-
* unknown types yield a function that always returns undefined.
|
|
260
|
-
*/
|
|
261
|
-
return (/** @type {string} */ name) =>
|
|
262
|
-
Object.hasOwn(resources, prop)
|
|
263
|
-
? resources[prop].find((r) => r.name === name)
|
|
264
|
-
: undefined
|
|
265
|
-
},
|
|
266
|
-
})
|
|
267
|
-
)
|
|
268
|
-
)
|
|
269
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function installConsole(): void
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {table as renderTable} from 'node:console'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import {inspect} from 'node:util'
|
|
4
|
+
|
|
5
|
+
// Monkey patch console methods to have logs match server log format
|
|
6
|
+
function logPrefix(level, ...args) {
|
|
7
|
+
const date = new Date()
|
|
8
|
+
const shouldUseColors = process.env.FORCE_COLOR === '1'
|
|
9
|
+
const message = args
|
|
10
|
+
.map((arg) =>
|
|
11
|
+
typeof arg === 'string' ? arg : inspect(arg, {depth: null, colors: shouldUseColors}),
|
|
12
|
+
)
|
|
13
|
+
.join(' ')
|
|
14
|
+
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()} ${level.toUpperCase()} ${message}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function installConsole() {
|
|
18
|
+
console.log = (...args) => process.stdout.write(`${logPrefix('info', ...args)}\n`)
|
|
19
|
+
console.info = (...args) => process.stdout.write(`${logPrefix('info', ...args)}\n`)
|
|
20
|
+
console.warn = (...args) => process.stdout.write(`${logPrefix('warn', ...args)}\n`)
|
|
21
|
+
console.error = (...args) => process.stdout.write(`${logPrefix('error', ...args)}\n`)
|
|
22
|
+
console.dir = (obj, options) => {
|
|
23
|
+
const shouldUseColors = process.env.FORCE_COLOR === '1'
|
|
24
|
+
const inspectOptions = {...options, colors: shouldUseColors}
|
|
25
|
+
process.stdout.write(`${logPrefix('info', inspect(obj, inspectOptions))}\n`)
|
|
26
|
+
}
|
|
27
|
+
console.table = (data, columns) => {
|
|
28
|
+
const prefix = logPrefix('info')
|
|
29
|
+
|
|
30
|
+
// Capture table output
|
|
31
|
+
const originalWrite = process.stdout.write.bind(process.stdout)
|
|
32
|
+
let buffer = ''
|
|
33
|
+
|
|
34
|
+
process.stdout.write = (chunk, _encoding, callback) => {
|
|
35
|
+
buffer += chunk
|
|
36
|
+
if (typeof callback === 'function') callback()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
renderTable(data, columns)
|
|
40
|
+
|
|
41
|
+
// Restore and print
|
|
42
|
+
process.stdout.write = originalWrite
|
|
43
|
+
for (const line of buffer.split('\n')) {
|
|
44
|
+
if (line.trim()) {
|
|
45
|
+
const message = `${!line.startsWith(prefix) ? `${prefix} ${line}` : line}`.trim()
|
|
46
|
+
process.stdout.write(`${message}\n`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|