@dokebi/cli 0.4.1 → 0.4.26
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 +262 -57
- package/README.md +35 -10
- package/catalog.json +2519 -257
- 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 +527 -35
- 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 +2 -0
- package/dist/check/shared-state-rules.js +114 -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 +307 -31
- 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-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/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 +29 -3
- package/dist/install-module.js +142 -13
- 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 +106 -27
- 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 +59 -4
- package/dist/npm-client.js +220 -16
- 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 +206 -20
- 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/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 +481 -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 +3016 -336
- 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/server.d.ts +50 -4
- package/dist/server.js +790 -103
- 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 +67 -0
- package/dist/transaction.js +428 -46
- package/dist/verify-tests.d.ts +98 -0
- package/dist/verify-tests.js +449 -0
- package/dist/verify.d.ts +113 -4
- package/dist/verify.js +326 -28
- 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
package/dist/plugin-options.js
CHANGED
|
@@ -1,10 +1,134 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
1
4
|
import { ConfigValidator, loadSchema } from '@dokebi/config';
|
|
5
|
+
import { debug, describeError } from './diagnostics.js';
|
|
2
6
|
import { CliError } from './errors.js';
|
|
7
|
+
import { optionsKey } from './manifest.js';
|
|
8
|
+
import { optionsFix } from './option-fixes.js';
|
|
9
|
+
import { readSettingsSchema } from './settings.js';
|
|
3
10
|
import { packageFile } from './workspace.js';
|
|
11
|
+
/** The smallest schema the validator compiles: whether it works at all. */
|
|
12
|
+
const TRIVIAL_SCHEMA = Object.freeze({
|
|
13
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
14
|
+
type: 'object',
|
|
15
|
+
});
|
|
16
|
+
/** The version of the @dokebi/config this CLI validates with ('unknown' when it cannot be read). */
|
|
17
|
+
function configVersion() {
|
|
18
|
+
try {
|
|
19
|
+
// ESM resolution: @dokebi/config exports only an `import` condition.
|
|
20
|
+
let directory = dirname(fileURLToPath(import.meta.resolve('@dokebi/config')));
|
|
21
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
22
|
+
const file = join(directory, 'package.json');
|
|
23
|
+
if (existsSync(file)) {
|
|
24
|
+
const pkg = JSON.parse(readFileSync(file, 'utf8'));
|
|
25
|
+
if (pkg.name === '@dokebi/config')
|
|
26
|
+
return String(pkg.version);
|
|
27
|
+
}
|
|
28
|
+
directory = dirname(directory);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
debug('The @dokebi/config version could not be read', { error: describeError(error) });
|
|
33
|
+
}
|
|
34
|
+
return 'unknown';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The first line of an error and of each cause under it, at most 500 characters: why a schema
|
|
38
|
+
* failed, never a value. @dokebi/config's ConfigError says only "Invalid or unsupported options
|
|
39
|
+
* schema"; ajv's words (`strict mode: unknown keyword: "x-dokebi-…"`) are its cause (R5C-1).
|
|
40
|
+
*/
|
|
41
|
+
function reasonOf(error) {
|
|
42
|
+
const lines = [];
|
|
43
|
+
let current = error ?? 'unknown';
|
|
44
|
+
for (let depth = 0; depth < 4 && current !== undefined && current !== null; depth++) {
|
|
45
|
+
const text = current instanceof Error ? `${current.name}: ${current.message}` : String(current);
|
|
46
|
+
lines.push(text.split('\n')[0] ?? '');
|
|
47
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
48
|
+
}
|
|
49
|
+
return lines.join(' <- caused by ').slice(0, 500);
|
|
50
|
+
}
|
|
4
51
|
export class PluginOptionsValidator {
|
|
5
52
|
#validator;
|
|
53
|
+
/** Whether the validator itself works (a trivial schema compiles); asked once, on a failure. */
|
|
54
|
+
#works;
|
|
6
55
|
constructor(options = {}) {
|
|
7
|
-
this.#validator =
|
|
56
|
+
this.#validator =
|
|
57
|
+
options.validator ??
|
|
58
|
+
new ConfigValidator({ ...(options.allErrors ? { allErrors: true } : {}) });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* AI-M45: a schema that cannot be read or compiled, with the reason. When even a trivial schema
|
|
62
|
+
* does not compile, the validator itself is broken (a damaged node_modules: ajv's files gone)
|
|
63
|
+
* and every plugin would be reported: the fix then says to restore the installation.
|
|
64
|
+
*/
|
|
65
|
+
async #failure(cause, subject, reinstall) {
|
|
66
|
+
// A schema the validator takes (it refuses one without the 2020-12 `$schema`): with
|
|
67
|
+
// `{ type: 'object' }` alone the probe always failed, so every schema that did not compile
|
|
68
|
+
// was reported as a damaged installation with the fix `npm ci`.
|
|
69
|
+
this.#works ??= this.#validator.check(TRIVIAL_SCHEMA, {}).then(() => true, () => false);
|
|
70
|
+
const validatorBroken = !(await this.#works);
|
|
71
|
+
// A schema of a newer release than this CLI's @dokebi/config (2026-10-28: an older CLI met
|
|
72
|
+
// x-dokebi-secret and x-dokebi-alias-of and was read as a broken installation).
|
|
73
|
+
const keyword = /unknown keyword:? "?(x-dokebi-[\w-]+)/.exec(reasonOf(cause))?.[1];
|
|
74
|
+
if (!validatorBroken && keyword !== undefined) {
|
|
75
|
+
const version = configVersion();
|
|
76
|
+
return new CliError('INVALID_OPTIONS_SCHEMA', `The options schema of ${String(subject.plugin ?? subject.section)} uses ${keyword}, which the @dokebi/config ${version} of this CLI does not know`, {
|
|
77
|
+
cause,
|
|
78
|
+
details: {
|
|
79
|
+
...subject,
|
|
80
|
+
reason: reasonOf(cause),
|
|
81
|
+
keyword,
|
|
82
|
+
config: version,
|
|
83
|
+
validator: false,
|
|
84
|
+
fix: `the schema comes from a newer release than this CLI: run the @dokebi/cli of the same release as the installed plugins (npm install the matching @dokebi/cli in the project, then npx dokebi), and keep @dokebi/core, @dokebi/config and @dokebi/cli of one release; the installation is not damaged`,
|
|
85
|
+
},
|
|
86
|
+
phase: 'plan',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return new CliError('INVALID_OPTIONS_SCHEMA', validatorBroken
|
|
90
|
+
? 'The options schema validator of @dokebi/config cannot load (the installation is damaged)'
|
|
91
|
+
: `The options schema of ${String(subject.plugin ?? subject.section)} cannot be read or compiled`, {
|
|
92
|
+
cause,
|
|
93
|
+
details: {
|
|
94
|
+
...subject,
|
|
95
|
+
reason: reasonOf(cause),
|
|
96
|
+
validator: validatorBroken,
|
|
97
|
+
fix: validatorBroken
|
|
98
|
+
? 'restore node_modules with npm ci (or npm install): the JSON Schema validator (ajv, a dependency of @dokebi/config) is missing files, so no options can be checked; every section is reported until then'
|
|
99
|
+
: reinstall,
|
|
100
|
+
},
|
|
101
|
+
phase: 'plan',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* D-0155: the options of an application settings section against the schema file beside its
|
|
106
|
+
* declaration (src/shared/settings/<section>.schema.json); no schema yet checks nothing.
|
|
107
|
+
*/
|
|
108
|
+
async checkSettings(root, section, options) {
|
|
109
|
+
const schema = await readSettingsSchema(root, section);
|
|
110
|
+
if (!schema)
|
|
111
|
+
return { schema: null, errors: [] };
|
|
112
|
+
try {
|
|
113
|
+
const checked = await this.#validator.check(schema, options);
|
|
114
|
+
return { schema, errors: checked.errors };
|
|
115
|
+
}
|
|
116
|
+
catch (cause) {
|
|
117
|
+
throw await this.#failure(cause, { section }, `run npx dokebi settings sync to write src/shared/settings/${section}.schema.json again`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A settings section against a schema in hand (`settings sync` checks the options against the
|
|
122
|
+
* schema it is about to write, before the file is on disk).
|
|
123
|
+
*/
|
|
124
|
+
async checkSettingsWith(section, schema, options) {
|
|
125
|
+
try {
|
|
126
|
+
const checked = await this.#validator.check(schema, options);
|
|
127
|
+
return { schema, errors: checked.errors };
|
|
128
|
+
}
|
|
129
|
+
catch (cause) {
|
|
130
|
+
throw await this.#failure(cause, { section }, `correct the declaration of ${section} so core renders a valid schema, then npx dokebi settings sync`);
|
|
131
|
+
}
|
|
8
132
|
}
|
|
9
133
|
async check(root, plugin, options) {
|
|
10
134
|
if (!plugin.optionsSchema)
|
|
@@ -16,7 +140,7 @@ export class PluginOptionsValidator {
|
|
|
16
140
|
return { schema, errors: checked.errors };
|
|
17
141
|
}
|
|
18
142
|
catch (cause) {
|
|
19
|
-
throw
|
|
143
|
+
throw await this.#failure(cause, { plugin: plugin.id, version: plugin.version, schema: plugin.optionsSchema }, `reinstall ${plugin.packageName} (npm install), or use another version of it; details.reason says why`);
|
|
20
144
|
}
|
|
21
145
|
}
|
|
22
146
|
}
|
|
@@ -24,12 +148,20 @@ export async function checkPluginOptions(root, plugin, options) {
|
|
|
24
148
|
return new PluginOptionsValidator().check(root, plugin, options);
|
|
25
149
|
}
|
|
26
150
|
export async function validatePluginOptions(root, plugins, options) {
|
|
27
|
-
|
|
151
|
+
// Every issue (D-0153): the fix names all unknown keys in one config unset.
|
|
152
|
+
const validator = new PluginOptionsValidator({ allErrors: true });
|
|
28
153
|
for (const plugin of plugins) {
|
|
29
|
-
|
|
154
|
+
// D-0151: the logger host's options are options.logging.
|
|
155
|
+
const key = optionsKey(plugin);
|
|
156
|
+
const result = await validator.check(root, plugin, Object.hasOwn(options, key) ? (options[key] ?? {}) : {});
|
|
30
157
|
if (result.errors.length)
|
|
31
|
-
throw new CliError('INVALID_PLUGIN_OPTIONS', plugin.id, {
|
|
32
|
-
details: {
|
|
158
|
+
throw new CliError('INVALID_PLUGIN_OPTIONS', `The options of ${plugin.id} do not match its schema: options.${key}${(result.errors[0]?.instancePath ?? '').replaceAll('/', '.')} (${result.errors[0]?.keyword ?? 'invalid'})`, {
|
|
159
|
+
details: {
|
|
160
|
+
plugin: plugin.id,
|
|
161
|
+
errors: result.errors,
|
|
162
|
+
fix: optionsFix(plugin.id, result.errors, `correct options.${key} in dokebi.yaml (npx dokebi plugins info ${plugin.id} --json lists the option keys)`, key, result.schema),
|
|
163
|
+
},
|
|
164
|
+
phase: 'plan',
|
|
33
165
|
});
|
|
34
166
|
}
|
|
35
167
|
}
|
package/dist/plugins-command.js
CHANGED
|
@@ -109,9 +109,14 @@ export async function pluginsList(root, options) {
|
|
|
109
109
|
export async function pluginsSearch(query) {
|
|
110
110
|
return searchCatalog(await loadCatalog(), query);
|
|
111
111
|
}
|
|
112
|
-
function unknownPackage(catalog, key
|
|
112
|
+
function unknownPackage(catalog, key,
|
|
113
|
+
/** An installed package the key names by its short name (AI-M04 F7), with its guide's path. */
|
|
114
|
+
installed) {
|
|
113
115
|
const search = searchCatalog(catalog, key);
|
|
114
|
-
|
|
116
|
+
const fix = installed
|
|
117
|
+
? `npx dokebi plugins doc ${installed.package} --json (or read ${installed.path})`
|
|
118
|
+
: undefined;
|
|
119
|
+
return new CliError('UNKNOWN_PLUGIN', `No Dokebi package "${key}"${installed ? `; did you mean ${installed.package}? ${fix}` : ''}`, {
|
|
115
120
|
phase: 'arguments',
|
|
116
121
|
details: {
|
|
117
122
|
query: key,
|
|
@@ -121,10 +126,44 @@ function unknownPackage(catalog, key) {
|
|
|
121
126
|
summary: entry.summary,
|
|
122
127
|
instead: entry.instead,
|
|
123
128
|
})),
|
|
124
|
-
|
|
129
|
+
...(installed && fix ? { package: installed.package, path: installed.path, fix } : {}),
|
|
130
|
+
hint: fix ?? 'npx dokebi plugins search <words> --json',
|
|
125
131
|
},
|
|
126
132
|
});
|
|
127
133
|
}
|
|
134
|
+
/** The guide path of an installed package's package.json (`dokebi.documentation`, else AI.md). */
|
|
135
|
+
function documentationPath(packageName, pkg) {
|
|
136
|
+
const manifest = pkg.dokebi;
|
|
137
|
+
const documentation = typeof manifest?.documentation === 'string' ? manifest.documentation : 'AI.md';
|
|
138
|
+
return `node_modules/${packageName}/${documentation}`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* An id that is not in the catalog but names an installed @dokebi package by its short name
|
|
142
|
+
* (`dokebi-core`, `dokebi-cli`: core, CLI and drivers have no plugin id), AI-M04 F7.
|
|
143
|
+
*/
|
|
144
|
+
async function installedByShortName(root, key) {
|
|
145
|
+
const short = key
|
|
146
|
+
.trim()
|
|
147
|
+
.toLowerCase()
|
|
148
|
+
.replace(/^@dokebi\//, '')
|
|
149
|
+
.replace(/^dokebi-/, '');
|
|
150
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(short))
|
|
151
|
+
return undefined;
|
|
152
|
+
const packageName = `@dokebi/${short}`;
|
|
153
|
+
try {
|
|
154
|
+
const pkg = parseObject(await readFile(await packageFile(root, packageName, 'package.json'), 'utf8'), packageName);
|
|
155
|
+
return { package: packageName, path: documentationPath(packageName, pkg) };
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (!isMissing(error) && !(error instanceof CliError))
|
|
159
|
+
throw error;
|
|
160
|
+
debug('plugins doc: no installed package by that short name', {
|
|
161
|
+
package: packageName,
|
|
162
|
+
error: describeError(error),
|
|
163
|
+
});
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
128
167
|
export async function pluginsInfo(root, key) {
|
|
129
168
|
const catalog = await loadCatalog();
|
|
130
169
|
const item = findPackage(catalog, key);
|
|
@@ -146,7 +185,7 @@ export async function pluginsDoc(root, key) {
|
|
|
146
185
|
const item = findPackage(catalog, key);
|
|
147
186
|
const packageName = item?.package ?? (key.startsWith('@') ? key : undefined);
|
|
148
187
|
if (!packageName)
|
|
149
|
-
throw unknownPackage(catalog, key);
|
|
188
|
+
throw unknownPackage(catalog, key, await installedByShortName(root, key));
|
|
150
189
|
let pkg;
|
|
151
190
|
try {
|
|
152
191
|
pkg = parseObject(await readFile(await packageFile(root, packageName, 'package.json'), 'utf8'), packageName);
|
|
@@ -167,9 +206,7 @@ export async function pluginsDoc(root, key) {
|
|
|
167
206
|
},
|
|
168
207
|
});
|
|
169
208
|
}
|
|
170
|
-
const
|
|
171
|
-
const documentation = typeof manifest?.documentation === 'string' ? manifest.documentation : 'AI.md';
|
|
172
|
-
const path = `node_modules/${packageName}/${documentation}`;
|
|
209
|
+
const path = documentationPath(packageName, pkg);
|
|
173
210
|
let content;
|
|
174
211
|
try {
|
|
175
212
|
content = await readFile(await installedPath(root, path), 'utf8');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Spawn with `detached: PROCESS_GROUPS`: on POSIX the child then leads a new process group. */
|
|
2
|
+
export declare const PROCESS_GROUPS: boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Signals the process group of `pid` (POSIX; the child leads it) or the child alone (Windows):
|
|
5
|
+
* true when the signal was sent, false when nothing of the group is left (ESRCH, the expected
|
|
6
|
+
* case after a clean run) or it could not be signalled (logged at debug: the caller decides).
|
|
7
|
+
*/
|
|
8
|
+
export declare function killGroup(pid: number | undefined, signal: NodeJS.Signals): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Whether the process group `pid` led still has a member (POSIX). A group outlives its leader
|
|
11
|
+
* while a member runs, and its id is not reused as long as it exists, so a member found here
|
|
12
|
+
* right after the leader exited is one the leader started.
|
|
13
|
+
*/
|
|
14
|
+
export declare function groupAlive(pid: number): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Ends what is left of the process group of `pid` after its leader exited: processes the leader
|
|
17
|
+
* started and did not stop (a helper, a server holding a port). SIGTERM first, SIGKILL after
|
|
18
|
+
* `graceMs`. `none` when nothing was left (the usual case), else how they were ended.
|
|
19
|
+
*/
|
|
20
|
+
export declare function endLeftovers(pid: number, graceMs: number): Promise<'none' | 'terminated' | 'killed'>;
|
|
21
|
+
/** The signals that end a command (Ctrl+C, an agent's command time limit, a closed terminal). */
|
|
22
|
+
export declare const TERMINATING: readonly ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
23
|
+
/**
|
|
24
|
+
* Holds off a terminating signal while a step must clean up first: the first SIGINT, SIGTERM or
|
|
25
|
+
* SIGHUP is recorded (`signal()`) instead of ending the process at once, so the step can see it,
|
|
26
|
+
* undo what it started and throw. `release()` removes the handlers and, when a signal came and
|
|
27
|
+
* nobody else listens for it, ends this process as that signal would have.
|
|
28
|
+
*/
|
|
29
|
+
export declare function holdTermination(): {
|
|
30
|
+
signal(): NodeJS.Signals | undefined;
|
|
31
|
+
release(): void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A detached group no longer gets the terminal's Ctrl+C (nor a SIGTERM or SIGHUP sent to this
|
|
35
|
+
* process). While the child runs, this passes such a signal on to its group and then lets this
|
|
36
|
+
* process end as it would have without the listener. Returns the function that stops forwarding;
|
|
37
|
+
* call it once the child closed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function forwardTermination(pid: number | undefined): () => void;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Child commands that may start their own children (npm run, Vite, tsc, test runners) run in a
|
|
2
|
+
// process group of their own, so a time limit can end all of them. Killing only the direct child
|
|
3
|
+
// leaves the grandchildren running, and as they still hold the stdout/stderr pipes the child's
|
|
4
|
+
// 'close' event (and the step waiting for it) would not come until they end by themselves.
|
|
5
|
+
import { debug, describeError } from './diagnostics.js';
|
|
6
|
+
/** Spawn with `detached: PROCESS_GROUPS`: on POSIX the child then leads a new process group. */
|
|
7
|
+
export const PROCESS_GROUPS = process.platform !== 'win32';
|
|
8
|
+
/**
|
|
9
|
+
* Signals the process group of `pid` (POSIX; the child leads it) or the child alone (Windows):
|
|
10
|
+
* true when the signal was sent, false when nothing of the group is left (ESRCH, the expected
|
|
11
|
+
* case after a clean run) or it could not be signalled (logged at debug: the caller decides).
|
|
12
|
+
*/
|
|
13
|
+
export function killGroup(pid, signal) {
|
|
14
|
+
if (pid === undefined)
|
|
15
|
+
return false;
|
|
16
|
+
try {
|
|
17
|
+
if (PROCESS_GROUPS)
|
|
18
|
+
process.kill(-pid, signal);
|
|
19
|
+
else
|
|
20
|
+
process.kill(pid, signal);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code !== 'ESRCH')
|
|
25
|
+
debug('Could not signal the process group', {
|
|
26
|
+
pid,
|
|
27
|
+
signal,
|
|
28
|
+
error: describeError(error),
|
|
29
|
+
});
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether the process group `pid` led still has a member (POSIX). A group outlives its leader
|
|
35
|
+
* while a member runs, and its id is not reused as long as it exists, so a member found here
|
|
36
|
+
* right after the leader exited is one the leader started.
|
|
37
|
+
*/
|
|
38
|
+
export function groupAlive(pid) {
|
|
39
|
+
if (!PROCESS_GROUPS)
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
process.kill(-pid, 0);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// ESRCH: nobody is left. EPERM: a member this user may not signal (not one the app started).
|
|
47
|
+
if (error.code !== 'ESRCH')
|
|
48
|
+
debug('Could not probe the process group', { pid, error: describeError(error) });
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Ends what is left of the process group of `pid` after its leader exited: processes the leader
|
|
54
|
+
* started and did not stop (a helper, a server holding a port). SIGTERM first, SIGKILL after
|
|
55
|
+
* `graceMs`. `none` when nothing was left (the usual case), else how they were ended.
|
|
56
|
+
*/
|
|
57
|
+
export async function endLeftovers(pid, graceMs) {
|
|
58
|
+
if (!groupAlive(pid))
|
|
59
|
+
return 'none';
|
|
60
|
+
debug('Processes are left in the group of an exited process; ending them', { pid, graceMs });
|
|
61
|
+
killGroup(pid, 'SIGTERM');
|
|
62
|
+
const until = performance.now() + graceMs;
|
|
63
|
+
while (performance.now() < until) {
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
65
|
+
if (!groupAlive(pid))
|
|
66
|
+
return 'terminated';
|
|
67
|
+
}
|
|
68
|
+
killGroup(pid, 'SIGKILL');
|
|
69
|
+
debug('Processes left in the group did not end on SIGTERM; killed', { pid, graceMs });
|
|
70
|
+
return 'killed';
|
|
71
|
+
}
|
|
72
|
+
/** The signals that end a command (Ctrl+C, an agent's command time limit, a closed terminal). */
|
|
73
|
+
export const TERMINATING = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
74
|
+
/**
|
|
75
|
+
* Holds off a terminating signal while a step must clean up first: the first SIGINT, SIGTERM or
|
|
76
|
+
* SIGHUP is recorded (`signal()`) instead of ending the process at once, so the step can see it,
|
|
77
|
+
* undo what it started and throw. `release()` removes the handlers and, when a signal came and
|
|
78
|
+
* nobody else listens for it, ends this process as that signal would have.
|
|
79
|
+
*/
|
|
80
|
+
export function holdTermination() {
|
|
81
|
+
let received;
|
|
82
|
+
const handlers = new Map();
|
|
83
|
+
for (const signal of TERMINATING) {
|
|
84
|
+
const handler = () => {
|
|
85
|
+
if (received)
|
|
86
|
+
return;
|
|
87
|
+
received = signal;
|
|
88
|
+
debug('A termination signal arrived; cleaning up before ending', { signal });
|
|
89
|
+
};
|
|
90
|
+
handlers.set(signal, handler);
|
|
91
|
+
process.on(signal, handler);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
signal: () => received,
|
|
95
|
+
release: () => {
|
|
96
|
+
for (const [signal, handler] of handlers)
|
|
97
|
+
process.off(signal, handler);
|
|
98
|
+
handlers.clear();
|
|
99
|
+
if (received && process.listenerCount(received) === 0)
|
|
100
|
+
process.kill(process.pid, received);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A detached group no longer gets the terminal's Ctrl+C (nor a SIGTERM or SIGHUP sent to this
|
|
106
|
+
* process). While the child runs, this passes such a signal on to its group and then lets this
|
|
107
|
+
* process end as it would have without the listener. Returns the function that stops forwarding;
|
|
108
|
+
* call it once the child closed.
|
|
109
|
+
*/
|
|
110
|
+
export function forwardTermination(pid) {
|
|
111
|
+
const handlers = new Map();
|
|
112
|
+
const stop = () => {
|
|
113
|
+
for (const [signal, handler] of handlers)
|
|
114
|
+
process.off(signal, handler);
|
|
115
|
+
handlers.clear();
|
|
116
|
+
};
|
|
117
|
+
if (!PROCESS_GROUPS || pid === undefined)
|
|
118
|
+
return stop;
|
|
119
|
+
for (const signal of TERMINATING) {
|
|
120
|
+
const handler = () => {
|
|
121
|
+
debug('Passing a termination signal on to the child process group', { pid, signal });
|
|
122
|
+
killGroup(pid, signal);
|
|
123
|
+
stop();
|
|
124
|
+
// Nobody else listens: the signal's default action (ending this process) applies again.
|
|
125
|
+
if (process.listenerCount(signal) === 0)
|
|
126
|
+
process.kill(process.pid, signal);
|
|
127
|
+
};
|
|
128
|
+
handlers.set(signal, handler);
|
|
129
|
+
process.on(signal, handler);
|
|
130
|
+
}
|
|
131
|
+
return stop;
|
|
132
|
+
}
|
package/dist/project-guide.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Catalog } from './catalog.js';
|
|
1
2
|
import type { PluginManifest } from './manifest.js';
|
|
2
3
|
import type { RequiredPlugin } from './required-plugins.js';
|
|
3
4
|
/** Markers of the CLI-maintained list of installed packages in AGENTS.md (D-0065). */
|
|
@@ -7,6 +8,12 @@ export declare const AGENTS_FILE = "AGENTS.md";
|
|
|
7
8
|
export declare const CLAUDE_FILE = "CLAUDE.md";
|
|
8
9
|
/** CLAUDE.md of a new project: Claude Code reads AGENTS.md through this import line. */
|
|
9
10
|
export declare const CLAUDE_GUIDE = "@AGENTS.md\n";
|
|
11
|
+
/**
|
|
12
|
+
* Markers of the project's own block in AGENTS.md (D-0077 G): the people of the project (or their
|
|
13
|
+
* tools) write rules there; the CLI never changes what is between them and doctor never judges it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PROJECT_START = "<!-- dokebi:project:start -->";
|
|
16
|
+
export declare const PROJECT_END = "<!-- dokebi:project:end -->";
|
|
10
17
|
/** One row of the installed list: a package, its version and its AI guide path. */
|
|
11
18
|
export interface InstalledEntry {
|
|
12
19
|
readonly id: string;
|
|
@@ -32,5 +39,16 @@ export declare function installedBlockEdited(text: string, entries: readonly Ins
|
|
|
32
39
|
* of order (the developer removed them; doctor reports GUIDE_BLOCK_MISSING and the file stays).
|
|
33
40
|
*/
|
|
34
41
|
export declare function replaceInstalledBlock(text: string, block: string): string | undefined;
|
|
42
|
+
/** The project block as init writes it (and as it is added to an older AGENTS.md). */
|
|
43
|
+
export declare function projectBlock(): string;
|
|
44
|
+
/**
|
|
45
|
+
* The state of the project block in an AGENTS.md text: `ok` (one start, then one end, outside
|
|
46
|
+
* the installed list), `missing` (no marker at all: the next add, upgrade or remove adds an
|
|
47
|
+
* empty block) or `invalid` (a marker alone, repeated, out of order, or inside the installed
|
|
48
|
+
* list: doctor warns GUIDE_PROJECT_BLOCK_INVALID and the CLI leaves the file alone).
|
|
49
|
+
*/
|
|
50
|
+
export declare function projectBlockState(text: string): 'ok' | 'missing' | 'invalid';
|
|
51
|
+
/** The text with an empty project block (and its heading) added at the end. */
|
|
52
|
+
export declare function withProjectBlock(text: string): string;
|
|
35
53
|
/** AGENTS.md of a new project (D-0065 skeleton): ask first, use the catalog, verify last. */
|
|
36
|
-
export declare function projectGuide(entries: readonly InstalledEntry[]): string;
|
|
54
|
+
export declare function projectGuide(entries: readonly InstalledEntry[], catalog?: Catalog): string;
|