@dokebi/cli 0.4.2 → 0.4.27
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/AI.md +279 -57
- package/README.md +35 -10
- package/catalog.json +2520 -258
- package/describe.schema.json +748 -0
- package/dist/api-app.d.ts +2 -0
- package/dist/api-app.js +35 -0
- package/dist/app-urls.js +3 -2
- package/dist/bin.js +638 -37
- package/dist/capability-guide.d.ts +30 -0
- package/dist/capability-guide.js +122 -0
- package/dist/catalog.d.ts +16 -2
- package/dist/catalog.js +19 -7
- package/dist/check/asset-rules.d.ts +6 -0
- package/dist/check/asset-rules.js +163 -0
- package/dist/check/browser-sources.d.ts +20 -0
- package/dist/check/browser-sources.js +89 -0
- package/dist/check/console-rules.d.ts +6 -0
- package/dist/check/console-rules.js +40 -0
- package/dist/check/error-rules.d.ts +21 -0
- package/dist/check/error-rules.js +212 -0
- package/dist/check/form-rules.d.ts +12 -0
- package/dist/check/form-rules.js +69 -6
- package/dist/check/formatter-rules.d.ts +2 -0
- package/dist/check/formatter-rules.js +71 -0
- package/dist/check/i18n-rules.d.ts +27 -0
- package/dist/check/i18n-rules.js +353 -0
- package/dist/check/index.d.ts +15 -1
- package/dist/check/index.js +89 -11
- package/dist/check/mail-rules.d.ts +4 -0
- package/dist/check/mail-rules.js +72 -0
- package/dist/check/module-rules.d.ts +13 -0
- package/dist/check/module-rules.js +104 -0
- package/dist/check/packages.d.ts +8 -0
- package/dist/check/packages.js +16 -0
- package/dist/check/pool-rules.d.ts +8 -0
- package/dist/check/pool-rules.js +164 -0
- package/dist/check/problems.d.ts +6 -1
- package/dist/check/problems.js +100 -2
- package/dist/check/project-rules.d.ts +23 -3
- package/dist/check/project-rules.js +265 -23
- package/dist/check/seal-rules.d.ts +6 -0
- package/dist/check/seal-rules.js +68 -0
- package/dist/check/security-rules.d.ts +10 -0
- package/dist/check/security-rules.js +450 -0
- package/dist/check/shared-state-rules.d.ts +15 -0
- package/dist/check/shared-state-rules.js +117 -0
- package/dist/check/source-rules.d.ts +26 -3
- package/dist/check/source-rules.js +249 -28
- package/dist/check/structure-rules.d.ts +23 -0
- package/dist/check/structure-rules.js +502 -0
- package/dist/check/timer-rules.d.ts +7 -0
- package/dist/check/timer-rules.js +543 -0
- package/dist/check/typescript.d.ts +35 -5
- package/dist/check/typescript.js +163 -17
- package/dist/check/vite-rules.d.ts +2 -0
- package/dist/check/vite-rules.js +136 -0
- package/dist/check/wait-rules.d.ts +16 -0
- package/dist/check/wait-rules.js +337 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +381 -36
- package/dist/config-diff.d.ts +42 -0
- package/dist/config-diff.js +289 -0
- package/dist/config-edit.d.ts +35 -2
- package/dist/config-edit.js +145 -13
- package/dist/config-init.d.ts +15 -0
- package/dist/config-init.js +61 -0
- package/dist/config-paths.d.ts +22 -0
- package/dist/config-paths.js +82 -0
- package/dist/config-secrets.d.ts +46 -0
- package/dist/config-secrets.js +187 -0
- package/dist/config-show.d.ts +82 -0
- package/dist/config-show.js +563 -0
- package/dist/config-values.d.ts +8 -0
- package/dist/config-values.js +68 -0
- package/dist/configuration-scaffold.d.ts +42 -0
- package/dist/configuration-scaffold.js +258 -4
- package/dist/configuration.d.ts +63 -3
- package/dist/configuration.js +178 -4
- package/dist/deploy-scripts.d.ts +4 -0
- package/dist/deploy-scripts.js +361 -0
- package/dist/deploy-templates.d.ts +46 -0
- package/dist/deploy-templates.js +379 -0
- package/dist/deploy.d.ts +46 -0
- package/dist/deploy.js +592 -0
- package/dist/deployment.d.ts +2 -0
- package/dist/deployment.js +111 -0
- package/dist/describe-db.d.ts +22 -0
- package/dist/describe-db.js +176 -0
- package/dist/describe.d.ts +92 -0
- package/dist/describe.js +417 -0
- package/dist/dev-compose.d.ts +20 -0
- package/dist/dev-compose.js +154 -0
- package/dist/dev-guard.js +44 -0
- package/dist/dev-host.d.ts +13 -0
- package/dist/dev-host.js +55 -0
- package/dist/dev-infra.d.ts +72 -4
- package/dist/dev-infra.js +237 -17
- package/dist/dev-watch.d.ts +34 -0
- package/dist/dev-watch.js +192 -0
- package/dist/diagnostics.d.ts +10 -7
- package/dist/diagnostics.js +22 -4
- package/dist/drift.d.ts +50 -0
- package/dist/drift.js +218 -0
- package/dist/env-file.d.ts +25 -1
- package/dist/env-file.js +64 -6
- package/dist/errors.d.ts +7 -1
- package/dist/example-config.d.ts +2 -0
- package/dist/example-config.js +3 -0
- package/dist/first-admin.d.ts +43 -0
- package/dist/first-admin.js +175 -0
- package/dist/formatter-ignore.d.ts +37 -0
- package/dist/formatter-ignore.js +344 -0
- package/dist/generator.d.ts +45 -3
- package/dist/generator.js +205 -15
- package/dist/guide.d.ts +1 -1
- package/dist/guide.js +42 -21
- package/dist/install-module.d.ts +20 -2
- package/dist/install-module.js +45 -8
- package/dist/install-plan.js +5 -3
- package/dist/layout.d.ts +12 -1
- package/dist/layout.js +19 -6
- package/dist/log-writer-control.d.ts +2 -0
- package/dist/log-writer-control.js +17 -2
- package/dist/log-writer.js +43 -10
- package/dist/logging-config.d.ts +8 -3
- package/dist/logging-config.js +19 -5
- package/dist/manifest.d.ts +29 -1
- package/dist/manifest.js +84 -6
- package/dist/merge-install-defaults.d.ts +0 -1
- package/dist/merge-install-defaults.js +13 -47
- package/dist/npm-client.d.ts +40 -4
- package/dist/npm-client.js +152 -15
- package/dist/npm-paths.js +18 -4
- package/dist/openapi.d.ts +27 -0
- package/dist/openapi.js +649 -0
- package/dist/option-fixes.d.ts +39 -0
- package/dist/option-fixes.js +130 -0
- package/dist/output.d.ts +1 -1
- package/dist/output.js +5 -5
- package/dist/plugin-lifecycle.d.ts +34 -2
- package/dist/plugin-lifecycle.js +195 -16
- package/dist/plugin-options.d.ts +19 -4
- package/dist/plugin-options.js +138 -6
- package/dist/plugins-command.js +44 -7
- package/dist/process-group.d.ts +39 -0
- package/dist/process-group.js +132 -0
- package/dist/production-doctor.d.ts +3 -0
- package/dist/production-doctor.js +176 -0
- package/dist/project-guide.d.ts +19 -1
- package/dist/project-guide.js +119 -20
- package/dist/project-inspector.d.ts +15 -0
- package/dist/project-inspector.js +497 -33
- package/dist/project-state.d.ts +5 -0
- package/dist/project-state.js +8 -0
- package/dist/project.d.ts +203 -19
- package/dist/project.js +3006 -333
- package/dist/recipe-facts.d.ts +127 -0
- package/dist/recipe-facts.js +563 -0
- package/dist/recipe-login.d.ts +63 -0
- package/dist/recipe-login.js +464 -0
- package/dist/recipe-mail.d.ts +21 -0
- package/dist/recipe-mail.js +214 -0
- package/dist/recipe-members.d.ts +35 -0
- package/dist/recipe-members.js +178 -0
- package/dist/recipe-record.d.ts +82 -7
- package/dist/recipe-record.js +226 -22
- package/dist/recipe-reset.d.ts +2 -0
- package/dist/recipe-reset.js +117 -0
- package/dist/recipe-site.d.ts +48 -0
- package/dist/recipe-site.js +396 -0
- package/dist/recipe-step.d.ts +42 -0
- package/dist/recipe-step.js +89 -0
- package/dist/recipe.d.ts +56 -20
- package/dist/recipe.js +703 -131
- package/dist/registry.d.ts +98 -2
- package/dist/registry.js +171 -5
- package/dist/reloadable.d.ts +35 -0
- package/dist/reloadable.js +257 -0
- package/dist/remote-verify.d.ts +31 -0
- package/dist/remote-verify.js +204 -0
- package/dist/server.d.ts +43 -4
- package/dist/server.js +601 -52
- package/dist/settings.d.ts +48 -0
- package/dist/settings.js +168 -0
- package/dist/site-auth.d.ts +39 -0
- package/dist/site-auth.js +195 -0
- package/dist/template-fixes.d.ts +26 -0
- package/dist/template-fixes.js +285 -0
- package/dist/test-reporter.d.ts +49 -0
- package/dist/test-reporter.js +190 -0
- package/dist/transaction.d.ts +47 -0
- package/dist/transaction.js +241 -7
- package/dist/verify-tests.d.ts +98 -0
- package/dist/verify-tests.js +449 -0
- package/dist/verify.d.ts +103 -3
- package/dist/verify.js +272 -21
- package/dist/view-choice.d.ts +58 -0
- package/dist/view-choice.js +131 -0
- package/dist/vite-watch.js +4 -2
- package/package.json +8 -6
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// D-0153: the `fix` of an options error names the commands that repair it. A key the plugin does
|
|
2
|
+
// not know (JSON Schema `additionalProperties`) is removed with `npx dokebi config unset`; a key
|
|
3
|
+
// that moved to another plugin (MOVED_OPTIONS, kept here) says where it went and how to carry its
|
|
4
|
+
// settings over. Fixes name keys and commands, never values.
|
|
5
|
+
/** Options that moved to another plugin, newest last. */
|
|
6
|
+
export const MOVED_OPTIONS = Object.freeze([
|
|
7
|
+
Object.freeze({
|
|
8
|
+
plugin: 'dokebi-web',
|
|
9
|
+
path: Object.freeze(['files']),
|
|
10
|
+
to: 'dokebi-static.mounts',
|
|
11
|
+
package: '@dokebi/static',
|
|
12
|
+
decision: 'D-0147',
|
|
13
|
+
set: 'npx dokebi config set dokebi-static.mounts.<name>.prefix=<prefix> dokebi-static.mounts.<name>.root=<folder> for each mount',
|
|
14
|
+
}),
|
|
15
|
+
]);
|
|
16
|
+
const SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
|
|
17
|
+
/** The option path (segments) an issue points at, with the unknown key for additionalProperties. */
|
|
18
|
+
function issuePath(issue) {
|
|
19
|
+
const parts = issue.instancePath
|
|
20
|
+
.split('/')
|
|
21
|
+
.slice(1)
|
|
22
|
+
.map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
23
|
+
return issue.keyword === 'additionalProperties' && issue.additionalProperty !== undefined
|
|
24
|
+
? [...parts, issue.additionalProperty]
|
|
25
|
+
: parts;
|
|
26
|
+
}
|
|
27
|
+
/** `<id>.<path>` of every key the plugin does not know, when the path can be unset. */
|
|
28
|
+
export function unknownOptionPaths(id, issues) {
|
|
29
|
+
const paths = [];
|
|
30
|
+
let unnamed = false;
|
|
31
|
+
for (const issue of issues) {
|
|
32
|
+
if (issue.keyword !== 'additionalProperties')
|
|
33
|
+
continue;
|
|
34
|
+
const path = issuePath(issue);
|
|
35
|
+
if (issue.additionalProperty === undefined || !path.every((part) => SEGMENT.test(part))) {
|
|
36
|
+
unnamed = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const where = [id, ...path].join('.');
|
|
40
|
+
if (!paths.includes(where))
|
|
41
|
+
paths.push(where);
|
|
42
|
+
}
|
|
43
|
+
return { paths, unnamed };
|
|
44
|
+
}
|
|
45
|
+
/** The moved option a path is, or is inside of. */
|
|
46
|
+
export function movedOption(id, path) {
|
|
47
|
+
return MOVED_OPTIONS.find((moved) => moved.plugin === id &&
|
|
48
|
+
moved.path.length <= path.length &&
|
|
49
|
+
moved.path.every((part, index) => path[index] === part));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The fix of INVALID_PLUGIN_OPTIONS for plugin `id`: the moved keys first (where they went, then
|
|
53
|
+
* show, unset, add, set), then `npx dokebi config unset` for the other unknown keys, then
|
|
54
|
+
* `fallback` (how to correct the remaining issues), which is the whole fix when no key is unknown.
|
|
55
|
+
*/
|
|
56
|
+
export function optionsFix(id, issues, fallback,
|
|
57
|
+
/** The section of dokebi.yaml the commands name (options.logging for the logger host). */
|
|
58
|
+
section = id,
|
|
59
|
+
/** The plugin's options schema: the fix then says what the refused field expects. */
|
|
60
|
+
schema) {
|
|
61
|
+
const expected = expectedShape(schema, issues, section);
|
|
62
|
+
if (expected)
|
|
63
|
+
fallback = `${fallback}; ${expected}`;
|
|
64
|
+
const { paths, unnamed } = unknownOptionPaths(section, issues);
|
|
65
|
+
const parts = [];
|
|
66
|
+
const moved = new Map();
|
|
67
|
+
const other = [];
|
|
68
|
+
for (const where of paths) {
|
|
69
|
+
const match = movedOption(id, where.split('.').slice(1));
|
|
70
|
+
if (match)
|
|
71
|
+
moved.set(match, [match.plugin, ...match.path].join('.'));
|
|
72
|
+
else
|
|
73
|
+
other.push(where);
|
|
74
|
+
}
|
|
75
|
+
for (const [match, from] of moved)
|
|
76
|
+
parts.push(`options.${from} moved to options.${match.to} (${match.decision}): npx dokebi config show ${from} to see what it held, npx dokebi config unset ${from}, npx dokebi add ${match.package} when it is not installed, then ${match.set}`);
|
|
77
|
+
if (other.length)
|
|
78
|
+
parts.push(`remove what ${id} does not know: npx dokebi config unset ${other.join(' ')} (npx dokebi plugins info ${id} --json lists its option keys)`);
|
|
79
|
+
const rest = issues.some((issue) => issue.keyword !== 'additionalProperties') || unnamed;
|
|
80
|
+
if (!parts.length)
|
|
81
|
+
return fallback;
|
|
82
|
+
return rest ? `${parts.join('; ')}; then ${fallback}` : parts.join('; ');
|
|
83
|
+
}
|
|
84
|
+
const node = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
85
|
+
? value
|
|
86
|
+
: undefined;
|
|
87
|
+
/** The schema of the field at an issue's instance path (properties, additionalProperties, items). */
|
|
88
|
+
function fieldSchema(schema, issue) {
|
|
89
|
+
let at = node(schema);
|
|
90
|
+
for (const part of issue.instancePath.split('/').slice(1)) {
|
|
91
|
+
if (!at)
|
|
92
|
+
return undefined;
|
|
93
|
+
const key = part.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
94
|
+
const properties = node(at.properties);
|
|
95
|
+
at =
|
|
96
|
+
properties && Object.hasOwn(properties, key)
|
|
97
|
+
? node(properties[key])
|
|
98
|
+
: /^[0-9]+$/.test(key) && node(at.items)
|
|
99
|
+
? node(at.items)
|
|
100
|
+
: node(at.additionalProperties);
|
|
101
|
+
}
|
|
102
|
+
return at;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* What the first refused field expects, from the schema only (community request D3: `config set`
|
|
106
|
+
* said `(pattern)` and nothing about the shape, so the value could not be corrected). Never the
|
|
107
|
+
* value from the configuration: the description, the allowed values, the type or the bounds.
|
|
108
|
+
*/
|
|
109
|
+
function expectedShape(schema, issues, section) {
|
|
110
|
+
const issue = issues.find((item) => item.keyword !== 'additionalProperties' && item.keyword !== 'required');
|
|
111
|
+
if (!issue)
|
|
112
|
+
return undefined;
|
|
113
|
+
const field = fieldSchema(schema, issue);
|
|
114
|
+
if (!field)
|
|
115
|
+
return undefined;
|
|
116
|
+
const said = [];
|
|
117
|
+
if (typeof field.description === 'string')
|
|
118
|
+
said.push(field.description);
|
|
119
|
+
if (Array.isArray(field.enum))
|
|
120
|
+
said.push(`one of ${field.enum.map((value) => JSON.stringify(value)).join(', ')}`);
|
|
121
|
+
else if (field.type !== undefined && issue.keyword === 'type')
|
|
122
|
+
said.push(`type ${[field.type].flat().join(' or ')}`);
|
|
123
|
+
for (const bound of ['minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems'])
|
|
124
|
+
if (issue.keyword === bound && typeof field[bound] === 'number')
|
|
125
|
+
said.push(`${bound} ${field[bound]}`);
|
|
126
|
+
if (!said.length)
|
|
127
|
+
return undefined;
|
|
128
|
+
const where = `options.${section}${issue.instancePath.replaceAll('/', '.')}`;
|
|
129
|
+
return `${where} expects: ${said.join('; ')}`;
|
|
130
|
+
}
|
package/dist/output.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.4.
|
|
1
|
+
export declare const CLI_VERSION = "0.4.27";
|
|
2
2
|
export declare function envelope(command: string, result: unknown, ok?: boolean): object;
|
|
3
3
|
export declare function failure(command: string, error: unknown, mutationStarted: boolean): object;
|
package/dist/output.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { CliError } from './errors.js';
|
|
2
|
-
export const CLI_VERSION = '0.4.
|
|
2
|
+
export const CLI_VERSION = '0.4.27';
|
|
3
3
|
export function envelope(command, result, ok = true) {
|
|
4
4
|
return { schemaVersion: 1, cliVersion: CLI_VERSION, command, ok, result, error: null };
|
|
5
5
|
}
|
|
6
6
|
export function failure(command, error, mutationStarted) {
|
|
7
7
|
const code = error instanceof CliError ? error.code : 'INTERNAL_ERROR';
|
|
8
8
|
// npm failing in the staging folder never touched the project (D-0065, AI-M01).
|
|
9
|
-
const staged = code === 'NPM_FAILED' &&
|
|
9
|
+
const staged = (code === 'NPM_FAILED' || code === 'REGISTRY_MISMATCH') &&
|
|
10
10
|
error instanceof CliError &&
|
|
11
11
|
error.details?.stage === 'staging';
|
|
12
12
|
const phase = (error instanceof CliError ? error.phase : undefined) ??
|
|
@@ -17,9 +17,9 @@ export function failure(command, error, mutationStarted) {
|
|
|
17
17
|
: code === 'USAGE'
|
|
18
18
|
? 'arguments'
|
|
19
19
|
: 'unknown');
|
|
20
|
-
// An error of the arguments phase is raised before anything is written (or before a
|
|
21
|
-
// transaction commits): the project is untouched.
|
|
22
|
-
const mutation = !mutationStarted || staged || phase === 'arguments'
|
|
20
|
+
// An error of the arguments or plan phase is raised before anything is written (or before a
|
|
21
|
+
// transaction commits): the project is untouched (D-0116).
|
|
22
|
+
const mutation = !mutationStarted || staged || phase === 'arguments' || phase === 'plan'
|
|
23
23
|
? 'none'
|
|
24
24
|
: code === 'COMMIT_FAILED'
|
|
25
25
|
? 'rolled-back'
|
|
@@ -14,8 +14,14 @@ export interface LifecyclePlan extends Omit<InstallPlan, 'configDefaults'> {
|
|
|
14
14
|
requires: string[];
|
|
15
15
|
configChanges: ConfigChange[];
|
|
16
16
|
/**
|
|
17
|
-
* planInstall only (D-0073):
|
|
18
|
-
*
|
|
17
|
+
* planInstall only (D-0073 addendum 2): option paths inside the plugin's own section that must
|
|
18
|
+
* hold a secret. `add` writes a generated value there in dokebi.yaml unless one is there.
|
|
19
|
+
*/
|
|
20
|
+
configSecrets: string[][];
|
|
21
|
+
/**
|
|
22
|
+
* planInstall only, deprecated (D-0073 §2, removed in 0.5): environment variables that must
|
|
23
|
+
* hold a secret. The CLI appends a generated value to the project's .env when neither the
|
|
24
|
+
* environment nor .env has one.
|
|
19
25
|
*/
|
|
20
26
|
envSecrets: string[];
|
|
21
27
|
}
|
|
@@ -41,10 +47,36 @@ export interface LifecycleInput {
|
|
|
41
47
|
}
|
|
42
48
|
/** An environment variable name a plan may ask a generated secret for. */
|
|
43
49
|
export declare const ENV_NAME: RegExp;
|
|
50
|
+
/**
|
|
51
|
+
* Keys a planInstall result may have. Install modules get them as `planKeys` in their context, so
|
|
52
|
+
* a plugin can tell whether this CLI takes `configSecrets` (0.4.3 and later) or only `envSecrets`.
|
|
53
|
+
*/
|
|
54
|
+
export declare const INSTALL_PLAN_KEYS: readonly string[];
|
|
44
55
|
export declare function optionsOf(options: Record<string, unknown>, id: string): Record<string, unknown>;
|
|
45
56
|
export declare function optionPath(value: unknown): string[];
|
|
46
57
|
/** Static `dokebi.install` merged with the module's planInstall or planUpgrade result. */
|
|
47
58
|
export declare function planLifecycle(input: LifecycleInput, phase: 'planInstall' | 'planUpgrade', extra?: Record<string, unknown>): Promise<LifecyclePlan>;
|
|
59
|
+
/** One issue of a plugin's own option rules, shaped like the schema validator's (D-0162). */
|
|
60
|
+
export interface PluginOptionIssue {
|
|
61
|
+
readonly instancePath: string;
|
|
62
|
+
readonly keyword: string;
|
|
63
|
+
readonly message: string;
|
|
64
|
+
}
|
|
65
|
+
/** D-0162: one plugin's verdict: the issues of its own rules, or why they could not run. */
|
|
66
|
+
export type PluginOptionsVerdict = {
|
|
67
|
+
readonly issues: readonly PluginOptionIssue[];
|
|
68
|
+
} | {
|
|
69
|
+
readonly failed: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* D-0162: the plugins' own option rules on their sections, cross-field checks included
|
|
73
|
+
* (dokebi-app's `unknownDomain` must be `reject` or a key of `domains`), which the JSON schema
|
|
74
|
+
* cannot say. Every listed plugin's main entry is imported in ONE permission-restricted child, as
|
|
75
|
+
* install modules are (a child per plugin made `dokebi check` about a second slower on a blog
|
|
76
|
+
* project), and its `createPlugin.checkOptions` gets its section. A plugin without it (a parser
|
|
77
|
+
* function, an older core) is left out of the result; a plugin whose entry fails has `failed`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function checkPluginsOptions(base: string, plugins: readonly PluginManifest[], options: Record<string, unknown>): Promise<Map<string, PluginOptionsVerdict>>;
|
|
48
80
|
export declare function checkPlugin(input: LifecycleInput): Promise<CheckResult>;
|
|
49
81
|
/** Runs check for every plugin and returns results by plugin id. */
|
|
50
82
|
export declare function checkAll(inputs: readonly Omit<LifecycleInput, 'plugins'>[], plugins: readonly PluginManifest[]): Promise<Map<string, CheckResult>>;
|
package/dist/plugin-lifecycle.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
1
4
|
import { debug, debugEnabled, describeError } from './diagnostics.js';
|
|
2
5
|
import { CliError } from './errors.js';
|
|
3
6
|
import { object } from './filesystem.js';
|
|
4
7
|
import { InstallPlanningError, installReadRoots, PHASE_MISSING, runInstallModule, } from './install-module.js';
|
|
5
8
|
import { readInstallPlan } from './install-plan.js';
|
|
6
9
|
import { assertLayout } from './layout.js';
|
|
7
|
-
import { ID } from './manifest.js';
|
|
10
|
+
import { ID, optionsKey } from './manifest.js';
|
|
8
11
|
import { packageDirectory, packageFile } from './workspace.js';
|
|
9
12
|
// Printable single line: no control characters (log/terminal injection) and a short bound.
|
|
10
13
|
const HINT = /^[^\p{Cc}]{1,200}$/u;
|
|
@@ -12,6 +15,28 @@ const CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
|
12
15
|
const KEY = /^[A-Za-z0-9_-]{1,64}$/;
|
|
13
16
|
/** An environment variable name a plan may ask a generated secret for. */
|
|
14
17
|
export const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
18
|
+
/** The most generated secrets one plan may ask for (configSecrets, envSecrets). */
|
|
19
|
+
const MAX_SECRETS = 8;
|
|
20
|
+
/**
|
|
21
|
+
* Keys a planInstall result may have. Install modules get them as `planKeys` in their context, so
|
|
22
|
+
* a plugin can tell whether this CLI takes `configSecrets` (0.4.3 and later) or only `envSecrets`.
|
|
23
|
+
*/
|
|
24
|
+
export const INSTALL_PLAN_KEYS = Object.freeze([
|
|
25
|
+
'directories',
|
|
26
|
+
'files',
|
|
27
|
+
'entry',
|
|
28
|
+
'configDefaults',
|
|
29
|
+
'requires',
|
|
30
|
+
'configSecrets',
|
|
31
|
+
'envSecrets',
|
|
32
|
+
]);
|
|
33
|
+
const UPGRADE_PLAN_KEYS = Object.freeze([
|
|
34
|
+
'directories',
|
|
35
|
+
'files',
|
|
36
|
+
'entry',
|
|
37
|
+
'configChanges',
|
|
38
|
+
'requires',
|
|
39
|
+
]);
|
|
15
40
|
export function optionsOf(options, id) {
|
|
16
41
|
const value = Object.hasOwn(options, id) ? options[id] : undefined;
|
|
17
42
|
return value === undefined || value === null ? {} : object(value, 'plugin options');
|
|
@@ -40,10 +65,28 @@ export function optionPath(value) {
|
|
|
40
65
|
}
|
|
41
66
|
return [...value];
|
|
42
67
|
}
|
|
68
|
+
/** `configSecrets: [{ path }]`: paths inside the plugin's own options, at most 8, no repeats. */
|
|
69
|
+
function configSecrets(value) {
|
|
70
|
+
if (value === undefined)
|
|
71
|
+
return [];
|
|
72
|
+
if (!Array.isArray(value) || value.length > MAX_SECRETS)
|
|
73
|
+
throw new Error('configSecrets');
|
|
74
|
+
const result = [];
|
|
75
|
+
for (const raw of value) {
|
|
76
|
+
const item = object(raw, 'config secret');
|
|
77
|
+
if (Object.keys(item).some((key) => key !== 'path'))
|
|
78
|
+
throw new Error('configSecrets');
|
|
79
|
+
const path = optionPath(item.path);
|
|
80
|
+
if (result.some((other) => other.join('.') === path.join('.')))
|
|
81
|
+
throw new Error('configSecrets');
|
|
82
|
+
result.push(path);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
43
86
|
function envSecrets(value) {
|
|
44
87
|
if (value === undefined)
|
|
45
88
|
return [];
|
|
46
|
-
if (!Array.isArray(value) || value.length >
|
|
89
|
+
if (!Array.isArray(value) || value.length > MAX_SECRETS)
|
|
47
90
|
throw new Error('envSecrets');
|
|
48
91
|
const result = [];
|
|
49
92
|
for (const item of value) {
|
|
@@ -81,7 +124,8 @@ async function run(input, phase, extra = {}) {
|
|
|
81
124
|
// the workspace root with their .env files.
|
|
82
125
|
const readRoots = await installReadRoots(directory);
|
|
83
126
|
return await runInstallModule(path, phase, {
|
|
84
|
-
|
|
127
|
+
// R4CB-6: the plugin's own section: options.logging for the logger host (D-0151).
|
|
128
|
+
options: optionsOf(input.options, optionsKey(input.plugin)),
|
|
85
129
|
plugins: input.plugins.map(({ id, version, packageName }) => ({
|
|
86
130
|
id,
|
|
87
131
|
version,
|
|
@@ -92,24 +136,28 @@ async function run(input, phase, extra = {}) {
|
|
|
92
136
|
}, readRoots);
|
|
93
137
|
}
|
|
94
138
|
catch (error) {
|
|
95
|
-
// The caller sees only the code
|
|
96
|
-
|
|
139
|
+
// The caller sees only the code (a module's message may carry option values, and the module
|
|
140
|
+
// is a package's code); debug keeps the cause, and the module's own words for its author
|
|
141
|
+
// (the error it threw or the end of its stderr, masked), which were dropped before.
|
|
142
|
+
if (debugEnabled())
|
|
97
143
|
debug('Install module could not be run', {
|
|
98
144
|
plugin: input.plugin.id,
|
|
99
145
|
phase,
|
|
100
|
-
error
|
|
146
|
+
...(error instanceof InstallPlanningError
|
|
147
|
+
? { code: error.code, ...(error.detail ? { module: error.detail } : {}) }
|
|
148
|
+
: { error: describeError(error) }),
|
|
101
149
|
});
|
|
102
150
|
throw new CliError('PLUGIN_INSTALL_FAILED', input.plugin.id +
|
|
103
151
|
': ' +
|
|
104
152
|
(error instanceof InstallPlanningError ? error.code : 'INSTALL_MODULE_FAILED'), { details: { plugin: input.plugin.id, phase } });
|
|
105
153
|
}
|
|
106
154
|
}
|
|
107
|
-
function invalidPlan(id) {
|
|
108
|
-
return new CliError('INVALID_MANIFEST', 'Invalid installation plan: ' + id);
|
|
155
|
+
function invalidPlan(id, cause) {
|
|
156
|
+
return new CliError('INVALID_MANIFEST', 'Invalid installation plan: ' + id, cause === undefined ? undefined : { cause });
|
|
109
157
|
}
|
|
110
158
|
/** Static `dokebi.install` merged with the module's planInstall or planUpgrade result. */
|
|
111
159
|
export async function planLifecycle(input, phase, extra = {}) {
|
|
112
|
-
const raw = await run(input, phase, extra);
|
|
160
|
+
const raw = await run(input, phase, phase === 'planInstall' ? { planKeys: [...INSTALL_PLAN_KEYS], ...extra } : extra);
|
|
113
161
|
const plugin = input.plugin;
|
|
114
162
|
const base = {
|
|
115
163
|
files: plugin.files,
|
|
@@ -120,24 +168,32 @@ export async function planLifecycle(input, phase, extra = {}) {
|
|
|
120
168
|
const staticDefaults = base.configDefaults ? [base.configDefaults] : [];
|
|
121
169
|
if (raw === PHASE_MISSING) {
|
|
122
170
|
const { configDefaults: _ignored, ...rest } = base;
|
|
123
|
-
return {
|
|
171
|
+
return {
|
|
172
|
+
...rest,
|
|
173
|
+
defaults: staticDefaults,
|
|
174
|
+
requires: [],
|
|
175
|
+
configChanges: [],
|
|
176
|
+
configSecrets: [],
|
|
177
|
+
envSecrets: [],
|
|
178
|
+
};
|
|
124
179
|
}
|
|
125
180
|
let plan;
|
|
126
181
|
let requires;
|
|
127
182
|
let changes;
|
|
183
|
+
let secretPaths;
|
|
128
184
|
let secrets;
|
|
129
185
|
try {
|
|
130
186
|
plan = object(raw, 'installation plan');
|
|
131
|
-
const allowed = phase === 'planInstall'
|
|
132
|
-
? ['directories', 'files', 'entry', 'configDefaults', 'requires', 'envSecrets']
|
|
133
|
-
: ['directories', 'files', 'entry', 'configChanges', 'requires'];
|
|
187
|
+
const allowed = phase === 'planInstall' ? INSTALL_PLAN_KEYS : UPGRADE_PLAN_KEYS;
|
|
134
188
|
if (Object.keys(plan).some((key) => !allowed.includes(key)))
|
|
135
189
|
throw new Error('keys');
|
|
136
190
|
requires = requirements(plan.requires, plugin.id);
|
|
137
191
|
changes = configChanges(plan.configChanges);
|
|
192
|
+
secretPaths = configSecrets(plan.configSecrets);
|
|
138
193
|
secrets = envSecrets(plan.envSecrets);
|
|
139
194
|
}
|
|
140
|
-
catch {
|
|
195
|
+
catch (error) {
|
|
196
|
+
debug('Install plan refused', { plugin: plugin.id, phase, error: describeError(error) });
|
|
141
197
|
throw invalidPlan(plugin.id);
|
|
142
198
|
}
|
|
143
199
|
if (plan.entry !== undefined && base.entry !== undefined)
|
|
@@ -160,8 +216,8 @@ export async function planLifecycle(input, phase, extra = {}) {
|
|
|
160
216
|
...(plan.entry !== undefined ? { entry: dynamic.entry } : {}),
|
|
161
217
|
}, plugin.id, plugin.hosts);
|
|
162
218
|
}
|
|
163
|
-
catch {
|
|
164
|
-
throw invalidPlan(plugin.id);
|
|
219
|
+
catch (error) {
|
|
220
|
+
throw invalidPlan(plugin.id, error);
|
|
165
221
|
}
|
|
166
222
|
const { configDefaults, ...files } = dynamic;
|
|
167
223
|
return {
|
|
@@ -169,9 +225,132 @@ export async function planLifecycle(input, phase, extra = {}) {
|
|
|
169
225
|
defaults: configDefaults ? [...staticDefaults, configDefaults] : staticDefaults,
|
|
170
226
|
requires,
|
|
171
227
|
configChanges: changes,
|
|
228
|
+
configSecrets: secretPaths,
|
|
172
229
|
envSecrets: secrets,
|
|
173
230
|
};
|
|
174
231
|
}
|
|
232
|
+
const ISSUE_CODE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
233
|
+
/** The package's main ESM entry, relative to its folder: `exports["."]` (import), else `main`. */
|
|
234
|
+
async function mainEntry(directory) {
|
|
235
|
+
const pkg = object(JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')), 'package');
|
|
236
|
+
const exported = pkg.exports;
|
|
237
|
+
const dot = typeof exported === 'string'
|
|
238
|
+
? exported
|
|
239
|
+
: exported && typeof exported === 'object' && !Array.isArray(exported)
|
|
240
|
+
? exported['.']
|
|
241
|
+
: undefined;
|
|
242
|
+
const target = typeof dot === 'string'
|
|
243
|
+
? dot
|
|
244
|
+
: dot && typeof dot === 'object'
|
|
245
|
+
? (dot.import ?? dot.default)
|
|
246
|
+
: pkg.main;
|
|
247
|
+
return typeof target === 'string' ? target.replace(/^\.\//, '') : undefined;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* D-0162: the plugins' own option rules on their sections, cross-field checks included
|
|
251
|
+
* (dokebi-app's `unknownDomain` must be `reject` or a key of `domains`), which the JSON schema
|
|
252
|
+
* cannot say. Every listed plugin's main entry is imported in ONE permission-restricted child, as
|
|
253
|
+
* install modules are (a child per plugin made `dokebi check` about a second slower on a blog
|
|
254
|
+
* project), and its `createPlugin.checkOptions` gets its section. A plugin without it (a parser
|
|
255
|
+
* function, an older core) is left out of the result; a plugin whose entry fails has `failed`.
|
|
256
|
+
*/
|
|
257
|
+
export async function checkPluginsOptions(base, plugins, options) {
|
|
258
|
+
const verdicts = new Map();
|
|
259
|
+
const entries = [];
|
|
260
|
+
for (const plugin of plugins) {
|
|
261
|
+
const directory = await packageDirectory(base, plugin.packageName);
|
|
262
|
+
const entry = await mainEntry(directory);
|
|
263
|
+
if (entry === undefined)
|
|
264
|
+
continue;
|
|
265
|
+
entries.push({
|
|
266
|
+
id: plugin.id,
|
|
267
|
+
path: await packageFile(base, plugin.packageName, entry),
|
|
268
|
+
directory,
|
|
269
|
+
options: optionsOf(options, optionsKey(plugin)),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const first = entries[0];
|
|
273
|
+
if (!first)
|
|
274
|
+
return verdicts;
|
|
275
|
+
const roots = new Set();
|
|
276
|
+
for (const entry of entries)
|
|
277
|
+
for (const root of await installReadRoots(entry.directory))
|
|
278
|
+
roots.add(root);
|
|
279
|
+
let raw;
|
|
280
|
+
try {
|
|
281
|
+
raw = await runInstallModule(first.path, 'checkOptions', {
|
|
282
|
+
// Each entry carries its own section; the shared fields stay empty for this phase.
|
|
283
|
+
options: {},
|
|
284
|
+
plugins: [],
|
|
285
|
+
entries: entries.map((entry) => ({
|
|
286
|
+
url: pathToFileURL(entry.path).href,
|
|
287
|
+
options: entry.options,
|
|
288
|
+
})),
|
|
289
|
+
}, [...roots]);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (debugEnabled())
|
|
293
|
+
debug('Plugin options could not be checked', {
|
|
294
|
+
plugins: entries.map((entry) => entry.id),
|
|
295
|
+
...(error instanceof InstallPlanningError
|
|
296
|
+
? { code: error.code, ...(error.detail ? { module: error.detail } : {}) }
|
|
297
|
+
: { error: describeError(error) }),
|
|
298
|
+
});
|
|
299
|
+
throw new CliError('PLUGIN_OPTIONS_CHECK_FAILED', `Plugin options could not be checked: ${error instanceof InstallPlanningError ? error.code : 'INSTALL_MODULE_FAILED'}`, { cause: error });
|
|
300
|
+
}
|
|
301
|
+
if (!Array.isArray(raw) || raw.length !== entries.length)
|
|
302
|
+
throw new CliError('INVALID_MANIFEST', 'Invalid options check result');
|
|
303
|
+
raw.forEach((item, index) => {
|
|
304
|
+
const entry = entries[index];
|
|
305
|
+
const answer = object(item, 'options check');
|
|
306
|
+
if (answer.ok === false) {
|
|
307
|
+
if (debugEnabled())
|
|
308
|
+
debug('Plugin options could not be checked', {
|
|
309
|
+
plugin: entry.id,
|
|
310
|
+
code: answer.code,
|
|
311
|
+
...(typeof answer.message === 'string' ? { module: answer.message } : {}),
|
|
312
|
+
});
|
|
313
|
+
verdicts.set(entry.id, {
|
|
314
|
+
failed: typeof answer.code === 'string' && ISSUE_CODE.test(answer.code)
|
|
315
|
+
? answer.code
|
|
316
|
+
: 'INSTALL_MODULE_FAILED',
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (answer.missing === true)
|
|
321
|
+
return;
|
|
322
|
+
verdicts.set(entry.id, { issues: optionIssues(answer.result, entry.id) });
|
|
323
|
+
});
|
|
324
|
+
return verdicts;
|
|
325
|
+
}
|
|
326
|
+
/** The issues of one checkOptions result, shaped like the schema validator's. */
|
|
327
|
+
function optionIssues(raw, id) {
|
|
328
|
+
const result = object(raw, 'options check');
|
|
329
|
+
const issues = result.issues;
|
|
330
|
+
if (typeof result.ok !== 'boolean' || !Array.isArray(issues) || issues.length > 100)
|
|
331
|
+
throw new CliError('INVALID_MANIFEST', `Invalid options check result: ${id}`);
|
|
332
|
+
return issues.map((item) => {
|
|
333
|
+
const issue = object(item, 'options issue');
|
|
334
|
+
if (typeof issue.path !== 'string' ||
|
|
335
|
+
issue.path.length > 500 ||
|
|
336
|
+
typeof issue.code !== 'string' ||
|
|
337
|
+
!ISSUE_CODE.test(issue.code) ||
|
|
338
|
+
typeof issue.message !== 'string' ||
|
|
339
|
+
issue.message.length > 500)
|
|
340
|
+
throw new CliError('INVALID_MANIFEST', `Invalid options check result: ${id}`);
|
|
341
|
+
const pointer = issue.path
|
|
342
|
+
.replace(/\[(\d+)\]/g, '.$1')
|
|
343
|
+
.split('.')
|
|
344
|
+
.filter(Boolean)
|
|
345
|
+
.map((part) => part.replaceAll('~', '~0').replaceAll('/', '~1'))
|
|
346
|
+
.join('/');
|
|
347
|
+
return {
|
|
348
|
+
instancePath: pointer ? `/${pointer}` : '',
|
|
349
|
+
keyword: issue.code,
|
|
350
|
+
message: issue.message,
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
175
354
|
export async function checkPlugin(input) {
|
|
176
355
|
const raw = await run(input, 'check', {
|
|
177
356
|
installation: input.installation?.entry ? { entry: input.installation.entry } : {},
|
package/dist/plugin-options.d.ts
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PluginManifest } from './manifest.js';
|
|
2
|
+
import { type OptionIssueLike } from './option-fixes.js';
|
|
2
3
|
export interface OptionsCheck {
|
|
3
4
|
schema: Record<string, unknown> | null;
|
|
4
|
-
errors:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
errors: OptionIssueLike[];
|
|
6
|
+
}
|
|
7
|
+
/** What checks options against a schema (ConfigValidator of @dokebi/config; a fake in tests). */
|
|
8
|
+
export interface SchemaChecker {
|
|
9
|
+
check(schema: unknown, options: unknown): Promise<{
|
|
10
|
+
errors: OptionIssueLike[];
|
|
7
11
|
}>;
|
|
8
12
|
}
|
|
9
13
|
export declare class PluginOptionsValidator {
|
|
10
14
|
#private;
|
|
11
15
|
constructor(options?: {
|
|
12
16
|
allErrors?: boolean;
|
|
17
|
+
validator?: SchemaChecker;
|
|
13
18
|
});
|
|
19
|
+
/**
|
|
20
|
+
* D-0155: the options of an application settings section against the schema file beside its
|
|
21
|
+
* declaration (src/shared/settings/<section>.schema.json); no schema yet checks nothing.
|
|
22
|
+
*/
|
|
23
|
+
checkSettings(root: string, section: string, options: unknown): Promise<OptionsCheck>;
|
|
24
|
+
/**
|
|
25
|
+
* A settings section against a schema in hand (`settings sync` checks the options against the
|
|
26
|
+
* schema it is about to write, before the file is on disk).
|
|
27
|
+
*/
|
|
28
|
+
checkSettingsWith(section: string, schema: Record<string, unknown>, options: unknown): Promise<OptionsCheck>;
|
|
14
29
|
check(root: string, plugin: PluginManifest, options: unknown): Promise<OptionsCheck>;
|
|
15
30
|
}
|
|
16
31
|
export declare function checkPluginOptions(root: string, plugin: PluginManifest, options: unknown): Promise<OptionsCheck>;
|