@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
package/dist/verify.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// `dokebi verify` (D-0065): the one check an AI runs before it says "done". doctor → check
|
|
2
|
-
// (D-0072, user-code conventions) → typecheck → build (Vite projects) →
|
|
2
|
+
// (D-0072, user-code conventions) → typecheck → build (Vite projects) → test (the project's
|
|
3
|
+
// `test` script, a result per node:test test: D-0077 H, verify-tests.ts) → preflight → start →
|
|
3
4
|
// HTTP checks → stop; `ok` only when every check that ran passed. The app it starts is always
|
|
4
5
|
// stopped again.
|
|
5
6
|
//
|
|
@@ -16,20 +17,127 @@ import { join } from 'node:path';
|
|
|
16
17
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
17
18
|
import { appUrls } from './app-urls.js';
|
|
18
19
|
import { check } from './check/index.js';
|
|
19
|
-
import { CONFIG_FILE, parseProjectConfig } from './configuration.js';
|
|
20
|
-
import { portInUse, URL_PLACEHOLDER_PREFIX } from './dev-infra.js';
|
|
21
|
-
import { debug, describeError } from './diagnostics.js';
|
|
20
|
+
import { CONFIG_FILE, configTarget, FRESH_COPY_FIX, parseProjectConfig, readNamedSettings, } from './configuration.js';
|
|
21
|
+
import { freePortAfter, portInUse, portQuestion, URL_PLACEHOLDER_PREFIX } from './dev-infra.js';
|
|
22
|
+
import { debug, describeError, isMissing, reportError } from './diagnostics.js';
|
|
23
|
+
import { envFileWarnings } from './env-file.js';
|
|
22
24
|
import { CliError } from './errors.js';
|
|
23
25
|
import { exists, parseObject, securePath } from './filesystem.js';
|
|
26
|
+
import { firstAdminPhrase, readFirstAdmin } from './first-admin.js';
|
|
24
27
|
import { findHost } from './manifest.js';
|
|
25
28
|
import { OutputTail } from './npm-client.js';
|
|
29
|
+
import { forwardTermination, killGroup, PROCESS_GROUPS } from './process-group.js';
|
|
26
30
|
import { ProjectInspector } from './project-inspector.js';
|
|
27
31
|
import { readState } from './project-state.js';
|
|
28
|
-
import {
|
|
32
|
+
import { factOf, factTexts, readProjectFacts } from './recipe-facts.js';
|
|
33
|
+
import { namesFirstAdmin, readAppliedRecipes, readRecordedChoices, TELL_USER_RELAY, tellUserLines, } from './recipe-record.js';
|
|
29
34
|
import * as server from './server.js';
|
|
35
|
+
import { probeUserStores } from './site-auth.js';
|
|
36
|
+
import { runTests, TEST_TIMEOUT_MS } from './verify-tests.js';
|
|
37
|
+
/**
|
|
38
|
+
* D-0105 §4: with @dokebi/db installed, what the agent's final message must contain besides
|
|
39
|
+
* tellUser. The guidance asks for it, and an AI consumer that read it many times still left it
|
|
40
|
+
* out (AI-M23, AI-M24): verify ok repeats it at the moment the agent is about to report.
|
|
41
|
+
*/
|
|
42
|
+
export function finalReportLines(ok, installed, allowed = []) {
|
|
43
|
+
if (!ok)
|
|
44
|
+
return [];
|
|
45
|
+
return [
|
|
46
|
+
...(installed.has('dokebi-db') ? [DB_PRACTICE_REPORT] : []),
|
|
47
|
+
...(allowed.length ? [allowedReport(allowed)] : []),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
/** The opening of the silenced-rules report and the words before its list of markers. */
|
|
51
|
+
const SILENCED = 'Silenced check rules: ';
|
|
52
|
+
const LISTED = 'why the rule does not apply there: ';
|
|
53
|
+
/**
|
|
54
|
+
* R1 measurement: a builder printed only `{ ok, checks, tests, tellUser }` of the verify JSON and
|
|
55
|
+
* never saw `finalReport`, so its final message lacked the section 6 table. The tellUser lines,
|
|
56
|
+
* which every builder relays, end with what finalReport asks for (the field stays as it is).
|
|
57
|
+
*/
|
|
58
|
+
export function tellUserWithReport(tellUser, finalReport, dbRules = []) {
|
|
59
|
+
if (!tellUser.length || !finalReport.length)
|
|
60
|
+
return [...tellUser];
|
|
61
|
+
// R1 T5 review: the lines are relayed verbatim (relay), so each is said to the user: a heading
|
|
62
|
+
// the builder's table or list follows, never an instruction to the builder.
|
|
63
|
+
// R3 Codex T2: Codex reads only ok, checks and tellUser of the result (never relay or
|
|
64
|
+
// finalReport) and put the table in a file for the fifth time; the heading itself says where
|
|
65
|
+
// the table is, in words the user reads as a plain statement.
|
|
66
|
+
return [
|
|
67
|
+
...tellUser,
|
|
68
|
+
...finalReport.map((line) => line === DB_PRACTICE_REPORT
|
|
69
|
+
? `Database practice (@dokebi/db AI.md section 6): the table below, here in this message, has one row per rule of that section (${dbRules.length ? dbRules.join('; ') : 'each by its number, 6.1 onwards'}), with the queries or jobs it applies to, the measurement (rows read, rate, refusals) and the verdict (pass, or the fix you made).`
|
|
70
|
+
: line.startsWith(SILENCED)
|
|
71
|
+
? `Silenced check rules (dokebi-check allow markers), each followed below, here in this message, by why the rule does not apply there: ${line.slice(line.indexOf(LISTED) + LISTED.length).replace(/\. A marker on work[\s\S]*$/, '')}.`
|
|
72
|
+
: line),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* R6 Codex T3: the heading said "one row per rule" and the builder, which reads only tellUser of
|
|
77
|
+
* the result and had read part of section 6, wrote a table of twelve rules of its own. The
|
|
78
|
+
* heading names the rules of the installed @dokebi/db AI.md (`### 6.N Title`), so each row has
|
|
79
|
+
* one to answer. No installed AI.md: none.
|
|
80
|
+
*/
|
|
81
|
+
export async function readDbPracticeRules(root) {
|
|
82
|
+
let text;
|
|
83
|
+
try {
|
|
84
|
+
text = await readFile(join(root, 'node_modules', '@dokebi', 'db', 'AI.md'), 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (!isMissing(error))
|
|
88
|
+
throw error;
|
|
89
|
+
debug('verify: no @dokebi/db AI.md for the section 6 rules', { root });
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
return [...text.matchAll(/^### (6\.\d+ .+)$/gm)].map((match) => (match[1] ?? '').trim());
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* M34: a builder silenced background-timer on a database polling loop and check passed. verify ok
|
|
96
|
+
* asks the final message to list every silenced problem with its reason, so the person reading it
|
|
97
|
+
* sees each one (the markers stay legitimate for per-process upkeep).
|
|
98
|
+
*/
|
|
99
|
+
export function allowedReport(allowed) {
|
|
100
|
+
const listed = allowed
|
|
101
|
+
.slice(0, 20)
|
|
102
|
+
.map((marker) => `${marker.file}:${marker.line} ${marker.rule} (${marker.reason})`)
|
|
103
|
+
.join('; ');
|
|
104
|
+
const more = allowed.length > 20 ? `; and ${allowed.length - 20} more (npx dokebi check)` : '';
|
|
105
|
+
return `${SILENCED}your final message lists every dokebi-check allow marker of the project with its reason and ${LISTED}${listed}${more}. A marker on work that reads or writes the database (a polling or folding loop) is not a valid reason: move that work to a job or an outbox handler.`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* R2 Codex T2, T4, T5 (and R1 T1, T2): the builder relayed the headings verbatim, "the table below
|
|
109
|
+
* …", and put the table in a results file; it reads relay and tellUser, not finalReport. With
|
|
110
|
+
* finalReport lines, relay itself says that each heading's table or list follows it in the message.
|
|
111
|
+
* R4 Claude T4 copied the other lines and wrote its own headings for these two (`## Database
|
|
112
|
+
* practice (…)`): relay called them "a heading". They are lines to copy like the others.
|
|
113
|
+
*/
|
|
114
|
+
export const REPORT_RELAY = Object.freeze({
|
|
115
|
+
tellUser: 'verbatim',
|
|
116
|
+
instruction: `${TELL_USER_RELAY.instruction} The lines that start with Database practice and Silenced check rules are copied word for word too (they are lines of this list, not headings to write in your own words), and right under each of them you write the table or list it announces, in the same final message, as finalReport says; a results file or a link to one does not replace it.`,
|
|
117
|
+
});
|
|
118
|
+
export const DB_PRACTICE_REPORT = 'Database practice (@dokebi/db is installed): your final message contains a table with one row per rule of section 6 of node_modules/@dokebi/db/AI.md: the rule, the queries or jobs it applies to, the measurement (rows read, rate, refusals) and the verdict (pass, or the fix you made). A rule you did not measure is not done; a separate results file does not replace the table in the message.';
|
|
119
|
+
/**
|
|
120
|
+
* The preflight problem for a taken web port (AI-M04 F5), with what the recipe preflight gives:
|
|
121
|
+
* a free `alternative`, `waitForUser`, `question` and `next`. Who holds the port is not looked up
|
|
122
|
+
* (no `ps`); verify only knows it is not an app `dokebi start` runs for this project (that is
|
|
123
|
+
* ALREADY_RUNNING). `delegated`: the last recipe ran with --defaults (`.dokebi/recipe.json`).
|
|
124
|
+
*/
|
|
125
|
+
export async function webPortProblem(port, delegated) {
|
|
126
|
+
const alternative = await freePortAfter(port);
|
|
127
|
+
debug('verify: web port in use', { port, alternative, delegated });
|
|
128
|
+
return {
|
|
129
|
+
code: 'PORT_IN_USE',
|
|
130
|
+
message: `Port ${port} (options.dokebi-web.port) is in use by another program (not an app npx dokebi start runs for this project; maybe its npx dokebi dev, another project or another program)`,
|
|
131
|
+
fix: `ask the user, then npx dokebi config set dokebi-web.port=${alternative ?? '<free port>'} and verify again, or stop what uses ${port}`,
|
|
132
|
+
...(alternative ? { alternative } : {}),
|
|
133
|
+
...portQuestion({ what: 'Web', port, alternative, flag: 'dokebi-web.port', delegated }),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
30
136
|
/**
|
|
31
137
|
* Runs an npm script of the project; resolves with its exit code and the bounded, redacted end
|
|
32
|
-
* of its output (stdout and stderr in arrival order, `OutputTail` of npm-client).
|
|
138
|
+
* of its output (stdout and stderr in arrival order, `OutputTail` of npm-client). The script runs
|
|
139
|
+
* in its own process group and the time limit kills the whole group: killing npm alone left tsc
|
|
140
|
+
* or Vite running, and the pipes they hold kept the step open until they ended by themselves.
|
|
33
141
|
*/
|
|
34
142
|
export async function npmScript(root, script, timeoutMs) {
|
|
35
143
|
const npmPath = process.env.npm_execpath;
|
|
@@ -37,7 +145,12 @@ export async function npmScript(root, script, timeoutMs) {
|
|
|
37
145
|
const args = [...(npmPath?.endsWith('npm-cli.js') ? [npmPath] : []), 'run', script];
|
|
38
146
|
debug('verify: running npm script', { script, root });
|
|
39
147
|
return new Promise((resolve, reject) => {
|
|
40
|
-
const child = spawn(command, args, {
|
|
148
|
+
const child = spawn(command, args, {
|
|
149
|
+
cwd: root,
|
|
150
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
151
|
+
detached: PROCESS_GROUPS,
|
|
152
|
+
});
|
|
153
|
+
const forwarding = forwardTermination(child.pid);
|
|
41
154
|
const output = new OutputTail();
|
|
42
155
|
const keep = (chunk) => output.push(chunk);
|
|
43
156
|
child.stdout.on('data', keep);
|
|
@@ -45,14 +158,17 @@ export async function npmScript(root, script, timeoutMs) {
|
|
|
45
158
|
let timedOut = false;
|
|
46
159
|
const timer = setTimeout(() => {
|
|
47
160
|
timedOut = true;
|
|
48
|
-
|
|
161
|
+
debug('verify: the npm script reached its time limit; killing it', { script, timeoutMs });
|
|
162
|
+
killGroup(child.pid, 'SIGKILL');
|
|
49
163
|
}, timeoutMs);
|
|
50
164
|
child.once('error', (cause) => {
|
|
51
165
|
clearTimeout(timer);
|
|
166
|
+
forwarding();
|
|
52
167
|
reject(new CliError('NPM_FAILED', `Could not run npm run ${script}`, { cause }));
|
|
53
168
|
});
|
|
54
169
|
child.once('close', (code) => {
|
|
55
170
|
clearTimeout(timer);
|
|
171
|
+
forwarding();
|
|
56
172
|
resolve({ code, output: output.text(), timedOut });
|
|
57
173
|
});
|
|
58
174
|
});
|
|
@@ -114,16 +230,45 @@ function get(url, host, timeoutMs = 5000) {
|
|
|
114
230
|
req.end();
|
|
115
231
|
});
|
|
116
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* A file verify needs before any check runs. A missing one is NOT_A_PROJECT with the file and a
|
|
235
|
+
* fix, never an INTERNAL_ERROR (a project copy without .dokebi/state.json, M32).
|
|
236
|
+
*/
|
|
237
|
+
async function projectFile(root, file) {
|
|
238
|
+
try {
|
|
239
|
+
return await readFile(join(root, file), 'utf8');
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (!isMissing(error))
|
|
243
|
+
throw new CliError('READ_FAILED', `Cannot read ${file}`, { cause: error });
|
|
244
|
+
throw new CliError('NOT_A_PROJECT', `No ${file}: verify runs in a Dokebi project (the folder with package.json, dokebi.yaml and .dokebi/state.json)`, {
|
|
245
|
+
phase: 'arguments',
|
|
246
|
+
details: {
|
|
247
|
+
root,
|
|
248
|
+
file,
|
|
249
|
+
fix: file === '.dokebi/state.json'
|
|
250
|
+
? 'run verify in the project folder itself; .dokebi/state.json is written by the CLI and belongs in version control, so a copy needs it too (copy it from the project or check it out from git)'
|
|
251
|
+
: file === CONFIG_FILE
|
|
252
|
+
? `run verify in the project folder; ${FRESH_COPY_FIX}`
|
|
253
|
+
: 'run verify in the project folder (npx dokebi verify from the folder with dokebi.yaml)',
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
117
258
|
async function settings(root) {
|
|
118
|
-
const pkg = parseObject(await
|
|
259
|
+
const pkg = parseObject(await projectFile(root, 'package.json'), 'package.json');
|
|
119
260
|
let vite = false;
|
|
120
261
|
for (const file of ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs'])
|
|
121
262
|
vite ||= await exists(await securePath(root, file));
|
|
122
|
-
const state = readState(parseObject(await
|
|
263
|
+
const state = readState(parseObject(await projectFile(root, '.dokebi/state.json'), 'state'));
|
|
123
264
|
const ids = new Set(state.plugins.map((plugin) => plugin.id));
|
|
124
265
|
// The domains to check belong to the plugin that hosts apps (D-0070), whatever its id.
|
|
125
266
|
const host = findHost(state.plugins, 'apps');
|
|
126
|
-
|
|
267
|
+
// M50: the settings file the app reads (DOKEBI_CONFIG, else dokebi.yaml).
|
|
268
|
+
const target = await configTarget(root);
|
|
269
|
+
const options = parseProjectConfig(target.fromEnvironment
|
|
270
|
+
? await readNamedSettings(root, target)
|
|
271
|
+
: await projectFile(root, CONFIG_FILE)).options;
|
|
127
272
|
const web = (options['dokebi-web'] ?? {});
|
|
128
273
|
const app = ((host ? options[host.id] : undefined) ?? {});
|
|
129
274
|
const db = (options['dokebi-db'] ?? {});
|
|
@@ -156,6 +301,8 @@ async function settings(root) {
|
|
|
156
301
|
* With neither a readiness path (`healthRoutes: false`) nor an app domain, one identity probe
|
|
157
302
|
* runs instead (ISSUE-0006 A08: no request at all used to pass): `GET /`, which passes with any
|
|
158
303
|
* status below 500 that carries the token, a 404 included (the app may have no route at `/`).
|
|
304
|
+
* Domain probes also accept 401/403 from this instance, reported as accessDenied: true.
|
|
305
|
+
* These prove reachability only; readiness still requires 2xx/3xx and login needs app tests.
|
|
159
306
|
* Each request must complete within `timeoutMs` (ISSUE-0006 A09).
|
|
160
307
|
*/
|
|
161
308
|
export async function httpChecks(base, paths, token, delayMs = 250, timeoutMs = 5000) {
|
|
@@ -167,12 +314,15 @@ export async function httpChecks(base, paths, token, delayMs = 250, timeoutMs =
|
|
|
167
314
|
try {
|
|
168
315
|
const answer = await get(url, host, timeoutMs);
|
|
169
316
|
const instance = answer.instance === token ? 'ours' : 'other';
|
|
170
|
-
const
|
|
317
|
+
const accessDenied = host !== undefined && (answer.status === 401 || answer.status === 403);
|
|
318
|
+
const ok = ((answer.status >= 200 && answer.status < (identity ? 500 : 400)) || accessDenied) &&
|
|
319
|
+
instance === 'ours';
|
|
171
320
|
if (ok || attempt >= attempts) {
|
|
172
321
|
requests.push({
|
|
173
322
|
url,
|
|
174
323
|
...(host ? { host } : {}),
|
|
175
324
|
status: answer.status,
|
|
325
|
+
...(accessDenied ? { accessDenied: true } : {}),
|
|
176
326
|
ok,
|
|
177
327
|
instance,
|
|
178
328
|
...kind,
|
|
@@ -230,6 +380,38 @@ export async function httpChecks(base, paths, token, delayMs = 250, timeoutMs =
|
|
|
230
380
|
details: { requests },
|
|
231
381
|
};
|
|
232
382
|
}
|
|
383
|
+
const TERMINATING = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
384
|
+
/**
|
|
385
|
+
* While the app verify started runs: a SIGINT, SIGTERM or SIGHUP to verify (Ctrl+C, an agent's
|
|
386
|
+
* command time limit) stops that app first (stopStarted, forced after `limitMs`), then ends verify
|
|
387
|
+
* as the signal would have. Without it the background app outlived verify and held its port; the
|
|
388
|
+
* next verify then refused with ALREADY_RUNNING. Returns the function that removes the handlers.
|
|
389
|
+
*/
|
|
390
|
+
export function stopOnTermination(root, started, limitMs = 5000) {
|
|
391
|
+
const handlers = new Map();
|
|
392
|
+
const remove = () => {
|
|
393
|
+
for (const [signal, handler] of handlers)
|
|
394
|
+
process.off(signal, handler);
|
|
395
|
+
handlers.clear();
|
|
396
|
+
};
|
|
397
|
+
for (const signal of TERMINATING) {
|
|
398
|
+
const handler = () => {
|
|
399
|
+
remove();
|
|
400
|
+
debug('verify: terminated; stopping the app it started', { signal, pid: started.pid });
|
|
401
|
+
server
|
|
402
|
+
.stopStarted(root, started, limitMs, true)
|
|
403
|
+
.catch((error) => reportError('VERIFY_STOP_FAILED', 'Unable to stop the app verify started', error))
|
|
404
|
+
.finally(() => {
|
|
405
|
+
// The signal's default action (ending this process) applies again.
|
|
406
|
+
if (process.listenerCount(signal) === 0)
|
|
407
|
+
process.kill(process.pid, signal);
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
handlers.set(signal, handler);
|
|
411
|
+
process.on(signal, handler);
|
|
412
|
+
}
|
|
413
|
+
return remove;
|
|
414
|
+
}
|
|
233
415
|
export async function verify(root, options = {}) {
|
|
234
416
|
const checks = [];
|
|
235
417
|
const run = async (name, body) => {
|
|
@@ -257,6 +439,16 @@ export async function verify(root, options = {}) {
|
|
|
257
439
|
};
|
|
258
440
|
const skip = (reason) => async () => ({ ok: true, skipped: reason });
|
|
259
441
|
const project = await settings(root);
|
|
442
|
+
// An unreadable .env fails the steps that need it (test, start) with its code.
|
|
443
|
+
let warnings = [];
|
|
444
|
+
try {
|
|
445
|
+
warnings = envFileWarnings(root).map(({ code, path, hint }) => ({ code, path, hint }));
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
if (!(error instanceof CliError))
|
|
449
|
+
throw error;
|
|
450
|
+
debug('verify: .env not read for its warnings', { code: error.code });
|
|
451
|
+
}
|
|
260
452
|
let inspection;
|
|
261
453
|
const doctor = await run('doctor', async () => {
|
|
262
454
|
const result = await new ProjectInspector().inspect(root, { runChecks: true });
|
|
@@ -272,8 +464,10 @@ export async function verify(root, options = {}) {
|
|
|
272
464
|
};
|
|
273
465
|
});
|
|
274
466
|
// D-0072: user-code conventions, on doctor's inspection (doctor is not run twice).
|
|
467
|
+
let allowed = [];
|
|
275
468
|
await run('check', async () => {
|
|
276
469
|
const result = await check(root, inspection ? { inspection } : {});
|
|
470
|
+
allowed = result.allowed;
|
|
277
471
|
return {
|
|
278
472
|
ok: result.ok,
|
|
279
473
|
...(result.ok
|
|
@@ -282,7 +476,12 @@ export async function verify(root, options = {}) {
|
|
|
282
476
|
code: 'CHECK_FAILED',
|
|
283
477
|
message: `${result.problems.length} convention problem(s): npx dokebi check`,
|
|
284
478
|
}),
|
|
285
|
-
details: {
|
|
479
|
+
details: {
|
|
480
|
+
problems: result.problems,
|
|
481
|
+
allowed: result.allowed,
|
|
482
|
+
files: result.files,
|
|
483
|
+
ms: result.ms,
|
|
484
|
+
},
|
|
286
485
|
};
|
|
287
486
|
});
|
|
288
487
|
const script = async (name, timeoutMs) => {
|
|
@@ -305,6 +504,17 @@ export async function verify(root, options = {}) {
|
|
|
305
504
|
await run('build', typeof project.scripts.build === 'string' && project.vite
|
|
306
505
|
? () => script('build', 300_000)
|
|
307
506
|
: skip('no Vite build in this project'));
|
|
507
|
+
// D-0077 H: the project's tests, after the build (they may need it) and before the preflight
|
|
508
|
+
// and the start (never next to the app: both may want the same port).
|
|
509
|
+
let tests;
|
|
510
|
+
await run('test', async () => {
|
|
511
|
+
const step = await runTests(root, project.scripts, {
|
|
512
|
+
timeoutMs: options.testTimeoutMs ?? TEST_TIMEOUT_MS,
|
|
513
|
+
});
|
|
514
|
+
const { tests: report, ...check } = step;
|
|
515
|
+
tests = report;
|
|
516
|
+
return check;
|
|
517
|
+
});
|
|
308
518
|
const preflight = await run('preflight', async () => {
|
|
309
519
|
const problems = [];
|
|
310
520
|
const running = await server.status(root);
|
|
@@ -315,11 +525,7 @@ export async function verify(root, options = {}) {
|
|
|
315
525
|
fix: 'npx dokebi stop',
|
|
316
526
|
});
|
|
317
527
|
else if (project.web.installed && project.web.port > 0 && (await portInUse(project.web.port)))
|
|
318
|
-
problems.push(
|
|
319
|
-
code: 'PORT_IN_USE',
|
|
320
|
-
message: `Port ${project.web.port} (options.dokebi-web.port) is in use`,
|
|
321
|
-
fix: 'stop what uses it, or npx dokebi config set dokebi-web.port=<free port>',
|
|
322
|
-
});
|
|
528
|
+
problems.push(await webPortProblem(project.web.port, (await readAppliedRecipes(root)).at(-1)?.answersFrom === 'defaults'));
|
|
323
529
|
for (const db of project.dbUrls)
|
|
324
530
|
if (db.missing)
|
|
325
531
|
problems.push({
|
|
@@ -337,6 +543,7 @@ export async function verify(root, options = {}) {
|
|
|
337
543
|
: { ok: true };
|
|
338
544
|
});
|
|
339
545
|
let started;
|
|
546
|
+
let release;
|
|
340
547
|
// Only the instance started here echoes this token (x-dokebi-verify, @dokebi/web).
|
|
341
548
|
const token = randomBytes(16).toString('hex');
|
|
342
549
|
const reason = !doctor.ok ? 'doctor failed' : !preflight.ok ? 'preflight failed' : undefined;
|
|
@@ -347,6 +554,7 @@ export async function verify(root, options = {}) {
|
|
|
347
554
|
waitMs: options.waitMs ?? 30_000,
|
|
348
555
|
env: { [VERIFY_TOKEN_ENV]: token },
|
|
349
556
|
});
|
|
557
|
+
release = stopOnTermination(root, started);
|
|
350
558
|
return {
|
|
351
559
|
ok: started.ready,
|
|
352
560
|
...(started.ready ? {} : { code: 'NOT_READY', message: started.warnings.join('; ') }),
|
|
@@ -413,15 +621,58 @@ export async function verify(root, options = {}) {
|
|
|
413
621
|
};
|
|
414
622
|
}
|
|
415
623
|
: skip('nothing was started'));
|
|
624
|
+
release?.();
|
|
416
625
|
}
|
|
417
626
|
const ok = checks.every((check) => check.ok);
|
|
418
|
-
const
|
|
627
|
+
const asked = (preflight.details?.problems ?? []).find((item) => item.question);
|
|
419
628
|
const shown = started ? listen : [];
|
|
629
|
+
const urls = shown.length ? appUrls(shown, await server.appHostsOrNone(root)) : [];
|
|
630
|
+
// The notices name the app's address as it is now (AI-M05: the port may have changed since),
|
|
631
|
+
// the first admin as dokebi.yaml and the seed configure it now (D-0115), and every fact they
|
|
632
|
+
// state as the project has it now (D-0137): the app verify started was ready, so each job
|
|
633
|
+
// queue it configures was bound.
|
|
634
|
+
const recipes = ok ? await readAppliedRecipes(root) : [];
|
|
635
|
+
let firstAdmin;
|
|
636
|
+
const readAdmin = () => {
|
|
637
|
+
firstAdmin ??= readFirstAdmin(root);
|
|
638
|
+
return firstAdmin;
|
|
639
|
+
};
|
|
640
|
+
const facts = recipes.some((entry) => entry.notices.some((notice) => factOf(notice)))
|
|
641
|
+
? await factTexts(recipes, await readProjectFacts(root, {
|
|
642
|
+
started: Boolean(started?.ready),
|
|
643
|
+
firstAdmin: readAdmin,
|
|
644
|
+
userStores: (realms) => probeUserStores(root, realms),
|
|
645
|
+
}))
|
|
646
|
+
: undefined;
|
|
647
|
+
const tellUser = ok
|
|
648
|
+
? tellUserLines(recipes, {
|
|
649
|
+
urls,
|
|
650
|
+
...(project.web.port > 0 ? { port: project.web.port } : {}),
|
|
651
|
+
},
|
|
652
|
+
// D-0098: the views of apps made outside a recipe, with who chose them.
|
|
653
|
+
await readRecordedChoices(root), recipes.some((entry) => entry.notices.some(namesFirstAdmin))
|
|
654
|
+
? firstAdminPhrase(await readAdmin())
|
|
655
|
+
: undefined, facts)
|
|
656
|
+
: [];
|
|
657
|
+
const finalReport = finalReportLines(ok, new Set(inspection?.plugins.flatMap((item) => item.installed?.id ?? []) ?? []), allowed);
|
|
658
|
+
if (finalReport.length)
|
|
659
|
+
debug('verify: final report reminders', { lines: finalReport.length });
|
|
420
660
|
return {
|
|
421
661
|
ok,
|
|
422
662
|
checks,
|
|
423
|
-
urls
|
|
663
|
+
urls,
|
|
424
664
|
listen: shown,
|
|
425
|
-
...(
|
|
665
|
+
...(tests ? { tests } : {}),
|
|
666
|
+
...(tellUser.length
|
|
667
|
+
? {
|
|
668
|
+
tellUser: tellUserWithReport(tellUser, finalReport, finalReport.includes(DB_PRACTICE_REPORT) ? await readDbPracticeRules(root) : []),
|
|
669
|
+
relay: finalReport.length ? REPORT_RELAY : TELL_USER_RELAY,
|
|
670
|
+
}
|
|
671
|
+
: {}),
|
|
672
|
+
...(finalReport.length ? { finalReport } : {}),
|
|
673
|
+
...(asked?.question
|
|
674
|
+
? { waitForUser: asked.waitForUser ?? true, question: asked.question, next: asked.next ?? '' }
|
|
675
|
+
: {}),
|
|
676
|
+
...(warnings.length ? { warnings } : {}),
|
|
426
677
|
};
|
|
427
678
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RecordedChoice } from './recipe-record.js';
|
|
2
|
+
/**
|
|
3
|
+
* The longest `--ai-reason` text, one line for the user like a catalog defaultReason: of a recipe
|
|
4
|
+
* answer (D-0095) and of the view `app add` records (D-0098). One rule, `aiReasonFits`, for both.
|
|
5
|
+
*/
|
|
6
|
+
export declare const AI_REASON_MAX = 200;
|
|
7
|
+
/** The id `--ai-chose` and `--ai-reason` name on `app add`. */
|
|
8
|
+
export declare const VIEW_ID = "view";
|
|
9
|
+
/** The view answer of a project without browser screens (recipe.ts NO_SCREENS), and of `--view none`. */
|
|
10
|
+
export declare const NO_VIEW = "none";
|
|
11
|
+
/** What `app add --view` takes: EJS pages, or no views (a JSON API). */
|
|
12
|
+
export type AppView = 'ejs' | 'none';
|
|
13
|
+
/**
|
|
14
|
+
* The view the project's latest recipe recorded (`view` answer), if any: `none` for the api
|
|
15
|
+
* recipe, so `app add` makes an app without views there unless a flag says otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function projectView(root: string): Promise<string | undefined>;
|
|
18
|
+
/** Who chose the view, as `app add` was told (`--answers-from`, `--ai-chose`, `--ai-reason`). */
|
|
19
|
+
export interface ViewSourceFlags {
|
|
20
|
+
readonly answersFrom?: 'user' | 'defaults';
|
|
21
|
+
readonly aiChose?: readonly string[];
|
|
22
|
+
readonly aiReasons?: Readonly<Record<string, string>>;
|
|
23
|
+
}
|
|
24
|
+
/** A trimmed `--ai-reason` text is one line of 1 to AI_REASON_MAX characters. */
|
|
25
|
+
export declare function aiReasonFits(reason: string): boolean;
|
|
26
|
+
/** The value of the view question for an app made with these flags. */
|
|
27
|
+
export declare function appViewValue(inertia: boolean, ssr: boolean, none?: boolean): string;
|
|
28
|
+
/**
|
|
29
|
+
* Checks the source flags of `app add` before anything changes: `--ai-chose` names only `view`,
|
|
30
|
+
* `--ai-reason` only an id of `--ai-chose`, and a reason is one line of 1 to 200 characters.
|
|
31
|
+
*/
|
|
32
|
+
export declare function checkViewSource(flags: ViewSourceFlags): void;
|
|
33
|
+
/**
|
|
34
|
+
* The record of an app's view: `ai` with `--ai-chose view` (or `--answers-from defaults`),
|
|
35
|
+
* `user` with `--answers-from user`, `unstated` for an EJS app or one without views, without
|
|
36
|
+
* either (not the default, so the user hears about it); none for an Inertia app without either
|
|
37
|
+
* (the default), nor for an app without views the project's recipe implied (`inherited`: the
|
|
38
|
+
* recipe's own line already says who chose no screens).
|
|
39
|
+
*/
|
|
40
|
+
export declare function appViewChoice(app: string, value: string, flags: ViewSourceFlags, now?: Date, inherited?: boolean): RecordedChoice | undefined;
|
|
41
|
+
/** The warning of an EJS app (or one without views) whose chooser was not stated (D-0098). */
|
|
42
|
+
export declare function unstatedViewWarning(app: string, value?: string): {
|
|
43
|
+
code: 'VIEW_SOURCE_UNSTATED';
|
|
44
|
+
path: string;
|
|
45
|
+
hint: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* After `app add` (or `add` with the first app): records the app's view with its source and adds
|
|
49
|
+
* VIEW_SOURCE_UNSTATED to the result's warnings for an EJS app without a source. A dry run only
|
|
50
|
+
* warns. The result is the command's own; it is returned with the warning added.
|
|
51
|
+
*/
|
|
52
|
+
export declare function recordAppView<T extends {
|
|
53
|
+
warnings?: object[];
|
|
54
|
+
appView?: {
|
|
55
|
+
value: string;
|
|
56
|
+
from: 'flags' | 'recipe';
|
|
57
|
+
};
|
|
58
|
+
}>(root: string, result: T, app: string, value: string, flags: ViewSourceFlags, dryRun: boolean): Promise<T>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// D-0098: the view of an app is a product choice whose default is Inertia + Vue. Outside a recipe
|
|
2
|
+
// it is made by `dokebi app add <name>` (EJS unless --inertia or --view none; none when the
|
|
3
|
+
// project's recipe built no browser screens, the api recipe) and by the first app of
|
|
4
|
+
// `dokebi add <apps host>` (EJS). These commands record it in `.dokebi/recipe.json` (`choices`)
|
|
5
|
+
// with who made it, the same way D-0095 records recipe answers, so verify's tellUser says it.
|
|
6
|
+
import { debug } from './diagnostics.js';
|
|
7
|
+
import { CliError } from './errors.js';
|
|
8
|
+
/**
|
|
9
|
+
* The longest `--ai-reason` text, one line for the user like a catalog defaultReason: of a recipe
|
|
10
|
+
* answer (D-0095) and of the view `app add` records (D-0098). One rule, `aiReasonFits`, for both.
|
|
11
|
+
*/
|
|
12
|
+
export const AI_REASON_MAX = 200;
|
|
13
|
+
/** The id `--ai-chose` and `--ai-reason` name on `app add`. */
|
|
14
|
+
export const VIEW_ID = 'view';
|
|
15
|
+
/** The view answer of a project without browser screens (recipe.ts NO_SCREENS), and of `--view none`. */
|
|
16
|
+
export const NO_VIEW = 'none';
|
|
17
|
+
/**
|
|
18
|
+
* The view the project's latest recipe recorded (`view` answer), if any: `none` for the api
|
|
19
|
+
* recipe, so `app add` makes an app without views there unless a flag says otherwise.
|
|
20
|
+
*/
|
|
21
|
+
export async function projectView(root) {
|
|
22
|
+
const { readAppliedRecipes } = await import('./recipe-record.js');
|
|
23
|
+
const recipes = await readAppliedRecipes(root);
|
|
24
|
+
for (const recipe of [...recipes].reverse()) {
|
|
25
|
+
const value = recipe.answers[VIEW_ID]?.value;
|
|
26
|
+
if (value !== undefined)
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/** A trimmed `--ai-reason` text is one line of 1 to AI_REASON_MAX characters. */
|
|
32
|
+
export function aiReasonFits(reason) {
|
|
33
|
+
return reason.length > 0 && reason.length <= AI_REASON_MAX && !/[\r\n]/.test(reason);
|
|
34
|
+
}
|
|
35
|
+
/** The value of the view question for an app made with these flags. */
|
|
36
|
+
export function appViewValue(inertia, ssr, none = false) {
|
|
37
|
+
return inertia ? (ssr ? 'inertia-vue-ssr' : 'inertia-vue') : none ? NO_VIEW : 'ejs';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Checks the source flags of `app add` before anything changes: `--ai-chose` names only `view`,
|
|
41
|
+
* `--ai-reason` only an id of `--ai-chose`, and a reason is one line of 1 to 200 characters.
|
|
42
|
+
*/
|
|
43
|
+
export function checkViewSource(flags) {
|
|
44
|
+
for (const id of flags.aiChose ?? [])
|
|
45
|
+
if (id !== VIEW_ID)
|
|
46
|
+
throw new CliError('AI_CHOICE_UNKNOWN', `app add records only the view, not ${id}`, {
|
|
47
|
+
details: { aiChose: id, questions: [VIEW_ID], fix: '--ai-chose view' },
|
|
48
|
+
phase: 'arguments',
|
|
49
|
+
});
|
|
50
|
+
for (const [id, text] of Object.entries(flags.aiReasons ?? {})) {
|
|
51
|
+
if (!(flags.aiChose ?? []).includes(id))
|
|
52
|
+
throw new CliError('AI_REASON_WITHOUT_CHOICE', `--ai-reason ${id} is given, but ${id} is not in --ai-chose`, {
|
|
53
|
+
details: {
|
|
54
|
+
question: id,
|
|
55
|
+
aiChose: [...(flags.aiChose ?? [])],
|
|
56
|
+
fix: 'add --ai-chose view if you chose the view because the user left it to you',
|
|
57
|
+
},
|
|
58
|
+
phase: 'arguments',
|
|
59
|
+
});
|
|
60
|
+
const reason = text.trim();
|
|
61
|
+
if (!aiReasonFits(reason))
|
|
62
|
+
throw new CliError('INVALID_AI_REASON', `--ai-reason ${id} must be one line of 1 to ${AI_REASON_MAX} characters`, {
|
|
63
|
+
details: {
|
|
64
|
+
question: id,
|
|
65
|
+
length: reason.length,
|
|
66
|
+
fix: 'give one short line the user reads: why you chose this view',
|
|
67
|
+
},
|
|
68
|
+
phase: 'arguments',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The record of an app's view: `ai` with `--ai-chose view` (or `--answers-from defaults`),
|
|
74
|
+
* `user` with `--answers-from user`, `unstated` for an EJS app or one without views, without
|
|
75
|
+
* either (not the default, so the user hears about it); none for an Inertia app without either
|
|
76
|
+
* (the default), nor for an app without views the project's recipe implied (`inherited`: the
|
|
77
|
+
* recipe's own line already says who chose no screens).
|
|
78
|
+
*/
|
|
79
|
+
export function appViewChoice(app, value, flags, now = new Date(), inherited = false) {
|
|
80
|
+
const recordedAt = now.toISOString();
|
|
81
|
+
if ((flags.aiChose ?? []).includes(VIEW_ID) || flags.answersFrom === 'defaults') {
|
|
82
|
+
const reason = flags.aiReasons?.[VIEW_ID]?.trim();
|
|
83
|
+
return {
|
|
84
|
+
app,
|
|
85
|
+
id: VIEW_ID,
|
|
86
|
+
value,
|
|
87
|
+
answeredBy: 'ai',
|
|
88
|
+
...(reason ? { aiReason: reason } : {}),
|
|
89
|
+
recordedAt,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (flags.answersFrom === 'user')
|
|
93
|
+
return { app, id: VIEW_ID, value, answeredBy: 'user', recordedAt };
|
|
94
|
+
if (inherited) {
|
|
95
|
+
debug('app view: no views, as the project recipe built no screens', { app, value });
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (value === 'ejs' || value === NO_VIEW) {
|
|
99
|
+
debug('app view: a view other than the default without a stated source', { app, value });
|
|
100
|
+
return { app, id: VIEW_ID, value, answeredBy: 'unstated', recordedAt };
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
/** The warning of an EJS app (or one without views) whose chooser was not stated (D-0098). */
|
|
105
|
+
export function unstatedViewWarning(app, value = 'ejs') {
|
|
106
|
+
const what = value === NO_VIEW ? 'an app without views (--view none)' : 'EJS';
|
|
107
|
+
return {
|
|
108
|
+
code: 'VIEW_SOURCE_UNSTATED',
|
|
109
|
+
path: `src/apps/${app}`,
|
|
110
|
+
hint: `The app ${app} is ${what}, which is not the default view (Inertia + Vue, npx dokebi app add <name> --inertia). verify's tellUser tells the user that nobody recorded who chose it. Next time say it: --answers-from user when the user or the request chose it, --ai-chose view --ai-reason view=<why> when the user left the view to you.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* After `app add` (or `add` with the first app): records the app's view with its source and adds
|
|
115
|
+
* VIEW_SOURCE_UNSTATED to the result's warnings for an EJS app without a source. A dry run only
|
|
116
|
+
* warns. The result is the command's own; it is returned with the warning added.
|
|
117
|
+
*/
|
|
118
|
+
export async function recordAppView(root, result, app, value, flags, dryRun) {
|
|
119
|
+
// app add says what the app really got (an app without views in an api project).
|
|
120
|
+
const actual = result.appView?.value ?? value;
|
|
121
|
+
const choice = appViewChoice(app, actual, flags, new Date(), result.appView?.from === 'recipe');
|
|
122
|
+
if (!choice)
|
|
123
|
+
return result;
|
|
124
|
+
if (!dryRun) {
|
|
125
|
+
const { saveRecordedChoice } = await import('./recipe-record.js');
|
|
126
|
+
await saveRecordedChoice(root, choice);
|
|
127
|
+
}
|
|
128
|
+
if (choice.answeredBy !== 'unstated')
|
|
129
|
+
return result;
|
|
130
|
+
return { ...result, warnings: [...(result.warnings ?? []), unstatedViewWarning(app, actual)] };
|
|
131
|
+
}
|
package/dist/vite-watch.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Run by `dokebi dev` (D-0044) for one Vite config: a watch build that never empties its outDir.
|
|
2
|
-
// The
|
|
3
|
-
//
|
|
2
|
+
// The templates of @dokebi/app write `emptyOutDir: false` (a rolling deploy keeps the previous
|
|
3
|
+
// chunks), but a vite.config of a project created before them still says `true`: the browser
|
|
4
|
+
// build (outDir dist/) would then delete dist/ssr/ on every rebuild, which `npm run build` (one
|
|
5
|
+
// build after the other) survives but two watchers do not. So dev overrides it for every config.
|
|
4
6
|
// Every finished build is reported to dev over the IPC channel ({ built: true } or
|
|
5
7
|
// { failed: true }); output goes to the terminal as usual.
|
|
6
8
|
import { join } from 'node:path';
|