@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
|
@@ -1,21 +1,101 @@
|
|
|
1
|
-
import { readdir, readFile, realpath } from 'node:fs/promises';
|
|
1
|
+
import { readdir, readFile, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
import satisfies from 'semver/functions/satisfies.js';
|
|
3
4
|
import valid from 'semver/functions/valid.js';
|
|
4
|
-
import { CONFIG_FILE, LEGACY_CONFIG_FILE, parseProjectConfig } from './configuration.js';
|
|
5
|
+
import { CONFIG_FILE, configTarget, FRESH_COPY_FIX, LEGACY_CONFIG_FILE, parseProjectConfig, readProjectConfigSource, } from './configuration.js';
|
|
6
|
+
import { connectionSecrets } from './dev-compose.js';
|
|
7
|
+
import { COMPOSE_FILE, composeAppPortHint, composeBuildsAppHint, composeServicesBuildingProject, composeServicesPublishing, composeUnsetVariables, composeVariablesHint, composeVariablesOf, envKeys, } from './dev-infra.js';
|
|
8
|
+
import { debug, describeError, isMissing } from './diagnostics.js';
|
|
9
|
+
import { ENV_FILE, envFileWarnings } from './env-file.js';
|
|
5
10
|
import { CliError } from './errors.js';
|
|
6
11
|
import { exists, fingerprint, hash, json, parseObject, securePath } from './filesystem.js';
|
|
7
|
-
import {
|
|
12
|
+
import { adminSections } from './first-admin.js';
|
|
13
|
+
import { legacyLayout, MOVED_RECIPE_FILES } from './layout.js';
|
|
8
14
|
import { loggingDiagnostics } from './logging-config.js';
|
|
9
|
-
import { orderPlugins, readManifest } from './manifest.js';
|
|
15
|
+
import { optionsKey, orderPlugins, readManifest } from './manifest.js';
|
|
10
16
|
import { dependencyMap, validateProjectPackage } from './npm-client.js';
|
|
11
|
-
import {
|
|
17
|
+
import { optionsFix } from './option-fixes.js';
|
|
18
|
+
import { checkPlugin, checkPluginsOptions } from './plugin-lifecycle.js';
|
|
12
19
|
import { PluginOptionsValidator } from './plugin-options.js';
|
|
13
|
-
import { AGENTS_FILE, guideEntries, INSTALLED_END, INSTALLED_START, installedBlockEdited, } from './project-guide.js';
|
|
20
|
+
import { AGENTS_FILE, guideEntries, INSTALLED_END, INSTALLED_START, installedBlockEdited, PROJECT_END, PROJECT_START, projectBlockState, } from './project-guide.js';
|
|
14
21
|
import { readState } from './project-state.js';
|
|
22
|
+
import { ADMIN_SECTION, LEGACY_ADMIN_SECTION, SEED_FILE } from './recipe-login.js';
|
|
15
23
|
import { readRequiredPlugins } from './required-plugins.js';
|
|
24
|
+
import { CONVENTION_SECTIONS, catalogPackageOf, renderSettingsSchema, settingsFile, settingsSchemaFile, undeclaredSection, } from './settings.js';
|
|
25
|
+
import { templateFixes } from './template-fixes.js';
|
|
16
26
|
import { installedPath } from './workspace.js';
|
|
27
|
+
/**
|
|
28
|
+
* Reads `.dokebi/lock` (`{ pid }`, written by Transaction.begin) without taking it (AI-M04 F6):
|
|
29
|
+
* only a signal-0 probe, no `ps`. A reused pid can look alive; `since` lets a reader see an old
|
|
30
|
+
* lock. Null when the lock is gone.
|
|
31
|
+
*/
|
|
32
|
+
export async function lockHolder(root) {
|
|
33
|
+
const file = join(root, '.dokebi', 'lock');
|
|
34
|
+
let pid = null;
|
|
35
|
+
let since = null;
|
|
36
|
+
try {
|
|
37
|
+
since = (await stat(file)).mtime.toISOString();
|
|
38
|
+
const value = JSON.parse(await readFile(file, 'utf8'));
|
|
39
|
+
if (Number.isSafeInteger(value?.pid) && value.pid > 0)
|
|
40
|
+
pid = value.pid;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// Gone since the listing: the command ended. Otherwise written half: no pid known.
|
|
44
|
+
if (isMissing(error))
|
|
45
|
+
return null;
|
|
46
|
+
debug('inspect: lock file without a readable pid', { error: describeError(error) });
|
|
47
|
+
}
|
|
48
|
+
if (pid === null)
|
|
49
|
+
return { pid, running: null, since };
|
|
50
|
+
let running;
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pid, 0);
|
|
53
|
+
running = true;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const code = error.code;
|
|
57
|
+
// EPERM: it lives under another user or sandbox; ESRCH: gone; anything else: unknown.
|
|
58
|
+
running = code === 'EPERM' ? true : code === 'ESRCH' ? false : null;
|
|
59
|
+
if (running === null)
|
|
60
|
+
debug('inspect: lock holder probe failed', { pid, error: describeError(error) });
|
|
61
|
+
}
|
|
62
|
+
debug('inspect: project lock held', { pid, running, since });
|
|
63
|
+
return { pid, running, since };
|
|
64
|
+
}
|
|
65
|
+
/** The PROJECT_LOCKED diagnostic with what to do (AI-M04 F6). */
|
|
66
|
+
export function lockDiagnostic(holder, transactions) {
|
|
67
|
+
const who = holder.pid === null ? 'a dokebi command' : `a dokebi command (pid ${holder.pid})`;
|
|
68
|
+
return {
|
|
69
|
+
code: 'PROJECT_LOCKED',
|
|
70
|
+
severity: 'error',
|
|
71
|
+
path: '.dokebi/lock',
|
|
72
|
+
plugin: null,
|
|
73
|
+
details: { ...holder, transactions },
|
|
74
|
+
hint: holder.running === true
|
|
75
|
+
? `${who} holds the lock and is changing the project right now (a --dry-run takes it too), so this read may show its unfinished work${transactions.length ? ` (${transactions.join(', ')} is its open transaction, not an interrupted one)` : ''}. inspect is read-only and takes no lock: run it again when that command has ended. Never delete the lock`
|
|
76
|
+
: holder.running === false
|
|
77
|
+
? `${who} took the lock and is no longer running: a change was interrupted. Do not delete .dokebi/lock or .dokebi/transaction-* (the backups of the originals); ask the user to look`
|
|
78
|
+
: `.dokebi/lock names no process. If no dokebi command is running, a change was interrupted: ask the user to look; never delete the lock`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
17
81
|
// Reads metadata only and never creates locks. With runChecks (doctor), each plugin's
|
|
18
82
|
// installation module check runs in the permission-restricted child process.
|
|
83
|
+
/**
|
|
84
|
+
* S-c: fixes of the @dokebi/db adapter templates a copy made earlier may lack: found by a marker
|
|
85
|
+
* of the fix in the installed template and its absence in the copy.
|
|
86
|
+
*/
|
|
87
|
+
/** The apps under src/apps with an Inertia browser entry (views/app.js), sorted by name. */
|
|
88
|
+
async function inertiaAppNames(root) {
|
|
89
|
+
const apps = await securePath(root, 'src/apps');
|
|
90
|
+
if (!(await exists(apps)))
|
|
91
|
+
return [];
|
|
92
|
+
const names = [];
|
|
93
|
+
for (const entry of await readdir(apps, { withFileTypes: true }))
|
|
94
|
+
if (entry.isDirectory() &&
|
|
95
|
+
(await exists(await securePath(root, `src/apps/${entry.name}/views/app.js`))))
|
|
96
|
+
names.push(entry.name);
|
|
97
|
+
return names.sort();
|
|
98
|
+
}
|
|
19
99
|
export class ProjectInspector {
|
|
20
100
|
async inspect(directory, settings = {}) {
|
|
21
101
|
const root = await realpath(directory);
|
|
@@ -29,7 +109,35 @@ export class ProjectInspector {
|
|
|
29
109
|
diagnostics: [],
|
|
30
110
|
};
|
|
31
111
|
const originals = new Map();
|
|
32
|
-
|
|
112
|
+
// M50: the settings file the app reads (DOKEBI_CONFIG, else dokebi.yaml); diagnostics name it.
|
|
113
|
+
const settingsTarget = await configTarget(root);
|
|
114
|
+
const configFile = settingsTarget.file;
|
|
115
|
+
// R4CC-7: a DOKEBI_CONFIG line of .env is never passed to the app (the environment names the
|
|
116
|
+
// settings file); doctor says so as a warning, as start, dev and verify do.
|
|
117
|
+
try {
|
|
118
|
+
for (const warning of envFileWarnings(root))
|
|
119
|
+
result.diagnostics.push({
|
|
120
|
+
code: warning.code,
|
|
121
|
+
path: warning.path,
|
|
122
|
+
plugin: null,
|
|
123
|
+
severity: 'warning',
|
|
124
|
+
hint: warning.hint,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (!(error instanceof CliError))
|
|
129
|
+
throw error;
|
|
130
|
+
result.diagnostics.push({
|
|
131
|
+
code: error.code,
|
|
132
|
+
path: ENV_FILE,
|
|
133
|
+
plugin: null,
|
|
134
|
+
severity: 'error',
|
|
135
|
+
...(error.details !== undefined ? { details: error.details } : {}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const composePath = await securePath(root, COMPOSE_FILE);
|
|
139
|
+
// Every issue (D-0153): the fix names all unknown keys in one config unset.
|
|
140
|
+
const optionsValidator = new PluginOptionsValidator({ allErrors: true });
|
|
33
141
|
const issue = (code, path, plugin = null, severity = 'error') => {
|
|
34
142
|
result.diagnostics.push({ code, path, plugin, severity });
|
|
35
143
|
};
|
|
@@ -57,24 +165,46 @@ export class ProjectInspector {
|
|
|
57
165
|
};
|
|
58
166
|
const read = async (path) => {
|
|
59
167
|
// node_modules paths resolve like Node in a workspace member (D-0035).
|
|
60
|
-
const text =
|
|
168
|
+
const text = path === configFile
|
|
169
|
+
? await readProjectConfigSource(root, settingsTarget.path)
|
|
170
|
+
: await readFile(await installedPath(root, path), 'utf8');
|
|
61
171
|
originals.set(path, hash(text));
|
|
62
172
|
return text;
|
|
63
173
|
};
|
|
64
174
|
const state = await check('.dokebi/state.json', async () => readState(parseObject(await read('.dokebi/state.json'), 'state')));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
175
|
+
// R3 Claude T4 review: a fresh copy without dokebi.yaml got ENOENT with no fix, and every
|
|
176
|
+
// plugin's options then read as missing (INVALID_PLUGIN_OPTIONS "correct options.…").
|
|
177
|
+
const configMissing = !settingsTarget.fromEnvironment &&
|
|
178
|
+
!(await exists(settingsTarget.path)) &&
|
|
179
|
+
!(await exists(await securePath(root, LEGACY_CONFIG_FILE)));
|
|
180
|
+
if (configMissing)
|
|
181
|
+
result.diagnostics.push({
|
|
182
|
+
code: 'NOT_A_PROJECT',
|
|
183
|
+
path: configFile,
|
|
184
|
+
plugin: null,
|
|
185
|
+
severity: 'error',
|
|
186
|
+
details: { file: configFile, fix: FRESH_COPY_FIX },
|
|
187
|
+
hint: FRESH_COPY_FIX,
|
|
188
|
+
});
|
|
189
|
+
const config = configMissing
|
|
190
|
+
? null
|
|
191
|
+
: await check(configFile, async () => {
|
|
192
|
+
if (await exists(await securePath(root, LEGACY_CONFIG_FILE)))
|
|
193
|
+
throw new CliError((await exists(await securePath(root, CONFIG_FILE)))
|
|
194
|
+
? 'CONFIG_CONFLICT'
|
|
195
|
+
: 'MIGRATION_REQUIRED', 'Explicit YAML migration is required');
|
|
196
|
+
return parseProjectConfig(await read(configFile));
|
|
197
|
+
});
|
|
72
198
|
result.requiredPlugins =
|
|
73
199
|
(await check('node_modules/@dokebi/core/package.json', () => readRequiredPlugins(root))) ??
|
|
74
200
|
[];
|
|
201
|
+
let typecheckScript;
|
|
75
202
|
const pkg = await check('package.json', async () => {
|
|
76
203
|
const value = parseObject(await read('package.json'), 'package');
|
|
77
204
|
validateProjectPackage(value);
|
|
205
|
+
const scripts = value.scripts;
|
|
206
|
+
if (scripts && typeof scripts === 'object' && !Array.isArray(scripts))
|
|
207
|
+
typecheckScript = scripts.typecheck;
|
|
78
208
|
return dependencyMap(value);
|
|
79
209
|
});
|
|
80
210
|
const corePath = 'node_modules/@dokebi/core/package.json';
|
|
@@ -102,19 +232,187 @@ export class ProjectInspector {
|
|
|
102
232
|
...(state?.plugins.map((plugin) => plugin.id) ?? []),
|
|
103
233
|
...result.requiredPlugins.map((plugin) => plugin.id),
|
|
104
234
|
]);
|
|
105
|
-
|
|
235
|
+
const loggerHost = state?.plugins.find((plugin) => plugin.hosts?.includes('logger'))?.id;
|
|
236
|
+
// M50: the paths name the file the app reads (configFile), as every other diagnostic.
|
|
237
|
+
// Typed with the `file` parameter of the config area's change, so this call compiles on
|
|
238
|
+
// either side of that merge (before it, the fourth argument is not read).
|
|
239
|
+
const logging = loggingDiagnostics;
|
|
240
|
+
result.diagnostics.push(...logging(config.options, known, loggerHost, configFile));
|
|
241
|
+
// D-0115: the first admin of an older recipe sits under options.blog-admin. It keeps
|
|
242
|
+
// working (the CLI reads both names); the CLI never renames a user's section itself.
|
|
243
|
+
if (Object.hasOwn(config.options, LEGACY_ADMIN_SECTION) && !known.has(LEGACY_ADMIN_SECTION)) {
|
|
244
|
+
debug('Older first-admin section in dokebi.yaml', { section: LEGACY_ADMIN_SECTION });
|
|
245
|
+
result.diagnostics.push({
|
|
246
|
+
code: 'LEGACY_ADMIN_SECTION',
|
|
247
|
+
severity: 'warning',
|
|
248
|
+
path: configFile,
|
|
249
|
+
plugin: null,
|
|
250
|
+
details: { section: LEGACY_ADMIN_SECTION, renameTo: ADMIN_SECTION },
|
|
251
|
+
hint: `options.${LEGACY_ADMIN_SECTION} (the first admin an older recipe wrote) keeps working and needs nothing now. Recipes now call it options.${ADMIN_SECTION}, a name not tied to one kind of app; to follow, rename the section in dokebi.yaml and the options['${LEGACY_ADMIN_SECTION}'] read in ${SEED_FILE} together (the CLI never renames them for you)`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// R3 Codex T2: the app itself as a compose.yaml service on its own port, so dev-infra up
|
|
256
|
+
// started it and start, dev and verify found the port taken.
|
|
257
|
+
// R2 Codex T2: a compose.yaml that `dokebi dev-infra up` cannot start (a variable without a
|
|
258
|
+
// default that only the project's own script set) was silent here. D-0165: the passwords of
|
|
259
|
+
// dokebi.yaml that dev-infra up passes count as set.
|
|
260
|
+
const passed = Object.keys(connectionSecrets(config?.options ?? {}));
|
|
261
|
+
const envPath = await securePath(root, ENV_FILE);
|
|
262
|
+
const provided = async () => [
|
|
263
|
+
...passed,
|
|
264
|
+
...((await exists(envPath)) ? envKeys(await readFile(envPath, 'utf8')) : []),
|
|
265
|
+
];
|
|
266
|
+
if (await exists(composePath)) {
|
|
267
|
+
const unset = composeUnsetVariables(await readFile(composePath, 'utf8'), await provided());
|
|
268
|
+
debug('doctor: compose.yaml variables', { unset: unset.length, passed: passed.length });
|
|
269
|
+
if (unset.length)
|
|
270
|
+
result.diagnostics.push({
|
|
271
|
+
code: 'COMPOSE_VARIABLES_UNSET',
|
|
272
|
+
path: COMPOSE_FILE,
|
|
273
|
+
plugin: null,
|
|
274
|
+
severity: 'warning',
|
|
275
|
+
details: { variables: unset },
|
|
276
|
+
hint: composeVariablesHint(unset),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const webPort = config?.options['dokebi-web']?.port;
|
|
280
|
+
if (typeof webPort === 'number' && webPort > 0 && (await exists(composePath))) {
|
|
281
|
+
const services = composeServicesPublishing(await readFile(composePath, 'utf8'), webPort);
|
|
282
|
+
debug('doctor: compose.yaml services on the web port', { port: webPort, services });
|
|
283
|
+
if (services.length)
|
|
284
|
+
result.diagnostics.push({
|
|
285
|
+
code: 'COMPOSE_PUBLISHES_APP_PORT',
|
|
286
|
+
path: COMPOSE_FILE,
|
|
287
|
+
plugin: null,
|
|
288
|
+
severity: 'warning',
|
|
289
|
+
details: { services, port: webPort },
|
|
290
|
+
hint: composeAppPortHint(services, webPort),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// R4 Codex T4: the app as a compose.yaml service built from the project (`build: .`) whose
|
|
294
|
+
// port is a variable without a default, which the port rule above cannot read.
|
|
295
|
+
if (await exists(composePath)) {
|
|
296
|
+
const text = await readFile(composePath, 'utf8');
|
|
297
|
+
const named = new Set(typeof webPort === 'number' && webPort > 0 ? composeServicesPublishing(text, webPort) : []);
|
|
298
|
+
const services = composeServicesBuildingProject(text).filter((name) => !named.has(name));
|
|
299
|
+
debug('doctor: compose.yaml services built from the project', { services });
|
|
300
|
+
if (services.length) {
|
|
301
|
+
const unset = composeUnsetVariables(text, await provided());
|
|
302
|
+
const variables = composeVariablesOf(text, services, unset);
|
|
303
|
+
result.diagnostics.push({
|
|
304
|
+
code: 'COMPOSE_BUILDS_APP',
|
|
305
|
+
path: COMPOSE_FILE,
|
|
306
|
+
plugin: null,
|
|
307
|
+
severity: 'warning',
|
|
308
|
+
details: { services, ...(variables.length ? { variables } : {}) },
|
|
309
|
+
hint: composeBuildsAppHint(services, variables),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// D-0155: the settings sections of the application, like plugin sections: their options
|
|
314
|
+
// against the schema file, and the schema file against the declaration it was written from.
|
|
315
|
+
if (config && state)
|
|
316
|
+
for (const section of state.settings ?? []) {
|
|
317
|
+
const file = settingsFile(section);
|
|
318
|
+
const schemaFile = settingsSchemaFile(section);
|
|
319
|
+
const value = Object.hasOwn(config.options, section) ? config.options[section] : undefined;
|
|
320
|
+
const checked = await check(schemaFile, () => optionsValidator.checkSettings(root, section, value ?? {}), section);
|
|
321
|
+
if (checked && !checked.schema)
|
|
322
|
+
result.diagnostics.push({
|
|
323
|
+
code: 'SETTINGS_SCHEMA_MISSING',
|
|
324
|
+
severity: 'error',
|
|
325
|
+
path: schemaFile,
|
|
326
|
+
plugin: section,
|
|
327
|
+
hint: 'npx dokebi settings sync writes it from the declaration',
|
|
328
|
+
});
|
|
329
|
+
if (checked?.errors.length)
|
|
330
|
+
result.diagnostics.push({
|
|
331
|
+
code: 'INVALID_PLUGIN_OPTIONS',
|
|
332
|
+
severity: 'error',
|
|
333
|
+
path: configFile,
|
|
334
|
+
plugin: section,
|
|
335
|
+
details: checked.errors,
|
|
336
|
+
hint: optionsFix(section, checked.errors, `correct options.${section} in dokebi.yaml (${schemaFile} lists the option keys)`, section, checked.schema),
|
|
337
|
+
});
|
|
338
|
+
const rendered = await check(file, () => renderSettingsSchema(root, section), section);
|
|
339
|
+
if (rendered !== null && checked?.schema) {
|
|
340
|
+
const current = await readFile(join(root, schemaFile), 'utf8');
|
|
341
|
+
if (current !== rendered)
|
|
342
|
+
result.diagnostics.push({
|
|
343
|
+
code: 'SETTINGS_SCHEMA_STALE',
|
|
344
|
+
severity: 'warning',
|
|
345
|
+
path: schemaFile,
|
|
346
|
+
plugin: section,
|
|
347
|
+
hint: `${file} changed since its schema was written: npx dokebi settings sync`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// D-0155: a section of dokebi.yaml that nothing declares (code that reads options.<name> by
|
|
352
|
+
// hand): never checked at start, unknown to config show/set/unset/secret and the example.
|
|
353
|
+
if (config && state) {
|
|
354
|
+
const declared = new Set([
|
|
355
|
+
'logging',
|
|
356
|
+
'reload',
|
|
357
|
+
...CONVENTION_SECTIONS,
|
|
358
|
+
// The first admin a recipe wrote, under any section name (D-0115).
|
|
359
|
+
...adminSections(config.options),
|
|
360
|
+
...state.plugins.flatMap((plugin) => [plugin.id, optionsKey(plugin)]),
|
|
361
|
+
...result.requiredPlugins.map((plugin) => plugin.id),
|
|
362
|
+
...(state.settings ?? []),
|
|
363
|
+
]);
|
|
364
|
+
const undeclared = Object.keys(config.options).filter((section) => !declared.has(section));
|
|
365
|
+
if (undeclared.length) {
|
|
366
|
+
const packageOf = await catalogPackageOf();
|
|
367
|
+
for (const section of undeclared) {
|
|
368
|
+
const found = undeclaredSection(section, packageOf, state.plugins.map((plugin) => plugin.id), state.settings ?? []);
|
|
369
|
+
debug('Undeclared section in dokebi.yaml', { section, code: found.code });
|
|
370
|
+
result.diagnostics.push({
|
|
371
|
+
code: found.code,
|
|
372
|
+
severity: 'warning',
|
|
373
|
+
path: configFile,
|
|
374
|
+
plugin: section,
|
|
375
|
+
details: found.details,
|
|
376
|
+
hint: `${found.message}; ${found.details.fix}`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
106
380
|
}
|
|
107
381
|
const generated = await check('src/dokebi.generated.ts', () => read('src/dokebi.generated.ts'));
|
|
108
382
|
if (state && generated !== null && hash(generated) !== state.generatedHash)
|
|
109
|
-
|
|
383
|
+
// R1 measurement: a code formatter over src/ is the usual cause; the hint says both.
|
|
384
|
+
result.diagnostics.push({
|
|
385
|
+
code: 'MODIFIED_REGISTRATION',
|
|
386
|
+
path: 'src/dokebi.generated.ts',
|
|
387
|
+
plugin: null,
|
|
388
|
+
severity: 'error',
|
|
389
|
+
hint: 'a hand edit or a code formatter (prettier, biome) changed it: restore it (git checkout -- src/dokebi.generated.ts) and keep formatters out of it (the file in .prettierignore, "!src/dokebi.generated.ts" in biome files.includes; npx dokebi check warns formatter-generated)',
|
|
390
|
+
});
|
|
110
391
|
const control = await check('.dokebi', async () => readdir(await securePath(root, '.dokebi')));
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
392
|
+
// AI-M04 F6: a lock whose command still runs is that command's work in progress (its open
|
|
393
|
+
// transaction folder included), not an interrupted change that needs recovery.
|
|
394
|
+
const transactions = (control ?? [])
|
|
395
|
+
.filter((name) => name.startsWith('transaction-'))
|
|
396
|
+
.map((name) => `.dokebi/${name}`);
|
|
397
|
+
await check('.dokebi', async () => {
|
|
398
|
+
const holder = control?.includes('lock') ? await lockHolder(root) : null;
|
|
399
|
+
if (holder)
|
|
400
|
+
result.diagnostics.push(lockDiagnostic(holder, transactions));
|
|
401
|
+
if (holder?.running !== true)
|
|
402
|
+
// A command that ended since the listing removed its folder with the lock.
|
|
403
|
+
for (const path of transactions)
|
|
404
|
+
if (await exists(await securePath(root, path)))
|
|
405
|
+
result.diagnostics.push({
|
|
406
|
+
code: 'RECOVERY_REQUIRED',
|
|
407
|
+
severity: 'error',
|
|
408
|
+
path,
|
|
409
|
+
plugin: null,
|
|
410
|
+
hint: 'an interrupted change left its transaction folder (backups of the originals). Never delete it or the lock; ask the user to look',
|
|
411
|
+
});
|
|
412
|
+
return true;
|
|
413
|
+
});
|
|
117
414
|
if (state) {
|
|
415
|
+
const ownRules = [];
|
|
118
416
|
for (const plugin of state.plugins) {
|
|
119
417
|
const path = 'node_modules/' + plugin.packageName + '/package.json';
|
|
120
418
|
const installed = await check(path, async () => readManifest(parseObject(await read(path), 'plugin')), plugin.id);
|
|
@@ -131,20 +429,27 @@ export class ProjectInspector {
|
|
|
131
429
|
issue('MISSING_PACKAGE_DEPENDENCY', 'package.json', plugin.id);
|
|
132
430
|
if (installed)
|
|
133
431
|
matches(installed.packageName, installed.version, plugin.id);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const checked = installed
|
|
432
|
+
// D-0151: the logger host's options are options.logging.
|
|
433
|
+
const key = optionsKey(plugin);
|
|
434
|
+
const options = config && Object.hasOwn(config.options, key) ? config.options[key] : undefined;
|
|
435
|
+
const checked = installed && !configMissing
|
|
138
436
|
? await check(path, async () => optionsValidator.check(root, installed, options ?? {}), plugin.id)
|
|
139
437
|
: null;
|
|
140
438
|
if (checked?.errors.length)
|
|
141
439
|
result.diagnostics.push({
|
|
142
440
|
code: 'INVALID_PLUGIN_OPTIONS',
|
|
143
441
|
severity: 'error',
|
|
144
|
-
path:
|
|
442
|
+
path: configFile,
|
|
145
443
|
plugin: plugin.id,
|
|
146
444
|
details: checked.errors,
|
|
445
|
+
// D-0153: unknown keys name `config unset`, moved ones where they went.
|
|
446
|
+
// The commands name the section (options.logging for the logger host, D-0151).
|
|
447
|
+
hint: optionsFix(plugin.id, checked.errors, `correct options.${optionsKey(plugin)} in dokebi.yaml (npx dokebi plugins info ${plugin.id} --json lists the option keys)`, optionsKey(plugin), checked.schema),
|
|
147
448
|
});
|
|
449
|
+
// D-0162: the plugin's own rules once its section passes the schema (checked below, all
|
|
450
|
+
// plugins in one restricted child).
|
|
451
|
+
if (settings.runChecks && installed && config && checked && !checked.errors.length)
|
|
452
|
+
ownRules.push(installed);
|
|
148
453
|
let documentation = null;
|
|
149
454
|
if (installed?.documentation) {
|
|
150
455
|
const candidate = 'node_modules/' + installed.packageName + '/' + installed.documentation;
|
|
@@ -178,7 +483,7 @@ export class ProjectInspector {
|
|
|
178
483
|
result.diagnostics.push({
|
|
179
484
|
code: 'MISSING_REQUIRED_PLUGIN',
|
|
180
485
|
severity: 'error',
|
|
181
|
-
path:
|
|
486
|
+
path: configFile,
|
|
182
487
|
plugin: plugin.id,
|
|
183
488
|
details: { requires: missing },
|
|
184
489
|
});
|
|
@@ -189,7 +494,7 @@ export class ProjectInspector {
|
|
|
189
494
|
result.diagnostics.push({
|
|
190
495
|
code: 'PLUGIN_CHECK',
|
|
191
496
|
severity: item.severity,
|
|
192
|
-
path:
|
|
497
|
+
path: configFile,
|
|
193
498
|
plugin: plugin.id,
|
|
194
499
|
details: {
|
|
195
500
|
code: item.code,
|
|
@@ -210,6 +515,38 @@ export class ProjectInspector {
|
|
|
210
515
|
: [],
|
|
211
516
|
});
|
|
212
517
|
}
|
|
518
|
+
// D-0162: the plugins' own option rules (cross-field checks the schema cannot say), as each
|
|
519
|
+
// plugin runs them when it is created at start.
|
|
520
|
+
if (ownRules.length && config) {
|
|
521
|
+
const verdicts = await check(configFile, () => checkPluginsOptions(root, ownRules, config.options));
|
|
522
|
+
for (const plugin of ownRules) {
|
|
523
|
+
const verdict = verdicts?.get(plugin.id);
|
|
524
|
+
if (!verdict)
|
|
525
|
+
continue;
|
|
526
|
+
if ('failed' in verdict) {
|
|
527
|
+
result.diagnostics.push({
|
|
528
|
+
code: 'PLUGIN_OPTIONS_CHECK_FAILED',
|
|
529
|
+
severity: 'warning',
|
|
530
|
+
path: configFile,
|
|
531
|
+
plugin: plugin.id,
|
|
532
|
+
details: { code: verdict.failed },
|
|
533
|
+
hint: `the plugin's own option rules could not run here (${verdict.failed}); npx dokebi start checks them too`,
|
|
534
|
+
});
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (verdict.issues.length)
|
|
538
|
+
result.diagnostics.push({
|
|
539
|
+
code: 'INVALID_PLUGIN_OPTIONS',
|
|
540
|
+
severity: 'error',
|
|
541
|
+
path: configFile,
|
|
542
|
+
plugin: plugin.id,
|
|
543
|
+
details: verdict.issues,
|
|
544
|
+
hint: `correct options.${optionsKey(plugin)} in dokebi.yaml: ${verdict.issues
|
|
545
|
+
.map((one) => `${one.instancePath.slice(1).replaceAll('/', '.') || '(section)'} ${one.message}`)
|
|
546
|
+
.join('; ')} (the app refuses to start with this; npx dokebi plugins info ${plugin.id} --json lists the option keys)`,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
213
550
|
if (core)
|
|
214
551
|
await check('.dokebi/state.json', async () => orderPlugins(state.plugins, core, result.requiredPlugins));
|
|
215
552
|
// D-0065 §11: only CLI-owned files (`owner: 'cli'`, regenerated by the CLI or a plugin) are
|
|
@@ -232,11 +569,109 @@ export class ProjectInspector {
|
|
|
232
569
|
}
|
|
233
570
|
for (const [path, expected] of originals) {
|
|
234
571
|
await check(path, async () => {
|
|
235
|
-
|
|
572
|
+
const text = path === configFile
|
|
573
|
+
? await readProjectConfigSource(root, settingsTarget.path)
|
|
574
|
+
: await readFile(await installedPath(root, path));
|
|
575
|
+
if (hash(text) !== expected)
|
|
236
576
|
throw new CliError('CONCURRENT_CHANGE', 'Metadata changed during inspection');
|
|
237
577
|
return true;
|
|
238
578
|
});
|
|
239
579
|
}
|
|
580
|
+
// A file an older recipe wrote at a place the current layout moved: it keeps working.
|
|
581
|
+
for (const [path, moveTo] of Object.entries(MOVED_RECIPE_FILES)) {
|
|
582
|
+
const present = await check(path, async () => exists(await securePath(root, path)));
|
|
583
|
+
if (present)
|
|
584
|
+
result.diagnostics.push({
|
|
585
|
+
code: 'LEGACY_LAYOUT',
|
|
586
|
+
severity: 'warning',
|
|
587
|
+
path,
|
|
588
|
+
plugin: null,
|
|
589
|
+
details: { moveTo },
|
|
590
|
+
hint: `${path} was written by an older recipe and keeps working; the src root holds only main.ts and dokebi.generated.ts. Move it to ${moveTo}, fix its relative imports and change its import in src/main.ts to './${moveTo.slice('src/'.length)}'`,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
// S-c: files copied from a template once are the app's, so a later fix of the template never
|
|
594
|
+
// reaches them. The fixes the installed template carries and the copy lacks are named
|
|
595
|
+
// (template-fixes.ts).
|
|
596
|
+
const installedId = (id) => result.plugins.find((item) => item.installed?.id === id)?.installed;
|
|
597
|
+
const appsHost = state?.plugins.find((plugin) => plugin.hosts?.includes('apps'));
|
|
598
|
+
const drift = await check('src/infra/db/adapters', () => templateFixes(root, root, {
|
|
599
|
+
db: installedId('dokebi-db')?.packageName,
|
|
600
|
+
apps: appsHost ? { id: appsHost.id, packageName: appsHost.packageName } : undefined,
|
|
601
|
+
inertia: installedId('dokebi-template-inertia') !== undefined,
|
|
602
|
+
}));
|
|
603
|
+
if (drift)
|
|
604
|
+
result.diagnostics.push(...drift);
|
|
605
|
+
// D-0154: an entry that does not pass configFile cannot read dokebi.yaml again; dokebi dev then
|
|
606
|
+
// restarts on every change of it (S-c). In cluster mode the workers' app is built in
|
|
607
|
+
// src/infra/runtime/application.ts (createApplication): a worker without configFile cannot
|
|
608
|
+
// take the reload its primary asks for (Studio v2, 10-31: only src/main.ts was read).
|
|
609
|
+
for (const entry of ['src/main.ts', 'src/infra/runtime/application.ts']) {
|
|
610
|
+
const text = await check(entry, async () => (await exists(await securePath(root, entry)))
|
|
611
|
+
? await readFile(await securePath(root, entry), 'utf8')
|
|
612
|
+
: undefined);
|
|
613
|
+
// The code without comments (R13): a word in a comment wires nothing.
|
|
614
|
+
const code = typeof text === 'string'
|
|
615
|
+
? text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`])\/\/.*$/gm, '$1')
|
|
616
|
+
: undefined;
|
|
617
|
+
if (code !== undefined &&
|
|
618
|
+
/\bnew\s+(?:Application|ClusterRunner)\s*(?:<[^>]*>)?\s*\(/.test(code) &&
|
|
619
|
+
!/\bconfigFile\s*[:,}]/.test(code))
|
|
620
|
+
result.diagnostics.push({
|
|
621
|
+
code: 'RELOAD_NOT_WIRED',
|
|
622
|
+
severity: 'warning',
|
|
623
|
+
path: entry,
|
|
624
|
+
plugin: null,
|
|
625
|
+
hint: entry === 'src/main.ts'
|
|
626
|
+
? 'src/main.ts passes no configFile to new Application (or ClusterRunner), so the app never reads dokebi.yaml again: options marked reloadable need a restart, and dokebi dev restarts on every change. Add configFile: the path of dokebi.yaml, as the generated main.ts does (D-0154)'
|
|
627
|
+
: `${entry} builds the workers' app with new Application and no configFile, so a worker cannot take the reload its primary asks for (the reload fails): pass configFile: the path of dokebi.yaml (process.env.DOKEBI_CONFIG || new URL('../../../dokebi.yaml', import.meta.url)), as @dokebi/cluster AI.md shows (D-0154)`,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
// AI-M23: an Inertia app made before `app add --inertia` prepared the Vue type check.
|
|
631
|
+
if (pkg) {
|
|
632
|
+
const inertiaApps = await check('src/apps', () => inertiaAppNames(root));
|
|
633
|
+
// D-0129: a template-inertia without the dev file policy (no dist/vite-files.js) lets the
|
|
634
|
+
// Vite dev server of `dokebi dev` serve the whole project root, dokebi.yaml included; with
|
|
635
|
+
// devHosts (a tunnel) that is public, so it is an error then.
|
|
636
|
+
// Only an installed package is judged (a missing one is reported above).
|
|
637
|
+
const inertia = result.plugins.find((item) => item.installed?.id === 'dokebi-template-inertia')?.installed;
|
|
638
|
+
if (inertiaApps?.length && inertia) {
|
|
639
|
+
const policy = `node_modules/${inertia.packageName}/dist/vite-files.js`;
|
|
640
|
+
const guarded = await check(policy, async () => exists(await installedPath(root, policy)));
|
|
641
|
+
if (guarded === false) {
|
|
642
|
+
const section = config?.options['dokebi-template-inertia'];
|
|
643
|
+
const devHosts = section && typeof section === 'object' && !Array.isArray(section)
|
|
644
|
+
? section.devHosts
|
|
645
|
+
: undefined;
|
|
646
|
+
const tunnel = Array.isArray(devHosts) && devHosts.length > 0;
|
|
647
|
+
debug('Inertia dev server without the file policy', { apps: inertiaApps, tunnel });
|
|
648
|
+
result.diagnostics.push({
|
|
649
|
+
code: 'DEV_FILES_UNGUARDED',
|
|
650
|
+
severity: tunnel ? 'error' : 'warning',
|
|
651
|
+
path: `node_modules/${inertia.packageName}`,
|
|
652
|
+
plugin: inertia.id,
|
|
653
|
+
details: { apps: inertiaApps, devHosts: tunnel ? devHosts.length : 0 },
|
|
654
|
+
hint: `the installed ${inertia.packageName} has no dev file policy (D-0129): its Vite dev server answers every file of the project root (GET /build/dokebi.yaml?raw returns the configuration with its secrets, /build/src/main.ts?raw the server code)${tunnel ? ', and devHosts makes that public through the tunnel' : ''}. Upgrade it (npx dokebi upgrade ${inertia.id}); until then do not run dokebi dev behind devHosts, and change any secret that was reachable that way`,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (inertiaApps?.length &&
|
|
659
|
+
!(typeof typecheckScript === 'string' &&
|
|
660
|
+
/\b(?:vue-tsc|typecheck-vue\.mjs)\b/.test(typecheckScript))) {
|
|
661
|
+
// The first apps host (more than one is HOST_CONFLICT elsewhere): its scaffold folder.
|
|
662
|
+
const host = state?.plugins.find((plugin) => plugin.hosts?.includes('apps'));
|
|
663
|
+
const templates = `node_modules/${host?.packageName ?? '@dokebi/app'}/templates/inertia-build`;
|
|
664
|
+
debug('Inertia apps without a Vue type check', { apps: inertiaApps });
|
|
665
|
+
result.diagnostics.push({
|
|
666
|
+
code: 'VUE_TYPECHECK_MISSING',
|
|
667
|
+
severity: 'warning',
|
|
668
|
+
path: 'package.json',
|
|
669
|
+
plugin: null,
|
|
670
|
+
details: { apps: inertiaApps, templates },
|
|
671
|
+
hint: `npm run typecheck checks no .vue file. Add the Vue check that dokebi app add --inertia now prepares: the devDependencies vue-tsc and @typescript/typescript6 at the versions of ${templates}/build.json, ${templates}/tsconfig.vue.json and typecheck-vue.mjs copied to the project root, ${templates}/page.d.ts copied to src/infra/inertia/page.d.ts, and " && node typecheck-vue.mjs -p tsconfig.vue.json" after the tsc of the typecheck script. They are part of the framework scaffold and need no separate permission`,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
240
675
|
// D-0065: AGENTS.md carries the CLI-maintained installed list between two markers.
|
|
241
676
|
await check(AGENTS_FILE, async () => {
|
|
242
677
|
const path = await securePath(root, AGENTS_FILE);
|
|
@@ -259,12 +694,25 @@ export class ProjectInspector {
|
|
|
259
694
|
plugin: null,
|
|
260
695
|
hint: `move notes outside ${INSTALLED_START} … ${INSTALLED_END} and restore the rows (one per installed package: npx dokebi inspect --json); add, upgrade and remove rewrite them`,
|
|
261
696
|
});
|
|
697
|
+
// D-0077 G: what is inside the project block is never judged, only that its markers can
|
|
698
|
+
// be found (one start, then one end, outside the installed list).
|
|
699
|
+
if (text !== null && projectBlockState(text) === 'invalid')
|
|
700
|
+
result.diagnostics.push({
|
|
701
|
+
code: 'GUIDE_PROJECT_BLOCK_INVALID',
|
|
702
|
+
severity: 'warning',
|
|
703
|
+
path: AGENTS_FILE,
|
|
704
|
+
plugin: null,
|
|
705
|
+
hint: `keep exactly one ${PROJECT_START} line and, after it, one ${PROJECT_END} line in AGENTS.md, outside the dokebi:installed block (remove the extra or misplaced marker, keep your text); until then add, upgrade and remove leave AGENTS.md alone`,
|
|
706
|
+
});
|
|
262
707
|
return true;
|
|
263
708
|
});
|
|
264
709
|
await check('.dokebi/lock', async () => {
|
|
265
710
|
if ((await exists(await securePath(root, '.dokebi/lock'))) &&
|
|
266
|
-
!result.diagnostics.some((item) => item.code === 'PROJECT_LOCKED'))
|
|
267
|
-
|
|
711
|
+
!result.diagnostics.some((item) => item.code === 'PROJECT_LOCKED')) {
|
|
712
|
+
const late = await lockHolder(root);
|
|
713
|
+
if (late)
|
|
714
|
+
result.diagnostics.push(lockDiagnostic(late, []));
|
|
715
|
+
}
|
|
268
716
|
return true;
|
|
269
717
|
});
|
|
270
718
|
result.healthy = !result.diagnostics.some((item) => item.severity === 'error');
|
package/dist/project-state.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export interface ProjectState {
|
|
|
30
30
|
apps?: string[];
|
|
31
31
|
/** Modules in registration order (src/shared/modules/<name>), D-0032, for the modules host. */
|
|
32
32
|
modules?: string[];
|
|
33
|
+
/**
|
|
34
|
+
* D-0155: the settings sections of the application (options.<section>), declared in
|
|
35
|
+
* src/shared/settings/<section>.ts with their schema beside it.
|
|
36
|
+
*/
|
|
37
|
+
settings?: string[];
|
|
33
38
|
}
|
|
34
39
|
/** App and template names: lower-case letters, digits and hyphens, starting with a letter. */
|
|
35
40
|
export declare const APP_NAME: RegExp;
|