@dokebi/cli 0.2.5 → 0.4.0

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.
Files changed (81) hide show
  1. package/AI.md +181 -87
  2. package/README.md +55 -24
  3. package/catalog.json +2199 -0
  4. package/dist/app-urls.d.ts +21 -0
  5. package/dist/app-urls.js +94 -0
  6. package/dist/banner-art.d.ts +4 -0
  7. package/dist/banner-art.js +31 -0
  8. package/dist/banner.d.ts +25 -0
  9. package/dist/banner.js +202 -0
  10. package/dist/bin.d.ts +2 -1
  11. package/dist/bin.js +475 -46
  12. package/dist/catalog.d.ts +161 -0
  13. package/dist/catalog.js +110 -0
  14. package/dist/check/form-rules.d.ts +13 -0
  15. package/dist/check/form-rules.js +224 -0
  16. package/dist/check/index.d.ts +23 -0
  17. package/dist/check/index.js +70 -0
  18. package/dist/check/packages.d.ts +17 -0
  19. package/dist/check/packages.js +161 -0
  20. package/dist/check/problems.d.ts +19 -0
  21. package/dist/check/problems.js +22 -0
  22. package/dist/check/project-rules.d.ts +10 -0
  23. package/dist/check/project-rules.js +142 -0
  24. package/dist/check/source-rules.d.ts +28 -0
  25. package/dist/check/source-rules.js +273 -0
  26. package/dist/check/typescript.d.ts +46 -0
  27. package/dist/check/typescript.js +168 -0
  28. package/dist/commands.d.ts +18 -0
  29. package/dist/commands.js +372 -0
  30. package/dist/config-edit.d.ts +5 -4
  31. package/dist/config-edit.js +9 -8
  32. package/dist/configuration.d.ts +1 -0
  33. package/dist/configuration.js +2 -1
  34. package/dist/dev-infra.d.ts +62 -0
  35. package/dist/dev-infra.js +215 -0
  36. package/dist/env-file.d.ts +24 -0
  37. package/dist/env-file.js +100 -0
  38. package/dist/generator.d.ts +24 -1
  39. package/dist/generator.js +57 -47
  40. package/dist/guide.d.ts +29 -0
  41. package/dist/guide.js +90 -0
  42. package/dist/install-plan.d.ts +8 -0
  43. package/dist/install-plan.js +21 -4
  44. package/dist/layout.d.ts +5 -4
  45. package/dist/layout.js +8 -7
  46. package/dist/log-writer-control.d.ts +55 -0
  47. package/dist/log-writer-control.js +327 -0
  48. package/dist/log-writer.js +53 -12
  49. package/dist/manifest.d.ts +28 -0
  50. package/dist/manifest.js +77 -2
  51. package/dist/npm-client.d.ts +49 -0
  52. package/dist/npm-client.js +162 -41
  53. package/dist/output.d.ts +1 -1
  54. package/dist/output.js +14 -8
  55. package/dist/plugin-lifecycle.d.ts +7 -0
  56. package/dist/plugin-lifecycle.js +21 -3
  57. package/dist/plugins-command.d.ts +47 -0
  58. package/dist/plugins-command.js +189 -0
  59. package/dist/project-guide.d.ts +36 -1
  60. package/dist/project-guide.js +141 -18
  61. package/dist/project-inspector.d.ts +10 -0
  62. package/dist/project-inspector.js +51 -2
  63. package/dist/project-state.d.ts +13 -2
  64. package/dist/project-state.js +8 -6
  65. package/dist/project.d.ts +89 -7
  66. package/dist/project.js +835 -85
  67. package/dist/recipe-record.d.ts +24 -0
  68. package/dist/recipe-record.js +67 -0
  69. package/dist/recipe.d.ts +120 -0
  70. package/dist/recipe.js +793 -0
  71. package/dist/registry.d.ts +79 -0
  72. package/dist/registry.js +212 -0
  73. package/dist/rotating-log.d.ts +12 -0
  74. package/dist/rotating-log.js +19 -0
  75. package/dist/server.d.ts +94 -4
  76. package/dist/server.js +624 -64
  77. package/dist/verify.d.ts +62 -0
  78. package/dist/verify.js +380 -0
  79. package/dist/vite-host-process.d.ts +20 -0
  80. package/dist/vite-host-process.js +117 -0
  81. package/package.json +30 -6
@@ -0,0 +1,46 @@
1
+ /** The part of a TypeScript 7 AST node the rules read. Other fields are read with `field`. */
2
+ export interface TsNode {
3
+ readonly kind: number;
4
+ readonly pos: number;
5
+ readonly end: number;
6
+ readonly parent?: TsNode;
7
+ forEachChild(visitor: (node: TsNode) => unknown): unknown;
8
+ getStart(): number;
9
+ }
10
+ export interface TsSourceFile extends TsNode {
11
+ readonly fileName: string;
12
+ readonly text: string;
13
+ getLineAndCharacterOfPosition(position: number): {
14
+ line: number;
15
+ character: number;
16
+ };
17
+ }
18
+ /** A child node of `node` by property name (`expression`, `block`, `name`, …). */
19
+ export declare function field(node: TsNode, name: string): TsNode | undefined;
20
+ /** A node list of `node` by property name (`arguments`, `statements`, `modifiers`, …). */
21
+ export declare function nodes(node: TsNode, name: string): readonly TsNode[];
22
+ /** A primitive property (`text` of identifiers and literals, `isExportEquals`, …). */
23
+ export declare function property(node: TsNode, name: string): unknown;
24
+ /** SyntaxKind names the rules use; each must exist in the loaded TypeScript. */
25
+ declare const KIND_NAMES: readonly ['ArrowFunction', 'Block', 'CallExpression', 'CatchClause', 'Constructor', 'Decorator', 'DeclareKeyword', 'EnumDeclaration', 'ExportAssignment', 'ExportDeclaration', 'ExpressionStatement', 'ExternalModuleReference', 'FunctionExpression', 'Identifier', 'ImportDeclaration', 'ImportEqualsDeclaration', 'ImportKeyword', 'ImportType', 'InterfaceDeclaration', 'LiteralType', 'ModuleBlock', 'ModuleDeclaration', 'NewExpression', 'NoSubstitutionTemplateLiteral', 'OverrideKeyword', 'Parameter', 'ParenthesizedExpression', 'PrivateKeyword', 'PropertyAccessExpression', 'ProtectedKeyword', 'PublicKeyword', 'ReadonlyKeyword', 'StringLiteral', 'TypeAliasDeclaration', 'TypeAssertionExpression'];
26
+ export type KindName = (typeof KIND_NAMES)[number];
27
+ export type Kinds = Readonly<Record<KindName, number>>;
28
+ /** An open TypeScript project: its user source files and a thenable test over the checker. */
29
+ export interface TypeScriptProject {
30
+ readonly version: string;
31
+ readonly kinds: Kinds;
32
+ /** Program files under the root, outside node_modules, without declaration files. */
33
+ readonly files: readonly TsSourceFile[];
34
+ /** Milliseconds to start tsgo and load the program (the part a check cannot avoid). */
35
+ readonly loadMs: number;
36
+ /** For each expression: is its type a Promise or PromiseLike (or a union with one)? */
37
+ promises(expressions: readonly TsNode[]): boolean[];
38
+ close(): void;
39
+ }
40
+ /**
41
+ * Starts the project's TypeScript and loads the program of `<root>/tsconfig.json`. The caller
42
+ * closes it. Throws TYPESCRIPT_UNAVAILABLE (TypeScript missing, older than 7, or its API changed)
43
+ * and TSCONFIG_INVALID (the program cannot be loaded).
44
+ */
45
+ export declare function openTypeScript(root: string): Promise<TypeScriptProject>;
46
+ export {};
@@ -0,0 +1,168 @@
1
+ // The project's TypeScript for `dokebi check` (D-0072). TypeScript 7 (the native port that init
2
+ // installs) exposes its parser and checker to JavaScript through `typescript/unstable/sync`: the
3
+ // API starts the project's tsgo binary and talks to it over a pipe. The CLI does not depend on
4
+ // TypeScript; it loads the copy the project installed, so the AST and the types are exactly what
5
+ // `npm run typecheck` sees. The API is marked unstable, so only the few calls below are used and
6
+ // a missing or changed API is a coded failure (TYPESCRIPT_UNAVAILABLE), never a silent pass.
7
+ import { createRequire } from 'node:module';
8
+ import { join, sep } from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ import { debug } from '../diagnostics.js';
11
+ import { CliError } from '../errors.js';
12
+ /** A child node of `node` by property name (`expression`, `block`, `name`, …). */
13
+ export function field(node, name) {
14
+ const value = node[name];
15
+ return value && typeof value === 'object' && 'kind' in value ? value : undefined;
16
+ }
17
+ /** A node list of `node` by property name (`arguments`, `statements`, `modifiers`, …). */
18
+ export function nodes(node, name) {
19
+ const value = node[name];
20
+ return Array.isArray(value) ? value : [];
21
+ }
22
+ /** A primitive property (`text` of identifiers and literals, `isExportEquals`, …). */
23
+ export function property(node, name) {
24
+ return node[name];
25
+ }
26
+ /** SyntaxKind names the rules use; each must exist in the loaded TypeScript. */
27
+ const KIND_NAMES = [
28
+ 'ArrowFunction',
29
+ 'Block',
30
+ 'CallExpression',
31
+ 'CatchClause',
32
+ 'Constructor',
33
+ 'Decorator',
34
+ 'DeclareKeyword',
35
+ 'EnumDeclaration',
36
+ 'ExportAssignment',
37
+ 'ExportDeclaration',
38
+ 'ExpressionStatement',
39
+ 'ExternalModuleReference',
40
+ 'FunctionExpression',
41
+ 'Identifier',
42
+ 'ImportDeclaration',
43
+ 'ImportEqualsDeclaration',
44
+ 'ImportKeyword',
45
+ 'ImportType',
46
+ 'InterfaceDeclaration',
47
+ 'LiteralType',
48
+ 'ModuleBlock',
49
+ 'ModuleDeclaration',
50
+ 'NewExpression',
51
+ 'NoSubstitutionTemplateLiteral',
52
+ 'OverrideKeyword',
53
+ 'Parameter',
54
+ 'ParenthesizedExpression',
55
+ 'PrivateKeyword',
56
+ 'PropertyAccessExpression',
57
+ 'ProtectedKeyword',
58
+ 'PublicKeyword',
59
+ 'ReadonlyKeyword',
60
+ 'StringLiteral',
61
+ 'TypeAliasDeclaration',
62
+ 'TypeAssertionExpression',
63
+ ];
64
+ function unavailable(root, message, cause) {
65
+ return new CliError('TYPESCRIPT_UNAVAILABLE', message, {
66
+ ...(cause === undefined ? {} : { cause }),
67
+ details: {
68
+ root,
69
+ hint: 'npm install in the project (init installs typescript 7 as a devDependency); dokebi check uses the project TypeScript API typescript/unstable/sync',
70
+ },
71
+ });
72
+ }
73
+ /**
74
+ * Starts the project's TypeScript and loads the program of `<root>/tsconfig.json`. The caller
75
+ * closes it. Throws TYPESCRIPT_UNAVAILABLE (TypeScript missing, older than 7, or its API changed)
76
+ * and TSCONFIG_INVALID (the program cannot be loaded).
77
+ */
78
+ export async function openTypeScript(root) {
79
+ const began = performance.now();
80
+ const require = createRequire(join(root, 'package.json'));
81
+ let sync;
82
+ let ast;
83
+ let version;
84
+ try {
85
+ version = String(require('typescript/package.json').version);
86
+ sync = await import(pathToFileURL(require.resolve('typescript/unstable/sync')).href);
87
+ ast = await import(pathToFileURL(require.resolve('typescript/unstable/ast')).href);
88
+ }
89
+ catch (cause) {
90
+ throw unavailable(root, 'Cannot load the TypeScript 7 API of this project', cause);
91
+ }
92
+ const kinds = {};
93
+ for (const name of KIND_NAMES) {
94
+ const value = ast.SyntaxKind?.[name];
95
+ if (typeof value !== 'number')
96
+ throw unavailable(root, `TypeScript ${version} has no SyntaxKind.${name}`);
97
+ kinds[name] = value;
98
+ }
99
+ const anyFlags = (sync.TypeFlags?.Any ?? 0) | (sync.TypeFlags?.Unknown ?? 0);
100
+ if (typeof sync.API !== 'function' || !anyFlags)
101
+ throw unavailable(root, `TypeScript ${version} has no usable unstable/sync API`);
102
+ const config = join(root, 'tsconfig.json');
103
+ let api;
104
+ try {
105
+ api = new sync.API({ cwd: root });
106
+ }
107
+ catch (cause) {
108
+ throw unavailable(root, 'Cannot start the TypeScript API (tsgo)', cause);
109
+ }
110
+ let project;
111
+ try {
112
+ project = api.updateSnapshot({ openProject: config }).getProject(config);
113
+ }
114
+ catch (cause) {
115
+ api.close();
116
+ throw new CliError('TSCONFIG_INVALID', `TypeScript cannot load ${config}`, {
117
+ cause,
118
+ details: { tsconfig: config },
119
+ });
120
+ }
121
+ if (!project) {
122
+ api.close();
123
+ throw new CliError('TSCONFIG_INVALID', `TypeScript has no project for ${config}`, {
124
+ details: { tsconfig: config },
125
+ });
126
+ }
127
+ const prefix = root.endsWith(sep) ? root : root + sep;
128
+ const modules = `${prefix}node_modules${sep}`;
129
+ const files = [];
130
+ for (const name of project.program.getSourceFileNames()) {
131
+ const native = name.split('/').join(sep);
132
+ if (!native.startsWith(prefix) || native.startsWith(modules))
133
+ continue;
134
+ if (!/\.(c|m)?tsx?$/.test(native) || /\.d\.(c|m)?ts$/.test(native))
135
+ continue;
136
+ const file = project.program.getSourceFile(name);
137
+ if (file)
138
+ files.push(file);
139
+ }
140
+ // The program loads lazily on the first file request, so the load ends here.
141
+ const loadMs = Math.round(performance.now() - began);
142
+ debug('check: TypeScript program loaded', { version, files: files.length, loadMs });
143
+ const checker = project.checker;
144
+ // A Promise (or PromiseLike) — not any object with a `then`: Fastify's reply and query
145
+ // builders are thenables whose methods return themselves, and leaving those alone is fine.
146
+ const promiseType = (type, depth) => {
147
+ if (!type || depth > 4 || type.flags & anyFlags)
148
+ return false;
149
+ if (type.isUnionType())
150
+ return (type.getTypes?.() ?? []).some((member) => promiseType(member, depth + 1));
151
+ const name = type.getSymbol?.()?.name;
152
+ return name === 'Promise' || name === 'PromiseLike';
153
+ };
154
+ return {
155
+ version,
156
+ kinds,
157
+ files,
158
+ loadMs,
159
+ promises(expressions) {
160
+ if (!expressions.length)
161
+ return [];
162
+ return checker.getTypeAtLocation(expressions).map((type) => promiseType(type, 0));
163
+ },
164
+ close() {
165
+ api.close();
166
+ },
167
+ };
168
+ }
@@ -0,0 +1,18 @@
1
+ /** One command of `dokebi --help`: its usage lines (format kept stable) and what it does. */
2
+ export interface CommandHelp {
3
+ readonly name: string;
4
+ readonly usage: string;
5
+ readonly summary: string;
6
+ }
7
+ /**
8
+ * Every command with its usage line. scripts/catalog/check.ts reads this list (from dist) to
9
+ * prove that an absent entry's `no-flag:<command> <flag>` still holds (D-0065).
10
+ */
11
+ export declare const COMMANDS: readonly CommandHelp[];
12
+ /** True when the usage line of `dokebi <command>` names `flag` (absent `no-flag:` rules). */
13
+ export declare function commandHasFlag(command: string, flag: string): boolean;
14
+ /** `dokebi <command> --help`: what each flag does, and one example (D-0065, AI-M01). */
15
+ export declare const COMMAND_DETAILS: Readonly<Record<string, {
16
+ readonly flags: readonly (readonly [string, string])[];
17
+ readonly example: string;
18
+ }>>;
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Every command with its usage line. scripts/catalog/check.ts reads this list (from dist) to
3
+ * prove that an absent entry's `no-flag:<command> <flag>` still holds (D-0065).
4
+ */
5
+ export const COMMANDS = [
6
+ {
7
+ name: 'guide',
8
+ usage: 'dokebi guide [--json]',
9
+ summary: 'AI first page: how to turn a one-line request into an app (ask, install, verify) — read this first',
10
+ },
11
+ {
12
+ name: 'init',
13
+ usage: 'dokebi init <directory> [--core <package|local.tgz>] [--dry-run] [--json]',
14
+ summary: 'Create a project (new folder, or an empty one such as .) with core, config, this CLI (@dokebi/cli) and TypeScript; prints next steps',
15
+ },
16
+ {
17
+ name: 'recipe list',
18
+ usage: 'dokebi recipe list [--json]',
19
+ summary: 'List recipes (blog, api, …): what each builds and which questions it asks',
20
+ },
21
+ {
22
+ name: 'recipe',
23
+ usage: 'dokebi recipe <name> [--dir <directory>] [--view <v>] [--db <d>] [--dev-infra <i>] [--admin <a>] [--port <n>] [--db-port <n>] [--answers-from <user|defaults>] [--defaults] [--dry-run] [--json]',
24
+ summary: "Without every answer: the questions to ask the user, then end your turn (installs nothing); with the user's answers: plan, then apply with --answers-from user",
25
+ },
26
+ {
27
+ name: 'plugins list',
28
+ usage: 'dokebi plugins list [--compatible] [--online] [--json]',
29
+ summary: 'Every Dokebi package with summary, capabilities and install state',
30
+ },
31
+ {
32
+ name: 'plugins search',
33
+ usage: 'dokebi plugins search <words|capability>... [--json]',
34
+ summary: 'Find packages by words or capability; nothing found returns what Dokebi does not have',
35
+ },
36
+ {
37
+ name: 'plugins info',
38
+ usage: 'dokebi plugins info <id|package> [--json]',
39
+ summary: 'One package: capabilities, variants, what it does not do, requirements, options',
40
+ },
41
+ {
42
+ name: 'plugins doc',
43
+ usage: 'dokebi plugins doc <id|package> [--json]',
44
+ summary: 'Path and content of the installed version AI.md (read it before writing code)',
45
+ },
46
+ {
47
+ name: 'add',
48
+ usage: 'dokebi add <package|local.tgz> [--set <path>=<value>]... [--adapter <pg|mysql2|mariadb|mongodb>] [--no-first-app] [--dry-run] [--json]',
49
+ summary: 'Install and register a plugin (and the plugins it requires); no version picks the newest one for this core',
50
+ },
51
+ {
52
+ name: 'upgrade',
53
+ usage: 'dokebi upgrade <plugin-id> [--to <version|package|local.tgz>] [--dry-run] [--json]',
54
+ summary: 'Change the version of an installed plugin',
55
+ },
56
+ {
57
+ name: 'remove',
58
+ usage: 'dokebi remove <plugin-id> [--dry-run] [--json]',
59
+ summary: 'Unregister and uninstall a plugin; user options are kept and marked',
60
+ },
61
+ {
62
+ name: 'config set',
63
+ usage: 'dokebi config set <plugin-id>.<path>=<value>... [--dry-run] [--json]',
64
+ summary: 'Set non-secret options in dokebi.yaml (checked against the plugin schema)',
65
+ },
66
+ {
67
+ name: 'config secret',
68
+ usage: 'dokebi config secret <plugin-id>.<path> [--dry-run] [--json]',
69
+ summary: 'Write a generated secret (never shown) into dokebi.yaml, e.g. dokebi-session.secrets',
70
+ },
71
+ {
72
+ name: 'dev-infra',
73
+ usage: 'dokebi dev-infra [--db-port <n>] [--dry-run] [--json]',
74
+ summary: 'Write compose.yaml, .env (generated password) and the dokebi.yaml address of the development database',
75
+ },
76
+ {
77
+ name: 'migrate',
78
+ usage: 'dokebi migrate [--dry-run] [--json]',
79
+ summary: 'Move a 0.1.x JSON-configured project to dokebi.yaml',
80
+ },
81
+ {
82
+ name: 'inspect',
83
+ usage: 'dokebi inspect [--verbose] [--json]',
84
+ summary: 'Show plugins, versions, documentation and optionsSchema paths, diagnostics',
85
+ },
86
+ {
87
+ name: 'doctor',
88
+ usage: 'dokebi doctor [--verbose] [--json]',
89
+ summary: 'Run all checks; exit 1 on an error diagnostic (repairs nothing)',
90
+ },
91
+ {
92
+ name: 'check',
93
+ usage: 'dokebi check [--json]',
94
+ summary: 'User-code conventions: empty catch, floating promises, syntax Node cannot strip, private or missing @dokebi imports, edited generated files, committable secrets',
95
+ },
96
+ {
97
+ name: 'verify',
98
+ usage: 'dokebi verify [--wait-ms <ms>] [--json]',
99
+ summary: 'doctor, check, typecheck, build, start, HTTP checks and stop; ok only when all pass (say done only then)',
100
+ },
101
+ {
102
+ name: 'app add',
103
+ usage: 'dokebi app add <name> [--domain <host>]... [--template <name>] [--inertia [--ssr]] [--dry-run] [--json]',
104
+ summary: 'Create the app src/apps/<name> (EJS, or Vue with --inertia) and map its domains',
105
+ },
106
+ {
107
+ name: 'app remove',
108
+ usage: 'dokebi app remove <name> [--dry-run] [--json]',
109
+ summary: 'Unregister an app (unlink its domains first); its folder is kept',
110
+ },
111
+ {
112
+ name: 'app link',
113
+ usage: 'dokebi app link <name> <domain> [--template <name>] [--dry-run] [--json]',
114
+ summary: 'Serve an app on one more domain, optionally with another template',
115
+ },
116
+ {
117
+ name: 'app unlink',
118
+ usage: 'dokebi app unlink <domain> [--dry-run] [--json]',
119
+ summary: 'Remove a domain from its app',
120
+ },
121
+ { name: 'app list', usage: 'dokebi app list [--json]', summary: 'List apps and their domains' },
122
+ {
123
+ name: 'module add',
124
+ usage: 'dokebi module add <name> [--no-views] [--dry-run] [--json]',
125
+ summary: 'Create the feature module src/shared/modules/<name>',
126
+ },
127
+ {
128
+ name: 'module remove',
129
+ usage: 'dokebi module remove <name> [--dry-run] [--json]',
130
+ summary: 'Unregister a feature module; its folder is kept',
131
+ },
132
+ { name: 'module list', usage: 'dokebi module list [--json]', summary: 'List feature modules' },
133
+ {
134
+ name: 'dev',
135
+ usage: 'dokebi dev',
136
+ summary: 'Run the app in the foreground and restart it when src or dokebi.yaml change',
137
+ },
138
+ {
139
+ name: 'start',
140
+ usage: 'dokebi start [--foreground] [--wait-ms <ms>] [--log-max-mb <MB>] [--log-files <n>] [--json]',
141
+ summary: 'Start the app in the background with rotated logs in .dokebi/run',
142
+ },
143
+ {
144
+ name: 'stop',
145
+ usage: 'dokebi stop [--timeout-ms <ms>] [--force] [--json]',
146
+ summary: 'Stop the background app (SIGTERM drain; SIGKILL with --force after the timeout)',
147
+ },
148
+ {
149
+ name: 'status',
150
+ usage: 'dokebi status [--json]',
151
+ summary: 'Show whether the background app runs (exit 3 when stopped)',
152
+ },
153
+ {
154
+ name: 'logs',
155
+ usage: 'dokebi logs [-n <lines>] [-f]',
156
+ summary: 'Print the last log lines of the background app; -f follows',
157
+ },
158
+ ];
159
+ /** True when the usage line of `dokebi <command>` names `flag` (absent `no-flag:` rules). */
160
+ export function commandHasFlag(command, flag) {
161
+ const usage = COMMANDS.find((item) => item.name === command)?.usage;
162
+ if (usage === undefined)
163
+ return false;
164
+ return new RegExp(`(^|[\\s\\[|])${flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=[\\s\\]|]|$)`).test(usage);
165
+ }
166
+ /** `dokebi <command> --help`: what each flag does, and one example (D-0065, AI-M01). */
167
+ export const COMMAND_DETAILS = {
168
+ guide: {
169
+ flags: [['--json', 'the guide as data: steps, rules, recipes, absent, commands']],
170
+ example: 'npx dokebi guide --json',
171
+ },
172
+ init: {
173
+ flags: [
174
+ ['--core <package|local.tgz>', 'core to install (default: @dokebi/core in this CLI range)'],
175
+ [
176
+ '<directory>',
177
+ 'a new folder, or an existing one that is empty or holds only .git, .npmrc, .npm, .npm-cache, .DS_Store, Thumbs.db, .idea, .vscode (init . works there); anything else is PROJECT_EXISTS',
178
+ ],
179
+ ['--dry-run', 'check the folder and print the next steps; nothing is installed or written'],
180
+ ],
181
+ example: 'npx -y @dokebi/cli init my-blog && cd my-blog && npx dokebi guide --json\nnpx -y @dokebi/cli init . --json # in an empty work folder',
182
+ },
183
+ 'recipe list': { flags: [], example: 'npx dokebi recipe list --json' },
184
+ recipe: {
185
+ flags: [
186
+ [
187
+ '--dir <directory>',
188
+ 'project folder; a new folder is created with init first (default: current project)',
189
+ ],
190
+ ['--view <v>', 'blog: ejs | inertia-vue | inertia-vue-ssr (inertia-react is absent)'],
191
+ ['--db <d>', 'postgres | mysql | mariadb | mongodb | none (sqlite is absent)'],
192
+ [
193
+ '--dev-infra <i>',
194
+ 'docker (compose.yaml, generated password) | url (you write the URL); only with a database',
195
+ ],
196
+ ['--admin <a>', 'blog: none | app-code (no auth plugin: the app writes the login)'],
197
+ ['--port <n>', 'web port (default 3000)'],
198
+ [
199
+ '--db-port <n>',
200
+ 'host port of the docker database (default: the database default, e.g. 5432)',
201
+ ],
202
+ [
203
+ '--answers-from <user|defaults>',
204
+ 'where the answers came from; applying needs it. user: the user answered these questions (ask, end your turn, wait). defaults: the user said to decide (same as --defaults). Recorded in `answersFrom` and `report`',
205
+ ],
206
+ [
207
+ '--defaults',
208
+ 'fill unanswered questions with defaults: only when the user left the choice to you; tell the user `report`',
209
+ ],
210
+ ['--dry-run', 'plan and preflight only; nothing is written'],
211
+ ],
212
+ example: 'npx dokebi recipe blog --json # the questions (exit 2, nothing installed): ask them and end your turn\nnpx dokebi recipe blog --view ejs --db postgres --dev-infra docker --admin none --answers-from user --dry-run --json',
213
+ },
214
+ 'plugins list': {
215
+ flags: [
216
+ ['--compatible', 'only packages whose core range accepts this project core'],
217
+ [
218
+ '--online',
219
+ 'ask the registry for the latest and compatible versions (CATALOG_OFFLINE when it cannot)',
220
+ ],
221
+ ],
222
+ example: 'npx dokebi plugins list --compatible --json',
223
+ },
224
+ 'plugins search': {
225
+ flags: [],
226
+ example: 'npx dokebi plugins search login --json # status: absent (auth)',
227
+ },
228
+ 'plugins info': { flags: [], example: 'npx dokebi plugins info db --json' },
229
+ 'plugins doc': { flags: [], example: 'npx dokebi plugins doc dokebi-db --json' },
230
+ add: {
231
+ flags: [
232
+ [
233
+ '<package>',
234
+ 'registry name (no version: newest for this core, see `resolved`) or a local .tgz',
235
+ ],
236
+ [
237
+ '--set <path>=<value>',
238
+ 'non-secret option of the added plugin (repeatable); secrets go in dokebi.yaml',
239
+ ],
240
+ [
241
+ '--adapter <id>',
242
+ '@dokebi/db: pg | mysql2 | mariadb | mongodb (adapter file, connection main, driver, @types/pg)',
243
+ ],
244
+ [
245
+ '--no-first-app',
246
+ '@dokebi/app: do not create the EJS app main (add the first app with app add)',
247
+ ],
248
+ ['--dry-run', 'plan only; packages may be downloaded into a temporary folder'],
249
+ ],
250
+ example: 'npx dokebi add @dokebi/db --adapter pg --dry-run --json',
251
+ },
252
+ upgrade: {
253
+ flags: [
254
+ ['--to <version|package|local.tgz>', 'target (default: latest)'],
255
+ ['--dry-run', 'plan only'],
256
+ ],
257
+ example: 'npx dokebi upgrade dokebi-web --to 0.4.0 --dry-run --json',
258
+ },
259
+ remove: {
260
+ flags: [['--dry-run', 'plan only']],
261
+ example: 'npx dokebi remove dokebi-health --dry-run --json',
262
+ },
263
+ 'config set': {
264
+ flags: [
265
+ ['<id>.<path>=<value>', 'JSON when it parses, else text; checked against the plugin schema'],
266
+ ['--dry-run', 'plan only'],
267
+ ],
268
+ example: 'npx dokebi config set dokebi-web.port=3000 --json',
269
+ },
270
+ 'config secret': {
271
+ flags: [
272
+ [
273
+ '<id>.<path>',
274
+ 'writes a random secret there (a list when the schema wants one); never printed',
275
+ ],
276
+ ['--dry-run', 'plan only'],
277
+ ],
278
+ example: 'npx dokebi config secret dokebi-session.secrets --json',
279
+ },
280
+ 'dev-infra': {
281
+ flags: [
282
+ ['--db-port <n>', 'host port of the database container (PORT_IN_USE names a free one)'],
283
+ ['--dry-run', 'plan only'],
284
+ ],
285
+ example: 'npx dokebi dev-infra --db-port 5433 --json && docker compose up -d --wait',
286
+ },
287
+ migrate: { flags: [['--dry-run', 'plan only']], example: 'npx dokebi migrate --dry-run --json' },
288
+ inspect: {
289
+ flags: [['--verbose', 'the whole inspection (default: a summary; --json is always complete)']],
290
+ example: 'npx dokebi inspect --json',
291
+ },
292
+ doctor: {
293
+ flags: [['--verbose', 'the whole inspection (default: a summary; --json is always complete)']],
294
+ example: 'npx dokebi doctor',
295
+ },
296
+ check: {
297
+ flags: [
298
+ [
299
+ '--json',
300
+ '{ ok, problems[{rule, file, line, message, fix}], files, ms }; exit 1 on a problem',
301
+ ],
302
+ ],
303
+ example: 'npx dokebi check --json\n# a justified empty catch or background promise: // dokebi-check: allow empty-catch <reason>',
304
+ },
305
+ verify: {
306
+ flags: [['--wait-ms <ms>', 'how long start waits for the started line (default 30000)']],
307
+ example: 'npx dokebi verify --json',
308
+ },
309
+ 'app add': {
310
+ flags: [
311
+ ['--domain <host>', 'host name served by the app (repeatable, no port)'],
312
+ ['--template <name>', 'views folder for those domains (default: default)'],
313
+ ['--inertia', 'Vue pages with Vite (adds vite.config.ts, the build script, vite and vue)'],
314
+ ['--ssr', 'with --inertia: server-rendered first HTML'],
315
+ ['--dry-run', 'plan only'],
316
+ ],
317
+ example: 'npx dokebi app add admin --domain admin.localhost --inertia --json',
318
+ },
319
+ 'app remove': {
320
+ flags: [['--dry-run', 'plan only']],
321
+ example: 'npx dokebi app remove admin --json',
322
+ },
323
+ 'app link': {
324
+ flags: [
325
+ ['--template <name>', 'views folder for this domain'],
326
+ ['--dry-run', 'plan only'],
327
+ ],
328
+ example: 'npx dokebi app link shop sale.localhost --template sale --json',
329
+ },
330
+ 'app unlink': {
331
+ flags: [['--dry-run', 'plan only']],
332
+ example: 'npx dokebi app unlink sale.localhost --json',
333
+ },
334
+ 'app list': { flags: [], example: 'npx dokebi app list --json' },
335
+ 'module add': {
336
+ flags: [
337
+ ['--no-views', 'routes answer JSON (no views folder)'],
338
+ ['--dry-run', 'plan only'],
339
+ ],
340
+ example: 'npx dokebi module add posts --json',
341
+ },
342
+ 'module remove': {
343
+ flags: [['--dry-run', 'plan only']],
344
+ example: 'npx dokebi module remove posts --json',
345
+ },
346
+ 'module list': { flags: [], example: 'npx dokebi module list --json' },
347
+ dev: { flags: [], example: 'npx dokebi dev' },
348
+ start: {
349
+ flags: [
350
+ ['--foreground', 'run in this terminal (systemd, Docker); no --json'],
351
+ ['--wait-ms <ms>', 'wait for the started line (default 10000)'],
352
+ ['--log-max-mb <MB>', 'rotate app.log at this size (default 50)'],
353
+ ['--log-files <n>', 'rotated files kept (default 5)'],
354
+ ],
355
+ example: 'npx dokebi start --json',
356
+ },
357
+ stop: {
358
+ flags: [
359
+ ['--timeout-ms <ms>', 'drain time before STOP_TIMEOUT (default 30000)'],
360
+ ['--force', 'SIGKILL after the timeout'],
361
+ ],
362
+ example: 'npx dokebi stop --json',
363
+ },
364
+ status: { flags: [], example: 'npx dokebi status --json' },
365
+ logs: {
366
+ flags: [
367
+ ['-n <lines>', 'last lines (default 100)'],
368
+ ['-f', 'follow until Ctrl+C'],
369
+ ],
370
+ example: 'npx dokebi logs -n 50',
371
+ },
372
+ };
@@ -13,11 +13,12 @@ export declare function setWarnings(sets: readonly SetOption[]): Warning[];
13
13
  export declare function applySetOptions(source: string, id: string, sets: readonly SetOption[]): string;
14
14
  export declare function applyConfigChanges(source: string, id: string, changes: readonly ConfigChange[]): string;
15
15
  /**
16
- * Removes `options.dokebi-app.apps.<app>` (D-0061 per-app settings) and the `apps` map when it is
17
- * left empty; a source without that entry is returned unchanged. dokebi-app refuses to start with
18
- * an entry for an app that is not defined (UNKNOWN_APP), so `dokebi app remove` calls this.
16
+ * Removes `options.<host>.apps.<app>` (D-0061 per-app settings of the apps host, D-0070) and the
17
+ * `apps` map when it is left empty; a source without that entry is returned unchanged. The host
18
+ * refuses to start with an entry for an app that is not defined (UNKNOWN_APP), so
19
+ * `dokebi app remove` calls this.
19
20
  */
20
- export declare function removeAppSettings(source: string, app: string): string;
21
+ export declare function removeAppSettings(source: string, host: string, app: string): string;
21
22
  /** Keeps a removed plugin's section and marks it for a later reinstall or manual cleanup. */
22
23
  export declare function markRemoved(source: string, id: string): {
23
24
  source: string;
@@ -110,19 +110,20 @@ export function applyConfigChanges(source, id, changes) {
110
110
  });
111
111
  }
112
112
  /**
113
- * Removes `options.dokebi-app.apps.<app>` (D-0061 per-app settings) and the `apps` map when it is
114
- * left empty; a source without that entry is returned unchanged. dokebi-app refuses to start with
115
- * an entry for an app that is not defined (UNKNOWN_APP), so `dokebi app remove` calls this.
113
+ * Removes `options.<host>.apps.<app>` (D-0061 per-app settings of the apps host, D-0070) and the
114
+ * `apps` map when it is left empty; a source without that entry is returned unchanged. The host
115
+ * refuses to start with an entry for an app that is not defined (UNKNOWN_APP), so
116
+ * `dokebi app remove` calls this.
116
117
  */
117
- export function removeAppSettings(source, app) {
118
- const path = ['options', 'dokebi-app', 'apps', app];
118
+ export function removeAppSettings(source, host, app) {
119
+ const path = ['options', host, 'apps', app];
119
120
  if (!parseDocument(source, { version: '1.2' }).hasIn(path))
120
121
  return source;
121
- return edit(source, 'dokebi-app', (document) => {
122
+ return edit(source, host, (document) => {
122
123
  document.deleteIn(path);
123
- const apps = document.getIn(['options', 'dokebi-app', 'apps'], true);
124
+ const apps = document.getIn(['options', host, 'apps'], true);
124
125
  if (isMap(apps) && apps.items.length === 0)
125
- document.deleteIn(['options', 'dokebi-app', 'apps']);
126
+ document.deleteIn(['options', host, 'apps']);
126
127
  });
127
128
  }
128
129
  /** Keeps a removed plugin's section and marks it for a later reinstall or manual cleanup. */
@@ -12,6 +12,7 @@ export declare const CONFIG_TEMPLATE = "# Settings for this machine. Not committ
12
12
  * file started from `options: {}`. Values are unchanged; only the layout is normalized.
13
13
  */
14
14
  export declare function renderConfig(document: Document): string;
15
+ /** Never committed: real settings, the legacy backup and the generated secrets in .env (D-0073). */
15
16
  export declare const CONFIG_IGNORES: string[];
16
17
  export declare function parseProjectConfig(source: string): ProjectConfig;
17
18
  export declare function addConfigSection(source: string, id: string): string;