@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,42 @@
|
|
|
1
|
+
import { type ConfigShowWarning } from './config-show.js';
|
|
2
|
+
export declare const EXAMPLE_FILE = "dokebi.example.yaml";
|
|
3
|
+
export interface ConfigDiffResult {
|
|
4
|
+
/** The settings file compared: dokebi.yaml, or the one DOKEBI_CONFIG names (M50). */
|
|
5
|
+
readonly file: string;
|
|
6
|
+
readonly example: typeof EXAMPLE_FILE;
|
|
7
|
+
/** The asked section, or null for every section. */
|
|
8
|
+
readonly section: string | null;
|
|
9
|
+
readonly inSync: boolean;
|
|
10
|
+
readonly onlyInConfig: string[];
|
|
11
|
+
readonly onlyInExample: string[];
|
|
12
|
+
readonly different: string[];
|
|
13
|
+
readonly secrets: string[];
|
|
14
|
+
readonly exampleSecrets: string[];
|
|
15
|
+
/**
|
|
16
|
+
* R4CA-2: what config show warns of either file, once per section (a schema that cannot be read:
|
|
17
|
+
* its text counts as secret in both files, so its sample values are listed in exampleSecrets;
|
|
18
|
+
* a section nothing declares). Absent when there is none.
|
|
19
|
+
*/
|
|
20
|
+
readonly warnings?: ConfigShowWarning[];
|
|
21
|
+
}
|
|
22
|
+
/** A path only in dokebi.yaml and its value without secrets (`sampleAdditions`). */
|
|
23
|
+
export interface SampleAddition {
|
|
24
|
+
/** Section first: `['dokebi-web', 'port']`. */
|
|
25
|
+
readonly path: readonly string[];
|
|
26
|
+
readonly value: unknown;
|
|
27
|
+
}
|
|
28
|
+
export declare function configDiff(root: string, section?: string): Promise<ConfigDiffResult>;
|
|
29
|
+
/**
|
|
30
|
+
* What `config diff --write` copies into the sample: every path only in dokebi.yaml with the
|
|
31
|
+
* secrets inside it dropped (a secret leaf; a list with a secret item is left out whole).
|
|
32
|
+
*/
|
|
33
|
+
export declare function sampleAdditions(root: string, configSource: string, exampleSource: string, section?: string,
|
|
34
|
+
/** The settings file `configSource` is (dokebi.yaml, or the one DOKEBI_CONFIG names). */
|
|
35
|
+
file?: string): Promise<{
|
|
36
|
+
result: ConfigDiffResult;
|
|
37
|
+
additions: SampleAddition[];
|
|
38
|
+
changes: SampleAddition[];
|
|
39
|
+
prunable: string[][];
|
|
40
|
+
}>;
|
|
41
|
+
/** The result for people: paths only. */
|
|
42
|
+
export declare function configDiffText(result: ConfigDiffResult): string;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// `dokebi config diff [<plugin-id>]` (AI-M45): which options dokebi.yaml and dokebi.example.yaml do
|
|
2
|
+
// not share, by path only. An agent keeping the sample in step dumped both files through
|
|
3
|
+
// loadConfig to compare them and printed a database password and two signing secrets into its
|
|
4
|
+
// log. This compares inside the CLI and never puts a value into the result, errors or debug logs.
|
|
5
|
+
//
|
|
6
|
+
// - onlyInConfig / onlyInExample: option paths one file sets and the other does not (a subtree
|
|
7
|
+
// the other lacks is one path, a list is one value);
|
|
8
|
+
// - different: paths both set to different non-secret values (a sample value is allowed to
|
|
9
|
+
// differ; the list says where);
|
|
10
|
+
// - secrets: secret paths of dokebi.yaml (as config show hides them, and a subtree only in
|
|
11
|
+
// dokebi.yaml that holds nothing else). They are never compared and never count as missing
|
|
12
|
+
// from the sample, which must not hold them;
|
|
13
|
+
// - exampleSecrets: secret paths that hold a value in dokebi.example.yaml (a leak to remove).
|
|
14
|
+
//
|
|
15
|
+
// `--write` (AI-M46: a builder kept the sample in step with a script of its own, since config set
|
|
16
|
+
// writes dokebi.yaml only) copies the paths only in dokebi.yaml into the sample, without their
|
|
17
|
+
// secrets: the same rule that keeps them out of the comparison drops them from the copy.
|
|
18
|
+
// `--write --values` (Studio v2, 10-31) also writes dokebi.yaml's value of every `different` path
|
|
19
|
+
// into the sample, the same way (a secret is never compared, so never copied).
|
|
20
|
+
import { readFile } from 'node:fs/promises';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { configShow, hideCredentials, readSettings, secretOption, } from './config-show.js';
|
|
23
|
+
import { CONFIG_FILE, missingConfig, parseProjectConfig } from './configuration.js';
|
|
24
|
+
import { debug, isMissing } from './diagnostics.js';
|
|
25
|
+
import { envFileWarnings } from './env-file.js';
|
|
26
|
+
import { CliError } from './errors.js';
|
|
27
|
+
export const EXAMPLE_FILE = 'dokebi.example.yaml';
|
|
28
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
/** A value worth calling set: not null, not '', not an empty list or mapping. */
|
|
30
|
+
function holdsValue(value) {
|
|
31
|
+
if (value === null || value === undefined || value === '')
|
|
32
|
+
return false;
|
|
33
|
+
if (Array.isArray(value))
|
|
34
|
+
return value.some(holdsValue);
|
|
35
|
+
if (isObject(value))
|
|
36
|
+
return Object.values(value).some(holdsValue);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function same(a, b) {
|
|
40
|
+
if (Array.isArray(a) || Array.isArray(b) || isObject(a) || isObject(b))
|
|
41
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
42
|
+
return a === b;
|
|
43
|
+
}
|
|
44
|
+
// Text with credentials (core's maskCredentials: `scheme://user:password@`, `Password=…`) is a
|
|
45
|
+
// secret wherever it is; a `<password>` placeholder is the documented shape (hideCredentials).
|
|
46
|
+
const URL_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
47
|
+
/** `scheme://user@`: a URL whose user has no password (group 1 is the scheme and slashes). */
|
|
48
|
+
const URL_USER_ONLY = /^([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+@/i;
|
|
49
|
+
/** A placeholder the scaffold writes for a value to fill in: `<set … here …>`. */
|
|
50
|
+
const PLACEHOLDER = /^<[^<>]*>$/;
|
|
51
|
+
/** Text anywhere in the value (what a secret name hides; numbers and booleans show). */
|
|
52
|
+
function holdsText(value) {
|
|
53
|
+
if (typeof value === 'string')
|
|
54
|
+
return value !== '';
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return value.some(holdsText);
|
|
57
|
+
if (isObject(value))
|
|
58
|
+
return Object.values(value).some(holdsText);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
export async function configDiff(root, section) {
|
|
62
|
+
// M50: the settings the app reads (DOKEBI_CONFIG, else dokebi.yaml), against the one sample.
|
|
63
|
+
const settings = await readSettings(root);
|
|
64
|
+
return (await compare(root, settings.source, await readProjectFile(root, EXAMPLE_FILE), section, settings.target.file)).result;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* What `config diff --write` copies into the sample: every path only in dokebi.yaml with the
|
|
68
|
+
* secrets inside it dropped (a secret leaf; a list with a secret item is left out whole).
|
|
69
|
+
*/
|
|
70
|
+
export async function sampleAdditions(root, configSource, exampleSource, section,
|
|
71
|
+
/** The settings file `configSource` is (dokebi.yaml, or the one DOKEBI_CONFIG names). */
|
|
72
|
+
file = CONFIG_FILE) {
|
|
73
|
+
return compare(root, configSource, exampleSource, section, file);
|
|
74
|
+
}
|
|
75
|
+
async function compare(root, configSource, exampleSource, section, file) {
|
|
76
|
+
const config = parseProjectConfig(configSource).options;
|
|
77
|
+
const example = parseProjectConfig(exampleSource).options;
|
|
78
|
+
// Schema-marked and name-marked secrets of dokebi.yaml, exactly as config show hides them.
|
|
79
|
+
const shown = await configShow(root, undefined, { source: configSource, file });
|
|
80
|
+
// The sample's own secrets too (schema-marked ones in a subtree or list item only it has).
|
|
81
|
+
const sampleShown = await configShow(root, undefined, { source: exampleSource });
|
|
82
|
+
// A section nobody declares hides every text value (fail closed in config show); in the
|
|
83
|
+
// sample that would call each old value a secret, so only declared sections count here.
|
|
84
|
+
// Sections without a schema in either file: there the name of a key is all there is to go on.
|
|
85
|
+
const unknownSections = new Set([...(shown.warnings ?? []), ...(sampleShown.warnings ?? [])]
|
|
86
|
+
.filter((warning) => warning.code !== 'OPTIONS_SCHEMA_UNREADABLE')
|
|
87
|
+
.map((warning) => warning.plugin));
|
|
88
|
+
const blind = new Set((sampleShown.warnings ?? [])
|
|
89
|
+
.filter((warning) => warning.code !== 'OPTIONS_SCHEMA_UNREADABLE')
|
|
90
|
+
.map((warning) => warning.plugin));
|
|
91
|
+
const sampleSecretPaths = new Set(sampleShown.secrets.filter((path) => !blind.has(path.split('.')[0] ?? '')));
|
|
92
|
+
// dokebi.yaml is judged fail closed (what config show hides is never compared or copied).
|
|
93
|
+
const secretPaths = new Set([...shown.secrets, ...sampleSecretPaths]);
|
|
94
|
+
// R4CA-1: a value of the sample is a leaked secret by the sample's own rule: in a section
|
|
95
|
+
// nobody declares (one a removed plugin left, D-0153) by name and credentials only. Before,
|
|
96
|
+
// the fail-closed paths of dokebi.yaml's same section called every plain sample value there a
|
|
97
|
+
// secret with a value (`remove them`).
|
|
98
|
+
for (const path of shown.secrets)
|
|
99
|
+
if (!unknownSections.has(path.split('.')[0] ?? ''))
|
|
100
|
+
sampleSecretPaths.add(path);
|
|
101
|
+
const isSecret = (path, value, paths = secretPaths) => {
|
|
102
|
+
for (let length = path.length; length > 0; length--)
|
|
103
|
+
if (paths.has(path.slice(0, length).join('.')))
|
|
104
|
+
return true;
|
|
105
|
+
const key = path[path.length - 1];
|
|
106
|
+
// Core's name rule, as config show applies it: `transientLogMaxKeys: 256` is a count, and a
|
|
107
|
+
// list or object under a secret name is a secret by its text. A section with a schema
|
|
108
|
+
// already went through config show, which honours `plain`.
|
|
109
|
+
if (key !== undefined &&
|
|
110
|
+
!/^\d+$/.test(key) &&
|
|
111
|
+
unknownSections.has(path[0] ?? '') &&
|
|
112
|
+
secretOption(key, value) &&
|
|
113
|
+
(Array.isArray(value) || isObject(value) ? holdsText(value) : holdsValue(value)))
|
|
114
|
+
return true;
|
|
115
|
+
return typeof value === 'string' && hideCredentials(value) !== undefined;
|
|
116
|
+
};
|
|
117
|
+
const sampleSecret = (path, value) => isSecret(path, value, sampleSecretPaths);
|
|
118
|
+
// A sample URL without credentials is the documented placeholder of a secret URL, not a leak;
|
|
119
|
+
// nor is the scaffold's `<set the postgres URL here or run npx dokebi dev-infra>`, also as the
|
|
120
|
+
// items of a list (a list or object leaks when one of its values does).
|
|
121
|
+
const leaks = (value) => {
|
|
122
|
+
if (Array.isArray(value))
|
|
123
|
+
return value.some(leaks);
|
|
124
|
+
if (isObject(value))
|
|
125
|
+
return Object.values(value).some(leaks);
|
|
126
|
+
if (!holdsValue(value))
|
|
127
|
+
return false;
|
|
128
|
+
if (typeof value !== 'string')
|
|
129
|
+
return true;
|
|
130
|
+
const text = value.trim();
|
|
131
|
+
// A user without a password (`postgres://blog@host`) is the documented shape too; core
|
|
132
|
+
// masks that user in logs and config show hides it, but it is no password in the sample.
|
|
133
|
+
const withoutUser = text.replace(URL_USER_ONLY, '$1');
|
|
134
|
+
return !((URL_SCHEME.test(text) && hideCredentials(withoutUser) === undefined) ||
|
|
135
|
+
PLACEHOLDER.test(text));
|
|
136
|
+
};
|
|
137
|
+
const DROP = Symbol('secret');
|
|
138
|
+
/** The value without its secrets; DROP when nothing but secrets (or nothing) is left. */
|
|
139
|
+
const scrub = (value, path) => {
|
|
140
|
+
if (isSecret(path, value))
|
|
141
|
+
return DROP;
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
const items = value.map((item, index) => scrub(item, [...path, String(index)]));
|
|
144
|
+
// Dropping an item would change what the list means: a list with a secret item is left out.
|
|
145
|
+
return items.includes(DROP) ? DROP : items;
|
|
146
|
+
}
|
|
147
|
+
if (isObject(value)) {
|
|
148
|
+
const kept = {};
|
|
149
|
+
for (const [key, item] of Object.entries(value)) {
|
|
150
|
+
const clean = scrub(item, [...path, key]);
|
|
151
|
+
if (clean !== DROP)
|
|
152
|
+
kept[key] = clean;
|
|
153
|
+
}
|
|
154
|
+
return Object.keys(kept).length || !Object.keys(value).length ? kept : DROP;
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
};
|
|
158
|
+
const additions = [];
|
|
159
|
+
/** `different` paths with dokebi.yaml's value without secrets (`--write --values`). */
|
|
160
|
+
const changes = [];
|
|
161
|
+
/** Secrets with a value in the sample, as paths (what `--prune` always removes). */
|
|
162
|
+
const prunable = [];
|
|
163
|
+
/**
|
|
164
|
+
* Secrets inside a sample value that is compared whole (a list, an object only the sample has):
|
|
165
|
+
* logging.targets.<n>.token of a network log target. The value itself was not a secret.
|
|
166
|
+
*/
|
|
167
|
+
const leakedInside = (value, path) => {
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
for (const [index, item] of value.entries())
|
|
170
|
+
leakedInside(item, [...path, String(index)]);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (isObject(value)) {
|
|
174
|
+
for (const [key, item] of Object.entries(value))
|
|
175
|
+
leakedInside(item, [...path, key]);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (sampleSecret(path, value) && leaks(value)) {
|
|
179
|
+
exampleSecrets.push(path.join('.'));
|
|
180
|
+
prunable.push(path);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const onlyInConfig = [];
|
|
184
|
+
const onlyInExample = [];
|
|
185
|
+
const different = [];
|
|
186
|
+
const secrets = [];
|
|
187
|
+
const exampleSecrets = [];
|
|
188
|
+
const walk = (left, right, path, hasLeft, hasRight) => {
|
|
189
|
+
const at = path.join('.');
|
|
190
|
+
const secretLeft = hasLeft && isSecret(path, left);
|
|
191
|
+
const secretRight = hasRight && isSecret(path, right);
|
|
192
|
+
if (secretLeft || secretRight) {
|
|
193
|
+
if (hasLeft)
|
|
194
|
+
secrets.push(at);
|
|
195
|
+
if (hasRight && sampleSecret(path, right) && leaks(right)) {
|
|
196
|
+
exampleSecrets.push(at);
|
|
197
|
+
prunable.push(path);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (isObject(left) && isObject(right)) {
|
|
202
|
+
for (const key of new Set([...Object.keys(left), ...Object.keys(right)]))
|
|
203
|
+
walk(left[key], right[key], [...path, key], Object.hasOwn(left, key), Object.hasOwn(right, key));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (hasLeft && !hasRight) {
|
|
207
|
+
// A subtree that holds nothing but secrets is a secret too: never missing from the sample.
|
|
208
|
+
const value = scrub(left, path);
|
|
209
|
+
if (value === DROP)
|
|
210
|
+
secrets.push(at);
|
|
211
|
+
else {
|
|
212
|
+
onlyInConfig.push(at);
|
|
213
|
+
additions.push({ path, value });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (hasRight && !hasLeft)
|
|
217
|
+
onlyInExample.push(at);
|
|
218
|
+
else if (!same(left, right)) {
|
|
219
|
+
different.push(at);
|
|
220
|
+
// A list or mapping is copied without its secrets, or not at all when one item is secret.
|
|
221
|
+
const value = scrub(left, path);
|
|
222
|
+
if (value !== DROP)
|
|
223
|
+
changes.push({ path, value });
|
|
224
|
+
}
|
|
225
|
+
if (hasRight && (Array.isArray(right) || isObject(right)))
|
|
226
|
+
leakedInside(right, path);
|
|
227
|
+
};
|
|
228
|
+
for (const name of new Set([...Object.keys(config), ...Object.keys(example)])) {
|
|
229
|
+
if (section !== undefined && name !== section)
|
|
230
|
+
continue;
|
|
231
|
+
walk(config[name], example[name], [name], Object.hasOwn(config, name), Object.hasOwn(example, name));
|
|
232
|
+
}
|
|
233
|
+
const warnings = new Map();
|
|
234
|
+
for (const warning of [
|
|
235
|
+
// R4CA-3: what the project's .env sets that neither the app nor this command reads.
|
|
236
|
+
...envFileWarnings(root).map(({ code, path, hint }) => ({ code, path, hint })),
|
|
237
|
+
...(shown.warnings ?? []),
|
|
238
|
+
...(sampleShown.warnings ?? []),
|
|
239
|
+
])
|
|
240
|
+
if (section === undefined || warning.plugin === undefined || warning.plugin === section)
|
|
241
|
+
warnings.set(`${warning.code} ${warning.plugin ?? warning.path}`, warning);
|
|
242
|
+
const result = {
|
|
243
|
+
file,
|
|
244
|
+
example: EXAMPLE_FILE,
|
|
245
|
+
section: section ?? null,
|
|
246
|
+
inSync: !onlyInConfig.length && !onlyInExample.length && !exampleSecrets.length,
|
|
247
|
+
onlyInConfig,
|
|
248
|
+
onlyInExample,
|
|
249
|
+
different,
|
|
250
|
+
secrets,
|
|
251
|
+
exampleSecrets,
|
|
252
|
+
...(warnings.size ? { warnings: [...warnings.values()] } : {}),
|
|
253
|
+
};
|
|
254
|
+
debug('Configuration compared with the sample', {
|
|
255
|
+
section: section ?? null,
|
|
256
|
+
onlyInConfig: onlyInConfig.length,
|
|
257
|
+
onlyInExample: onlyInExample.length,
|
|
258
|
+
different: different.length,
|
|
259
|
+
secrets: secrets.length,
|
|
260
|
+
exampleSecrets: exampleSecrets.length,
|
|
261
|
+
});
|
|
262
|
+
return { result, additions, changes, prunable };
|
|
263
|
+
}
|
|
264
|
+
async function readProjectFile(root, file) {
|
|
265
|
+
try {
|
|
266
|
+
return await readFile(join(root, file), 'utf8');
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (isMissing(error))
|
|
270
|
+
throw file === CONFIG_FILE
|
|
271
|
+
? missingConfig(root, `No ${file}: run this in a Dokebi project (the folder with dokebi.yaml)`)
|
|
272
|
+
: new CliError('EXAMPLE_MISSING', `No ${file} to compare with (it is the sample of dokebi.yaml without secrets)`, { phase: 'arguments', details: { root, file } });
|
|
273
|
+
throw new CliError('READ_FAILED', `Cannot read ${file}`, { cause: error });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** The result for people: paths only. */
|
|
277
|
+
export function configDiffText(result) {
|
|
278
|
+
const list = (label, paths) => paths.length ? [`${label} [${paths.length}]:`, ...paths.map((path) => ` ${path}`)] : [];
|
|
279
|
+
const lines = [
|
|
280
|
+
`# ${result.file} against ${result.example}${result.section ? ` (${result.section})` : ''}: ${result.inSync ? 'in step' : 'not in step'}; values are never shown`,
|
|
281
|
+
...list(`only in ${result.file}`, result.onlyInConfig),
|
|
282
|
+
...list(`only in ${result.example}`, result.onlyInExample),
|
|
283
|
+
...list('set in both to different values', result.different),
|
|
284
|
+
...list(`secrets of ${result.file} (never compared; the sample must not hold them)`, result.secrets),
|
|
285
|
+
...list(`secrets with a value in ${result.example} (remove them)`, result.exampleSecrets),
|
|
286
|
+
...(result.warnings ?? []).map((warning) => `# warning ${warning.code}: ${warning.hint}`),
|
|
287
|
+
];
|
|
288
|
+
return `${lines.join('\n')}\n`;
|
|
289
|
+
}
|
package/dist/config-edit.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Document } from 'yaml';
|
|
1
2
|
import type { ConfigChange } from './plugin-lifecycle.js';
|
|
2
3
|
export interface SetOption {
|
|
3
4
|
readonly path: string[];
|
|
@@ -9,9 +10,41 @@ export interface Warning {
|
|
|
9
10
|
}
|
|
10
11
|
/** Parses one `--set <path>=<value>` argument. Values are JSON when parseable, otherwise text. */
|
|
11
12
|
export declare function parseSetOption(argument: string): SetOption;
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* A path whose list items are named turned into item indexes: `targets.studio.token` is
|
|
15
|
+
* `targets.<index>.token` of the item of `targets` whose `name` is studio (else, for an item
|
|
16
|
+
* without a name, whose `type` is). An index stays an index. `undefined` when a named item or an
|
|
17
|
+
* index is not there (callers decide: an error for set, not set for unset). D-0151 follow-up:
|
|
18
|
+
* the targets of options.logging are a list.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveItems(document: Document, path: readonly string[]): string[] | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* A path with each list index named the way resolveItems finds the item (`name`, else `type` of
|
|
23
|
+
* an item without a name), so the same item is found in another file whose list has another
|
|
24
|
+
* order. `undefined` when an indexed item has neither (it cannot be found elsewhere for sure);
|
|
25
|
+
* the path as given when the item is not in this file.
|
|
26
|
+
*/
|
|
27
|
+
export declare function namedItems(source: string, id: string, path: readonly string[]): string[] | undefined;
|
|
13
28
|
export declare function applySetOptions(source: string, id: string, sets: readonly SetOption[]): string;
|
|
14
|
-
|
|
29
|
+
/** The value at `<id>.<path>` of a dokebi.yaml source (list items by name or index), or undefined. */
|
|
30
|
+
export declare function optionAtPath(source: string, id: string, path: readonly string[]): unknown;
|
|
31
|
+
/**
|
|
32
|
+
* `dokebi config unset` (D-0153): removes `options.<id>.<path>` for each path that is set, and
|
|
33
|
+
* the mappings this leaves empty below the plugin section (the section itself stays). Returns
|
|
34
|
+
* the new source and the paths that were set; a source without any of them comes back as it is.
|
|
35
|
+
* Values are never read out.
|
|
36
|
+
*/
|
|
37
|
+
export declare function applyUnsetOptions(source: string, id: string, paths: readonly (readonly string[])[]): {
|
|
38
|
+
source: string;
|
|
39
|
+
removed: string[][];
|
|
40
|
+
};
|
|
41
|
+
/** A manifest's change, or `set` (the CLI's own: replaces a value, `config diff --values`). */
|
|
42
|
+
export type ConfigEdit = ConfigChange | {
|
|
43
|
+
readonly op: 'set';
|
|
44
|
+
readonly path: readonly string[];
|
|
45
|
+
readonly value: unknown;
|
|
46
|
+
};
|
|
47
|
+
export declare function applyConfigChanges(source: string, id: string, changes: readonly ConfigEdit[]): string;
|
|
15
48
|
/**
|
|
16
49
|
* Removes `options.<host>.apps.<app>` (D-0061 per-app settings of the apps host, D-0070) and the
|
|
17
50
|
* `apps` map when it is left empty; a source without that entry is returned unchanged. The host
|
package/dist/config-edit.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
-
import { isMap, isNode, isScalar, parseDocument } from 'yaml';
|
|
2
|
+
import { isMap, isNode, isScalar, isSeq, parseDocument } from 'yaml';
|
|
3
|
+
import { validateDefaults } from './config-values.js';
|
|
3
4
|
import { parseProjectConfig, renderConfig } from './configuration.js';
|
|
4
5
|
import { CliError } from './errors.js';
|
|
5
|
-
import { validateDefaults } from './merge-install-defaults.js';
|
|
6
6
|
const SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7
7
|
const FORBIDDEN = new Set(['__proto__', 'constructor', 'prototype', '<<']);
|
|
8
|
-
const SECRET = /(password|passwd|secret|token|credential|api.?key|private.?key)/i;
|
|
9
8
|
/** Parses one `--set <path>=<value>` argument. Values are JSON when parseable, otherwise text. */
|
|
10
9
|
export function parseSetOption(argument) {
|
|
11
10
|
const index = argument.indexOf('=');
|
|
@@ -20,25 +19,93 @@ export function parseSetOption(argument) {
|
|
|
20
19
|
value = JSON.parse(text);
|
|
21
20
|
}
|
|
22
21
|
catch {
|
|
23
|
-
|
|
22
|
+
// dokebi-check: allow empty-catch a value that is not JSON is the plain text the user typed
|
|
24
23
|
}
|
|
25
24
|
try {
|
|
26
25
|
validateDefaults({ value });
|
|
27
26
|
}
|
|
28
|
-
catch {
|
|
29
|
-
throw new CliError('INVALID_SET', 'Invalid --set value');
|
|
27
|
+
catch (cause) {
|
|
28
|
+
throw new CliError('INVALID_SET', 'Invalid --set value', { cause });
|
|
30
29
|
}
|
|
31
30
|
return { path, value };
|
|
32
31
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const INDEX = /^(?:0|[1-9][0-9]{0,5})$/;
|
|
33
|
+
/**
|
|
34
|
+
* A path whose list items are named turned into item indexes: `targets.studio.token` is
|
|
35
|
+
* `targets.<index>.token` of the item of `targets` whose `name` is studio (else, for an item
|
|
36
|
+
* without a name, whose `type` is). An index stays an index. `undefined` when a named item or an
|
|
37
|
+
* index is not there (callers decide: an error for set, not set for unset). D-0151 follow-up:
|
|
38
|
+
* the targets of options.logging are a list.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveItems(document, path) {
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const segment of path) {
|
|
43
|
+
const node = document.getIn(result, true);
|
|
44
|
+
if (!isSeq(node)) {
|
|
45
|
+
result.push(segment);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
let index = INDEX.test(segment)
|
|
49
|
+
? Number(segment)
|
|
50
|
+
: node.items.findIndex((item) => isMap(item) && item.get('name') === segment);
|
|
51
|
+
if (index < 0)
|
|
52
|
+
index = node.items.findIndex((item) => isMap(item) && !item.has('name') && item.get('type') === segment);
|
|
53
|
+
if (index < 0 || index >= node.items.length)
|
|
54
|
+
return undefined;
|
|
55
|
+
result.push(String(index));
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A path with each list index named the way resolveItems finds the item (`name`, else `type` of
|
|
61
|
+
* an item without a name), so the same item is found in another file whose list has another
|
|
62
|
+
* order. `undefined` when an indexed item has neither (it cannot be found elsewhere for sure);
|
|
63
|
+
* the path as given when the item is not in this file.
|
|
64
|
+
*/
|
|
65
|
+
export function namedItems(source, id, path) {
|
|
66
|
+
const document = parseDocument(source, { version: '1.2' });
|
|
67
|
+
const named = [];
|
|
68
|
+
const walked = ['options', id];
|
|
69
|
+
for (const segment of path) {
|
|
70
|
+
const node = document.getIn(walked, true);
|
|
71
|
+
if (isSeq(node) && INDEX.test(segment)) {
|
|
72
|
+
const item = node.items[Number(segment)];
|
|
73
|
+
if (item === undefined)
|
|
74
|
+
return [...path];
|
|
75
|
+
const name = isMap(item) ? item.get('name') : undefined;
|
|
76
|
+
const type = isMap(item) && !item.has('name') ? item.get('type') : undefined;
|
|
77
|
+
if (typeof name === 'string')
|
|
78
|
+
named.push(name);
|
|
79
|
+
else if (typeof type === 'string')
|
|
80
|
+
named.push(type);
|
|
81
|
+
else
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
named.push(segment);
|
|
86
|
+
walked.push(segment);
|
|
87
|
+
}
|
|
88
|
+
return named;
|
|
89
|
+
}
|
|
90
|
+
/** The names of the items of the list a path goes through (for the error of a missing one). */
|
|
91
|
+
function itemNames(document, path) {
|
|
92
|
+
for (let length = path.length - 1; length > 0; length--) {
|
|
93
|
+
const prefix = resolveItems(document, path.slice(0, length));
|
|
94
|
+
const node = prefix ? document.getIn(prefix, true) : undefined;
|
|
95
|
+
if (!isSeq(node))
|
|
96
|
+
continue;
|
|
97
|
+
return node.items.map((item, index) => isMap(item) ? String(item.get('name') ?? item.get('type') ?? index) : String(index));
|
|
98
|
+
}
|
|
99
|
+
return [];
|
|
37
100
|
}
|
|
38
101
|
function ensureMap(document, path, code) {
|
|
39
102
|
for (let length = 1; length <= path.length; length++) {
|
|
40
103
|
const current = path.slice(0, length);
|
|
41
104
|
const node = document.getIn(current, true);
|
|
105
|
+
// An item of a list (an index resolveItems found) is crossed, never created.
|
|
106
|
+
const next = path[length];
|
|
107
|
+
if (isSeq(node) && next !== undefined && INDEX.test(next) && Number(next) < node.items.length)
|
|
108
|
+
continue;
|
|
42
109
|
if (!document.hasIn(current) || (isScalar(node) && node.value === null))
|
|
43
110
|
document.setIn(current, document.createNode({}));
|
|
44
111
|
else if (!isMap(node))
|
|
@@ -68,12 +135,77 @@ export function applySetOptions(source, id, sets) {
|
|
|
68
135
|
}
|
|
69
136
|
return edit(source, id, (document) => {
|
|
70
137
|
for (const set of sets) {
|
|
71
|
-
const
|
|
138
|
+
const wanted = ['options', id, ...set.path];
|
|
139
|
+
const path = resolveItems(document, wanted);
|
|
140
|
+
if (!path)
|
|
141
|
+
throw new CliError('INVALID_SET', `No list item at ${[id, ...set.path].join('.')}`, {
|
|
142
|
+
details: {
|
|
143
|
+
path: [id, ...set.path].join('.'),
|
|
144
|
+
items: itemNames(document, wanted),
|
|
145
|
+
fix: 'name an item of the list by its name (else its type) or by its index, as npx dokebi config show lists them',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
72
148
|
ensureMap(document, path.slice(0, -1), 'INVALID_SET');
|
|
73
149
|
document.setIn(path, document.createNode(set.value));
|
|
74
150
|
}
|
|
75
151
|
});
|
|
76
152
|
}
|
|
153
|
+
/** The value at `<id>.<path>` of a dokebi.yaml source (list items by name or index), or undefined. */
|
|
154
|
+
export function optionAtPath(source, id, path) {
|
|
155
|
+
const document = parseDocument(source, { version: '1.2' });
|
|
156
|
+
const resolved = resolveItems(document, ['options', id, ...path]);
|
|
157
|
+
if (!resolved || !document.hasIn(resolved))
|
|
158
|
+
return undefined;
|
|
159
|
+
const value = document.getIn(resolved, false);
|
|
160
|
+
return isNode(value) ? value.toJSON() : value;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* `dokebi config unset` (D-0153): removes `options.<id>.<path>` for each path that is set, and
|
|
164
|
+
* the mappings this leaves empty below the plugin section (the section itself stays). Returns
|
|
165
|
+
* the new source and the paths that were set; a source without any of them comes back as it is.
|
|
166
|
+
* Values are never read out.
|
|
167
|
+
*/
|
|
168
|
+
export function applyUnsetOptions(source, id, paths) {
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
for (const path of paths) {
|
|
171
|
+
const key = path.join('.');
|
|
172
|
+
if (seen.has(key))
|
|
173
|
+
throw new CliError('INVALID_SET', 'Repeated config unset path');
|
|
174
|
+
seen.add(key);
|
|
175
|
+
}
|
|
176
|
+
const document = parseDocument(source, { version: '1.2' });
|
|
177
|
+
// List items may be named (resolveItems); one that is not there is a path that is not set.
|
|
178
|
+
const present = [];
|
|
179
|
+
for (const path of paths) {
|
|
180
|
+
const full = resolveItems(document, ['options', id, ...path]);
|
|
181
|
+
if (full && document.hasIn(full))
|
|
182
|
+
present.push({ path: [...path], full });
|
|
183
|
+
}
|
|
184
|
+
const removed = present.map((item) => item.path);
|
|
185
|
+
if (!removed.length)
|
|
186
|
+
return { source, removed };
|
|
187
|
+
// Later list items first, so removing one never shifts the index of another. One item named
|
|
188
|
+
// twice (`targets.studio` and `targets.3`) is removed once: removing it again would take the
|
|
189
|
+
// item that moved into its place.
|
|
190
|
+
const order = [
|
|
191
|
+
...new Map(present.map((item) => [item.full.join('\0'), item.full])).values(),
|
|
192
|
+
].sort((a, b) => b.join('\0').localeCompare(a.join('\0'), 'en', { numeric: true }));
|
|
193
|
+
const result = edit(source, id, (target) => {
|
|
194
|
+
for (const full of order) {
|
|
195
|
+
// A path below one removed earlier in this call is gone already.
|
|
196
|
+
if (!target.hasIn(full))
|
|
197
|
+
continue;
|
|
198
|
+
target.deleteIn(full);
|
|
199
|
+
for (let length = full.length - 1; length > 2; length--) {
|
|
200
|
+
const parent = target.getIn(full.slice(0, length), true);
|
|
201
|
+
if (!isMap(parent) || parent.items.length)
|
|
202
|
+
break;
|
|
203
|
+
target.deleteIn(full.slice(0, length));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return { source: result, removed };
|
|
208
|
+
}
|
|
77
209
|
export function applyConfigChanges(source, id, changes) {
|
|
78
210
|
if (!changes.length)
|
|
79
211
|
return source;
|
|
@@ -98,9 +230,9 @@ export function applyConfigChanges(source, id, changes) {
|
|
|
98
230
|
if (document.hasIn(path))
|
|
99
231
|
document.deleteIn(path);
|
|
100
232
|
}
|
|
101
|
-
else if (change.op === 'default' && change.path) {
|
|
233
|
+
else if ((change.op === 'default' || change.op === 'set') && change.path) {
|
|
102
234
|
const path = [...base, ...change.path];
|
|
103
|
-
if (document.hasIn(path))
|
|
235
|
+
if (change.op === 'default' && document.hasIn(path))
|
|
104
236
|
continue;
|
|
105
237
|
validateDefaults({ value: change.value });
|
|
106
238
|
ensureMap(document, path.slice(0, -1), 'UPGRADE_CONFIG_CONFLICT');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The option keys of a validator issue's instancePath (`/connections/main` → connections, main). */
|
|
2
|
+
export declare function issuePathOf(instancePath: string): string[];
|
|
3
|
+
/**
|
|
4
|
+
* Whether the option at `path` of a section is a secret: marked so in its schema (`writeOnly`,
|
|
5
|
+
* `x-dokebi-secret: true`), or named like one by core's rule unless the schema marks it plain
|
|
6
|
+
* (`x-dokebi-secret: false`), as `config show` hides it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function secretInSchema(schema: unknown, path: readonly string[]): boolean;
|
|
9
|
+
/** A sample value still to fill in (`<set the postgres URL here or run npx dokebi dev-infra>`). */
|
|
10
|
+
export declare function isPlaceholder(value: unknown): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The option paths whose value is a placeholder of the sample (`<set the postgres URL here or
|
|
13
|
+
* run npx dokebi dev-infra>`, `<password>`), as `section.key.key`; list items by index.
|
|
14
|
+
*/
|
|
15
|
+
export declare function placeholderPaths(options: Record<string, unknown>): string[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Pure parts of `dokebi config init` (D-0163): which missing options are secrets the CLI may
|
|
2
|
+
// generate, and which sample values are placeholders still to fill.
|
|
3
|
+
import { secretOption } from './config-show.js';
|
|
4
|
+
/** The option keys of a validator issue's instancePath (`/connections/main` → connections, main). */
|
|
5
|
+
export function issuePathOf(instancePath) {
|
|
6
|
+
return instancePath
|
|
7
|
+
.split('/')
|
|
8
|
+
.slice(1)
|
|
9
|
+
.map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
10
|
+
}
|
|
11
|
+
function propertySchema(node, key) {
|
|
12
|
+
if (!node || typeof node !== 'object')
|
|
13
|
+
return undefined;
|
|
14
|
+
const properties = node.properties;
|
|
15
|
+
if (!properties || typeof properties !== 'object')
|
|
16
|
+
return undefined;
|
|
17
|
+
const found = properties[key];
|
|
18
|
+
return found && typeof found === 'object' ? found : undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether the option at `path` of a section is a secret: marked so in its schema (`writeOnly`,
|
|
22
|
+
* `x-dokebi-secret: true`), or named like one by core's rule unless the schema marks it plain
|
|
23
|
+
* (`x-dokebi-secret: false`), as `config show` hides it.
|
|
24
|
+
*/
|
|
25
|
+
export function secretInSchema(schema, path) {
|
|
26
|
+
let node = schema;
|
|
27
|
+
for (const key of path)
|
|
28
|
+
node = propertySchema(node, key);
|
|
29
|
+
const marked = node;
|
|
30
|
+
if (marked?.['x-dokebi-secret'] === false)
|
|
31
|
+
return false;
|
|
32
|
+
if (marked?.writeOnly === true || marked?.['x-dokebi-secret'] === true)
|
|
33
|
+
return true;
|
|
34
|
+
return secretOption(path.at(-1) ?? '', 'text');
|
|
35
|
+
}
|
|
36
|
+
/** A sample value still to fill in (`<set the postgres URL here or run npx dokebi dev-infra>`). */
|
|
37
|
+
export function isPlaceholder(value) {
|
|
38
|
+
return typeof value === 'string' && /^<[^<>]+>$/.test(value.trim());
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The option paths whose value is a placeholder of the sample (`<set the postgres URL here or
|
|
42
|
+
* run npx dokebi dev-infra>`, `<password>`), as `section.key.key`; list items by index.
|
|
43
|
+
*/
|
|
44
|
+
export function placeholderPaths(options) {
|
|
45
|
+
const found = [];
|
|
46
|
+
const walk = (value, path) => {
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
if (isPlaceholder(value))
|
|
49
|
+
found.push(path.join('.'));
|
|
50
|
+
}
|
|
51
|
+
else if (Array.isArray(value)) {
|
|
52
|
+
for (const [index, item] of value.entries())
|
|
53
|
+
walk(item, [...path, `${index}`]);
|
|
54
|
+
}
|
|
55
|
+
else if (value && typeof value === 'object')
|
|
56
|
+
for (const [key, item] of Object.entries(value))
|
|
57
|
+
walk(item, [...path, key]);
|
|
58
|
+
};
|
|
59
|
+
walk(options, []);
|
|
60
|
+
return found;
|
|
61
|
+
}
|