@dokebi/cli 0.2.4 → 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.
- package/AI.md +181 -87
- package/README.md +55 -24
- package/catalog.json +2199 -0
- package/dist/app-urls.d.ts +21 -0
- package/dist/app-urls.js +94 -0
- package/dist/banner-art.d.ts +4 -0
- package/dist/banner-art.js +31 -0
- package/dist/banner.d.ts +25 -0
- package/dist/banner.js +202 -0
- package/dist/bin.d.ts +2 -1
- package/dist/bin.js +475 -46
- package/dist/catalog.d.ts +161 -0
- package/dist/catalog.js +110 -0
- package/dist/check/form-rules.d.ts +13 -0
- package/dist/check/form-rules.js +224 -0
- package/dist/check/index.d.ts +23 -0
- package/dist/check/index.js +70 -0
- package/dist/check/packages.d.ts +17 -0
- package/dist/check/packages.js +161 -0
- package/dist/check/problems.d.ts +19 -0
- package/dist/check/problems.js +22 -0
- package/dist/check/project-rules.d.ts +10 -0
- package/dist/check/project-rules.js +142 -0
- package/dist/check/source-rules.d.ts +28 -0
- package/dist/check/source-rules.js +273 -0
- package/dist/check/typescript.d.ts +46 -0
- package/dist/check/typescript.js +168 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +372 -0
- package/dist/config-edit.d.ts +7 -0
- package/dist/config-edit.js +17 -0
- package/dist/configuration.d.ts +1 -0
- package/dist/configuration.js +2 -1
- package/dist/dev-infra.d.ts +62 -0
- package/dist/dev-infra.js +215 -0
- package/dist/env-file.d.ts +24 -0
- package/dist/env-file.js +100 -0
- package/dist/generator.d.ts +24 -1
- package/dist/generator.js +57 -47
- package/dist/guide.d.ts +29 -0
- package/dist/guide.js +90 -0
- package/dist/install-plan.d.ts +8 -0
- package/dist/install-plan.js +21 -4
- package/dist/layout.d.ts +5 -4
- package/dist/layout.js +8 -7
- package/dist/log-writer-control.d.ts +55 -0
- package/dist/log-writer-control.js +327 -0
- package/dist/log-writer.js +53 -12
- package/dist/manifest.d.ts +28 -0
- package/dist/manifest.js +77 -2
- package/dist/npm-client.d.ts +49 -0
- package/dist/npm-client.js +162 -41
- package/dist/output.d.ts +1 -1
- package/dist/output.js +14 -8
- package/dist/plugin-lifecycle.d.ts +7 -0
- package/dist/plugin-lifecycle.js +21 -3
- package/dist/plugins-command.d.ts +47 -0
- package/dist/plugins-command.js +189 -0
- package/dist/project-guide.d.ts +36 -1
- package/dist/project-guide.js +141 -18
- package/dist/project-inspector.d.ts +10 -0
- package/dist/project-inspector.js +51 -2
- package/dist/project-state.d.ts +13 -2
- package/dist/project-state.js +8 -6
- package/dist/project.d.ts +89 -7
- package/dist/project.js +856 -85
- package/dist/recipe-record.d.ts +24 -0
- package/dist/recipe-record.js +67 -0
- package/dist/recipe.d.ts +120 -0
- package/dist/recipe.js +793 -0
- package/dist/registry.d.ts +79 -0
- package/dist/registry.js +212 -0
- package/dist/rotating-log.d.ts +12 -0
- package/dist/rotating-log.js +19 -0
- package/dist/server.d.ts +94 -4
- package/dist/server.js +624 -64
- package/dist/verify.d.ts +62 -0
- package/dist/verify.js +380 -0
- package/dist/vite-host-process.d.ts +20 -0
- package/dist/vite-host-process.js +117 -0
- package/package.json +30 -6
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type Catalog } from './catalog.js';
|
|
2
|
+
/** npm's built-in default registry. */
|
|
3
|
+
export declare const PUBLIC_REGISTRY = "https://registry.npmjs.org/";
|
|
4
|
+
/** Where npm would fetch a package from, as npm itself resolves it in a folder. */
|
|
5
|
+
export interface RegistrySettings {
|
|
6
|
+
/** Base URL ending with `/`, without credentials (reads are auth-free). */
|
|
7
|
+
readonly url: string;
|
|
8
|
+
/** `scope`: `@scope:registry` · `registry`: npm's `registry` · `default`: npm gave none. */
|
|
9
|
+
readonly from: 'scope' | 'registry' | 'default';
|
|
10
|
+
/** npm `offline=true`: npm itself would not touch the network, so neither does the CLI. */
|
|
11
|
+
readonly offline: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The registry npm would use for `name` in `cwd`: `@scope:registry` for a scoped package, else
|
|
15
|
+
* `registry`, from npm's own config resolution (CLI-less: env `npm_config_*`, the project
|
|
16
|
+
* `.npmrc` (the workspace root's for a member), the user `.npmrc`, the global `npmrc`, npm's
|
|
17
|
+
* default). The CLI asks npm (`npm config get`, about 0.1 s, no network) instead of reading the
|
|
18
|
+
* files itself, so precedence, `${VAR}` expansion and `npm_config_userconfig`/`globalconfig`
|
|
19
|
+
* are exactly npm's. When npm cannot answer, the public registry (npm's default) is used and
|
|
20
|
+
* the cause goes to the debug log.
|
|
21
|
+
*/
|
|
22
|
+
export declare function registryFor(name: string, cwd?: string): Promise<RegistrySettings>;
|
|
23
|
+
/** `npm config get a b c` prints `a=value` lines (one key alone prints just the value). */
|
|
24
|
+
export declare function parseConfigGet(stdout: string): Map<string, string>;
|
|
25
|
+
/** An http(s) registry URL ending with `/` and without `user:password@`, else undefined. */
|
|
26
|
+
export declare function registryBase(value: string | undefined): string | undefined;
|
|
27
|
+
export type VersionManifest = {
|
|
28
|
+
dokebi?: {
|
|
29
|
+
core?: string;
|
|
30
|
+
};
|
|
31
|
+
deprecated?: string;
|
|
32
|
+
};
|
|
33
|
+
export type Packument = ({
|
|
34
|
+
readonly status: 'found';
|
|
35
|
+
readonly versions: Readonly<Record<string, VersionManifest>>;
|
|
36
|
+
readonly latest: string | null;
|
|
37
|
+
} | {
|
|
38
|
+
readonly status: 'missing';
|
|
39
|
+
} | {
|
|
40
|
+
readonly status: 'offline';
|
|
41
|
+
readonly error: string;
|
|
42
|
+
}) & {
|
|
43
|
+
readonly registry: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Reads a package document from the registry npm would use for it in `cwd` (registryFor), with
|
|
47
|
+
* a plain auth-free GET. `offline` covers network and server errors, and npm `offline=true`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function fetchPackument(name: string, cwd?: string): Promise<Packument>;
|
|
50
|
+
/**
|
|
51
|
+
* The highest release whose `dokebi.core` range accepts `coreVersion` (a version without the
|
|
52
|
+
* field counts as compatible). Prereleases are never chosen (semver's `includePrerelease:
|
|
53
|
+
* false`; one is installed only when named, `add <package>@<version>`), nor a `0.0.0-…`
|
|
54
|
+
* placeholder. A deprecated version is chosen only when no compatible one is undeprecated.
|
|
55
|
+
*/
|
|
56
|
+
export declare function highestCompatible(packument: {
|
|
57
|
+
readonly versions: Readonly<Record<string, VersionManifest>>;
|
|
58
|
+
}, coreVersion: string): string | null;
|
|
59
|
+
export interface Resolution {
|
|
60
|
+
readonly package: string;
|
|
61
|
+
readonly version: string;
|
|
62
|
+
readonly source: 'registry' | 'snapshot';
|
|
63
|
+
readonly core: string;
|
|
64
|
+
/** The registry the version was read from (npm's, for this package and project). */
|
|
65
|
+
readonly registry?: string;
|
|
66
|
+
/** The registry's deprecation message: no undeprecated compatible version exists. */
|
|
67
|
+
readonly deprecated?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Picks the version `dokebi add <package>` installs when no version is given: the highest one
|
|
71
|
+
* compatible with the project core, from the registry npm uses for that package in `cwd`, else
|
|
72
|
+
* from the CLI snapshot. Throws NOT_PUBLISHED (the registry has no such package) or
|
|
73
|
+
* NO_COMPATIBLE_VERSION. Returns undefined with an offline marker when neither source knows the
|
|
74
|
+
* package.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveVersion(name: string, coreVersion: string, catalog: Catalog, cwd?: string): Promise<{
|
|
77
|
+
resolution?: Resolution;
|
|
78
|
+
offline?: string;
|
|
79
|
+
}>;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import prerelease from 'semver/functions/prerelease.js';
|
|
2
|
+
import rcompare from 'semver/functions/rcompare.js';
|
|
3
|
+
import satisfies from 'semver/functions/satisfies.js';
|
|
4
|
+
import valid from 'semver/functions/valid.js';
|
|
5
|
+
import { findPackage } from './catalog.js';
|
|
6
|
+
import { debug, describeError } from './diagnostics.js';
|
|
7
|
+
import { CliError } from './errors.js';
|
|
8
|
+
import { runNpm } from './npm-client.js';
|
|
9
|
+
const FETCH_TIMEOUT_MS = 8000;
|
|
10
|
+
const NPM_CONFIG_TIMEOUT_MS = 30_000;
|
|
11
|
+
/** npm's built-in default registry. */
|
|
12
|
+
export const PUBLIC_REGISTRY = 'https://registry.npmjs.org/';
|
|
13
|
+
// One `npm config get` per folder, scope and npm environment for the life of the process.
|
|
14
|
+
const settingsCache = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* The registry npm would use for `name` in `cwd`: `@scope:registry` for a scoped package, else
|
|
17
|
+
* `registry`, from npm's own config resolution (CLI-less: env `npm_config_*`, the project
|
|
18
|
+
* `.npmrc` (the workspace root's for a member), the user `.npmrc`, the global `npmrc`, npm's
|
|
19
|
+
* default). The CLI asks npm (`npm config get`, about 0.1 s, no network) instead of reading the
|
|
20
|
+
* files itself, so precedence, `${VAR}` expansion and `npm_config_userconfig`/`globalconfig`
|
|
21
|
+
* are exactly npm's. When npm cannot answer, the public registry (npm's default) is used and
|
|
22
|
+
* the cause goes to the debug log.
|
|
23
|
+
*/
|
|
24
|
+
export function registryFor(name, cwd = process.cwd()) {
|
|
25
|
+
const scope = /^(@[^/]+)\//.exec(name)?.[1];
|
|
26
|
+
const environment = Object.entries(process.env)
|
|
27
|
+
.filter(([key]) => key.toLowerCase().startsWith('npm_config_'))
|
|
28
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
29
|
+
const key = JSON.stringify([cwd, scope ?? '', process.env.npm_execpath ?? '', environment]);
|
|
30
|
+
let found = settingsCache.get(key);
|
|
31
|
+
if (!found) {
|
|
32
|
+
found = askNpm(cwd, scope);
|
|
33
|
+
settingsCache.set(key, found);
|
|
34
|
+
}
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
async function askNpm(cwd, scope) {
|
|
38
|
+
const keys = ['registry', 'offline', ...(scope ? [`${scope}:registry`] : [])];
|
|
39
|
+
let values;
|
|
40
|
+
try {
|
|
41
|
+
const result = await runNpm(cwd, ['config', 'get', ...keys], {
|
|
42
|
+
timeoutMs: NPM_CONFIG_TIMEOUT_MS,
|
|
43
|
+
});
|
|
44
|
+
if (result.exitCode !== 0 || result.timedOut) {
|
|
45
|
+
debug('npm config get failed; using the public registry', {
|
|
46
|
+
exitCode: result.exitCode,
|
|
47
|
+
timedOut: result.timedOut,
|
|
48
|
+
stderr: result.stderr,
|
|
49
|
+
});
|
|
50
|
+
return { url: PUBLIC_REGISTRY, from: 'default', offline: false };
|
|
51
|
+
}
|
|
52
|
+
values = parseConfigGet(result.stdout);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// npm could not be started: its default registry is what an install would use, too.
|
|
56
|
+
debug('npm config get could not run; using the public registry', {
|
|
57
|
+
error: describeError(error),
|
|
58
|
+
});
|
|
59
|
+
return { url: PUBLIC_REGISTRY, from: 'default', offline: false };
|
|
60
|
+
}
|
|
61
|
+
const offline = values.get('offline') === 'true';
|
|
62
|
+
const scoped = scope ? registryBase(values.get(`${scope}:registry`)) : undefined;
|
|
63
|
+
const plain = registryBase(values.get('registry'));
|
|
64
|
+
const settings = scoped
|
|
65
|
+
? { url: scoped, from: 'scope', offline }
|
|
66
|
+
: plain
|
|
67
|
+
? { url: plain, from: 'registry', offline }
|
|
68
|
+
: { url: PUBLIC_REGISTRY, from: 'default', offline };
|
|
69
|
+
debug('Registry resolved by npm config', { cwd, scope: scope ?? null, ...settings });
|
|
70
|
+
return settings;
|
|
71
|
+
}
|
|
72
|
+
/** `npm config get a b c` prints `a=value` lines (one key alone prints just the value). */
|
|
73
|
+
export function parseConfigGet(stdout) {
|
|
74
|
+
const values = new Map();
|
|
75
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
76
|
+
const at = line.indexOf('=');
|
|
77
|
+
if (at > 0)
|
|
78
|
+
values.set(line.slice(0, at).trim(), line.slice(at + 1).trim());
|
|
79
|
+
}
|
|
80
|
+
return values;
|
|
81
|
+
}
|
|
82
|
+
/** An http(s) registry URL ending with `/` and without `user:password@`, else undefined. */
|
|
83
|
+
export function registryBase(value) {
|
|
84
|
+
if (!value || value === 'undefined' || value === 'null')
|
|
85
|
+
return undefined;
|
|
86
|
+
let url;
|
|
87
|
+
try {
|
|
88
|
+
url = new URL(value);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
debug('npm registry setting is not a URL; ignored', { error: describeError(error) });
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
95
|
+
return undefined;
|
|
96
|
+
url.username = '';
|
|
97
|
+
url.password = '';
|
|
98
|
+
url.search = '';
|
|
99
|
+
url.hash = '';
|
|
100
|
+
const text = url.href;
|
|
101
|
+
return text.endsWith('/') ? text : `${text}/`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Reads a package document from the registry npm would use for it in `cwd` (registryFor), with
|
|
105
|
+
* a plain auth-free GET. `offline` covers network and server errors, and npm `offline=true`.
|
|
106
|
+
*/
|
|
107
|
+
export async function fetchPackument(name, cwd = process.cwd()) {
|
|
108
|
+
const settings = await registryFor(name, cwd);
|
|
109
|
+
const registry = settings.url;
|
|
110
|
+
if (settings.offline) {
|
|
111
|
+
debug('npm is configured offline; registry not asked', { package: name, registry });
|
|
112
|
+
return { status: 'offline', error: 'npm offline=true', registry };
|
|
113
|
+
}
|
|
114
|
+
const url = `${registry}${name.replace('/', '%2f')}`;
|
|
115
|
+
const began = performance.now();
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
headers: { accept: 'application/json' },
|
|
119
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
120
|
+
});
|
|
121
|
+
debug('Registry answered', {
|
|
122
|
+
package: name,
|
|
123
|
+
registry,
|
|
124
|
+
status: response.status,
|
|
125
|
+
ms: Math.round(performance.now() - began),
|
|
126
|
+
});
|
|
127
|
+
if (response.status === 404)
|
|
128
|
+
return { status: 'missing', registry };
|
|
129
|
+
if (!response.ok)
|
|
130
|
+
return { status: 'offline', error: `HTTP ${response.status}`, registry };
|
|
131
|
+
const document = (await response.json());
|
|
132
|
+
return {
|
|
133
|
+
status: 'found',
|
|
134
|
+
versions: document.versions ?? {},
|
|
135
|
+
latest: document['dist-tags']?.latest ?? null,
|
|
136
|
+
registry,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
debug('Registry not reachable', { package: name, registry, error: describeError(error) });
|
|
141
|
+
return {
|
|
142
|
+
status: 'offline',
|
|
143
|
+
error: error instanceof Error ? error.message : String(error),
|
|
144
|
+
registry,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** `0.0.0-<tag>`: a name-reserving placeholder, never a real release. */
|
|
149
|
+
const PLACEHOLDER = /^0\.0\.0-/;
|
|
150
|
+
/**
|
|
151
|
+
* The highest release whose `dokebi.core` range accepts `coreVersion` (a version without the
|
|
152
|
+
* field counts as compatible). Prereleases are never chosen (semver's `includePrerelease:
|
|
153
|
+
* false`; one is installed only when named, `add <package>@<version>`), nor a `0.0.0-…`
|
|
154
|
+
* placeholder. A deprecated version is chosen only when no compatible one is undeprecated.
|
|
155
|
+
*/
|
|
156
|
+
export function highestCompatible(packument, coreVersion) {
|
|
157
|
+
const candidates = Object.entries(packument.versions)
|
|
158
|
+
.filter(([version, manifest]) => {
|
|
159
|
+
if (!valid(version) || prerelease(version) || PLACEHOLDER.test(version))
|
|
160
|
+
return false;
|
|
161
|
+
const range = manifest.dokebi?.core;
|
|
162
|
+
return range === undefined || satisfies(coreVersion, range);
|
|
163
|
+
})
|
|
164
|
+
.sort(([a], [b]) => rcompare(a, b));
|
|
165
|
+
return candidates.find(([, manifest]) => !manifest.deprecated)?.[0] ?? candidates[0]?.[0] ?? null;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Picks the version `dokebi add <package>` installs when no version is given: the highest one
|
|
169
|
+
* compatible with the project core, from the registry npm uses for that package in `cwd`, else
|
|
170
|
+
* from the CLI snapshot. Throws NOT_PUBLISHED (the registry has no such package) or
|
|
171
|
+
* NO_COMPATIBLE_VERSION. Returns undefined with an offline marker when neither source knows the
|
|
172
|
+
* package.
|
|
173
|
+
*/
|
|
174
|
+
export async function resolveVersion(name, coreVersion, catalog, cwd = process.cwd()) {
|
|
175
|
+
const found = await fetchPackument(name, cwd);
|
|
176
|
+
if (found.status === 'missing')
|
|
177
|
+
throw new CliError('NOT_PUBLISHED', `${name} is not published on the registry`, {
|
|
178
|
+
details: { package: name, registry: found.registry },
|
|
179
|
+
phase: 'install',
|
|
180
|
+
});
|
|
181
|
+
if (found.status === 'found') {
|
|
182
|
+
const version = highestCompatible(found, coreVersion);
|
|
183
|
+
if (!version)
|
|
184
|
+
throw new CliError('NO_COMPATIBLE_VERSION', `No version of ${name} supports @dokebi/core ${coreVersion}`, {
|
|
185
|
+
details: {
|
|
186
|
+
package: name,
|
|
187
|
+
core: coreVersion,
|
|
188
|
+
latest: found.latest,
|
|
189
|
+
registry: found.registry,
|
|
190
|
+
},
|
|
191
|
+
phase: 'install',
|
|
192
|
+
});
|
|
193
|
+
const deprecated = found.versions[version]?.deprecated;
|
|
194
|
+
return {
|
|
195
|
+
resolution: {
|
|
196
|
+
package: name,
|
|
197
|
+
version,
|
|
198
|
+
source: 'registry',
|
|
199
|
+
core: coreVersion,
|
|
200
|
+
registry: found.registry,
|
|
201
|
+
...(deprecated ? { deprecated } : {}),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const known = findPackage(catalog, name);
|
|
206
|
+
if (known && known.package === name && (!known.core || satisfies(coreVersion, known.core)))
|
|
207
|
+
return {
|
|
208
|
+
resolution: { package: name, version: known.version, source: 'snapshot', core: coreVersion },
|
|
209
|
+
offline: found.error,
|
|
210
|
+
};
|
|
211
|
+
return { offline: found.error };
|
|
212
|
+
}
|
package/dist/rotating-log.d.ts
CHANGED
|
@@ -9,6 +9,18 @@ export declare class LineBuffer {
|
|
|
9
9
|
/** The unfinished end (a line without its newline yet), emptied. */
|
|
10
10
|
take(): Buffer;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* The writer's output path (D-0053): whole lines of stdout and stderr go to `log`, and `onStarted`
|
|
14
|
+
* runs once, right **after** the data holding `marker` (the start line) was written. The CLI reads
|
|
15
|
+
* app.log as soon as it hears `ready` (it counts on the start line and the lines before it being
|
|
16
|
+
* there, and parses the listening URLs from them), so the notice must never overtake the write.
|
|
17
|
+
* Sending it first raced a slow write — a busy machine hands the writer the whole startup output in
|
|
18
|
+
* one chunk, and a rotation that fails while writing it reports the failure before the rest is
|
|
19
|
+
* appended; `dokebi start` then saw only the lines that fitted before the limit.
|
|
20
|
+
*/
|
|
21
|
+
export declare function forwardOutput(log: {
|
|
22
|
+
write(data: Buffer): void;
|
|
23
|
+
}, marker: string, onStarted: () => void): (data: Buffer) => void;
|
|
12
24
|
export declare class RotatingLog {
|
|
13
25
|
#private;
|
|
14
26
|
readonly file: string;
|
package/dist/rotating-log.js
CHANGED
|
@@ -41,6 +41,25 @@ export class LineBuffer {
|
|
|
41
41
|
return rest;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The writer's output path (D-0053): whole lines of stdout and stderr go to `log`, and `onStarted`
|
|
46
|
+
* runs once, right **after** the data holding `marker` (the start line) was written. The CLI reads
|
|
47
|
+
* app.log as soon as it hears `ready` (it counts on the start line and the lines before it being
|
|
48
|
+
* there, and parses the listening URLs from them), so the notice must never overtake the write.
|
|
49
|
+
* Sending it first raced a slow write — a busy machine hands the writer the whole startup output in
|
|
50
|
+
* one chunk, and a rotation that fails while writing it reports the failure before the rest is
|
|
51
|
+
* appended; `dokebi start` then saw only the lines that fitted before the limit.
|
|
52
|
+
*/
|
|
53
|
+
export function forwardOutput(log, marker, onStarted) {
|
|
54
|
+
let started = false;
|
|
55
|
+
return (data) => {
|
|
56
|
+
log.write(data);
|
|
57
|
+
if (started || !data.includes(marker))
|
|
58
|
+
return;
|
|
59
|
+
started = true;
|
|
60
|
+
onStarted();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
44
63
|
export class RotatingLog {
|
|
45
64
|
file;
|
|
46
65
|
maxBytes;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AppHosts } from './app-urls.js';
|
|
1
2
|
/** Rotation defaults and limits (D-0053): `--log-max-mb`, `--log-files`. */
|
|
2
3
|
export declare const LOG_LIMITS: {
|
|
3
4
|
readonly maxMb: {
|
|
@@ -20,7 +21,20 @@ export interface PidRecord {
|
|
|
20
21
|
readonly logWriter?: number;
|
|
21
22
|
readonly logMaxBytes?: number;
|
|
22
23
|
readonly logFiles?: number;
|
|
24
|
+
/** The app's DOKEBI_INSTANCE_TOKEN (D-0073); absent in pid files of older CLIs. */
|
|
25
|
+
readonly token?: string;
|
|
26
|
+
/** Where the app answered (the web listening line), for the proof without `ps`. */
|
|
27
|
+
readonly urls?: string[];
|
|
28
|
+
/** The log writer's loopback control port (D-0073 addendum); absent when it could not listen. */
|
|
29
|
+
readonly control?: number;
|
|
23
30
|
}
|
|
31
|
+
/** How a pid was shown to be this project's app (ServerStatus.proof). */
|
|
32
|
+
export type Proof = 'process' | 'http' | 'log-writer';
|
|
33
|
+
/**
|
|
34
|
+
* Sends a signal like `process.kill` (signal 0 tests the pid). Tests replace it to act as a
|
|
35
|
+
* sandbox that refuses signals (EPERM).
|
|
36
|
+
*/
|
|
37
|
+
export type ProcessSignaller = (pid: number, signal: NodeJS.Signals | 0) => void;
|
|
24
38
|
export interface LogFile {
|
|
25
39
|
readonly file: string;
|
|
26
40
|
readonly bytes: number;
|
|
@@ -33,6 +47,12 @@ export interface ServerStatus {
|
|
|
33
47
|
readonly rssMb?: number;
|
|
34
48
|
/** A pid file was left by a process that is gone (crashed or killed); it was removed. */
|
|
35
49
|
readonly stale?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* How the recorded pid was shown to be this project's app: `process` (ps or /proc: command and
|
|
52
|
+
* start time), `log-writer` (no process information; the app's log writer proved the instance
|
|
53
|
+
* token on its control socket) or `http` (the app answered the instance challenge).
|
|
54
|
+
*/
|
|
55
|
+
readonly proof?: Proof;
|
|
36
56
|
readonly log: string;
|
|
37
57
|
/** app.log and its rotated files, newest first. */
|
|
38
58
|
readonly logFiles: LogFile[];
|
|
@@ -44,7 +64,38 @@ export interface ServerStatus {
|
|
|
44
64
|
/** The writer's own diagnostics, when it wrote any. */
|
|
45
65
|
readonly logWriterLog?: string;
|
|
46
66
|
}
|
|
47
|
-
export
|
|
67
|
+
export interface ProcessInfo {
|
|
68
|
+
startedAt: number;
|
|
69
|
+
rssKb: number;
|
|
70
|
+
command: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reads a process: undefined when there is no such process. When the information cannot be
|
|
74
|
+
* read (no `ps`, a sandbox, /proc unreadable) it throws `PROCESS_INFO_FAILED`: the process may
|
|
75
|
+
* be alive, so callers never treat that as a stale pid file.
|
|
76
|
+
*/
|
|
77
|
+
export type ProcessReader = (pid: number) => Promise<ProcessInfo | undefined>;
|
|
78
|
+
export declare function status(root: string,
|
|
79
|
+
/** Tests replace how a process is read; the CLI uses ps (/proc on Linux). */
|
|
80
|
+
read?: ProcessReader,
|
|
81
|
+
/** Tests replace how a process is signalled; the CLI uses process.kill. */
|
|
82
|
+
kill?: ProcessSignaller): Promise<ServerStatus>;
|
|
83
|
+
/** The variable `start` gives the app (D-0073); @dokebi/web answers the challenge with it. */
|
|
84
|
+
export declare const INSTANCE_TOKEN_ENV = "DOKEBI_INSTANCE_TOKEN";
|
|
85
|
+
/** The challenge request header and the answer response header of @dokebi/web. */
|
|
86
|
+
export declare const INSTANCE_HEADER = "x-dokebi-instance";
|
|
87
|
+
/** The answer to a challenge: base64url HMAC-SHA256 keyed with the instance token. */
|
|
88
|
+
export declare function instanceAnswer(token: string, challenge: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* Whether the app at the recorded address knows the record's instance token (D-0073): a random
|
|
91
|
+
* challenge must come back as HMAC-SHA256(token, challenge). The token never goes on the wire.
|
|
92
|
+
*/
|
|
93
|
+
export declare function proveInstance(root: string, record: Pick<PidRecord, 'token' | 'urls'>, timeoutMs?: number): Promise<{
|
|
94
|
+
proven: boolean;
|
|
95
|
+
url?: string;
|
|
96
|
+
tried: string[];
|
|
97
|
+
reason?: string;
|
|
98
|
+
}>;
|
|
48
99
|
export interface LogOptions {
|
|
49
100
|
/** Rotate app.log when it would pass this size (default 50 MB). */
|
|
50
101
|
readonly logMaxBytes?: number;
|
|
@@ -60,16 +111,35 @@ export interface StartResult {
|
|
|
60
111
|
readonly logFiles: number;
|
|
61
112
|
/** False when the process is alive but never logged the start line within the wait. */
|
|
62
113
|
readonly ready: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Where the app answers (AI-M03): with @dokebi/app, `<scheme>://<domain>:<port>` for each
|
|
116
|
+
* configured loopback domain (`localhost`, `*.localhost`, `127.x.x.x`); without it, `listen`.
|
|
117
|
+
*/
|
|
118
|
+
readonly urls: string[];
|
|
119
|
+
/** Addresses from the web plugin's listening log line (empty when it logged none). */
|
|
120
|
+
readonly listen: string[];
|
|
63
121
|
readonly ms: number;
|
|
64
122
|
readonly warnings: string[];
|
|
65
123
|
}
|
|
66
124
|
export interface StartOptions extends LogOptions {
|
|
67
125
|
/** How long to wait for the start line (default 10000 ms). */
|
|
68
126
|
readonly waitMs?: number;
|
|
127
|
+
/** Extra environment for the app (verify passes its instance token, D-0065 §11). */
|
|
128
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
69
129
|
}
|
|
70
130
|
export declare function start(root: string, options?: StartOptions,
|
|
71
131
|
/** Tests replace the project inspection; the CLI always inspects. */
|
|
72
132
|
check?: (root: string) => Promise<string>): Promise<StartResult>;
|
|
133
|
+
/**
|
|
134
|
+
* HTTP URLs from `Web server listening` log lines (D-0065): `address`, `addresses` or `urls`
|
|
135
|
+
* fields as strings (`http://…`) or `{ address, port }` objects. Unknown shapes are skipped.
|
|
136
|
+
*/
|
|
137
|
+
export declare function listeningUrls(text: string): string[];
|
|
138
|
+
/**
|
|
139
|
+
* The app host settings for the shown URLs; when they cannot be read the listening addresses are
|
|
140
|
+
* shown instead (the app already runs: this is no reason to fail the command).
|
|
141
|
+
*/
|
|
142
|
+
export declare function appHostsOrNone(root: string): Promise<AppHosts>;
|
|
73
143
|
export interface StopResult {
|
|
74
144
|
readonly stopped: boolean;
|
|
75
145
|
readonly pid?: number;
|
|
@@ -78,8 +148,26 @@ export interface StopResult {
|
|
|
78
148
|
readonly stale?: boolean;
|
|
79
149
|
/** Set when the log writer was still running 5 s after the app stopped. */
|
|
80
150
|
readonly warnings?: string[];
|
|
151
|
+
/** How the pid was shown to be this project's app (see ServerStatus.proof). */
|
|
152
|
+
readonly proof?: Proof;
|
|
153
|
+
/**
|
|
154
|
+
* Who sent the stop signals: `signal` (this command) or `log-writer` (the app's log writer on
|
|
155
|
+
* request, when this shell may not signal the app: an agent sandbox per command).
|
|
156
|
+
*/
|
|
157
|
+
readonly via?: 'signal' | 'log-writer';
|
|
81
158
|
}
|
|
82
|
-
export declare function stop(root: string, timeoutMs?: number, force?: boolean
|
|
159
|
+
export declare function stop(root: string, timeoutMs?: number, force?: boolean,
|
|
160
|
+
/** Tests replace how a process is read; the CLI uses ps (/proc on Linux). */
|
|
161
|
+
read?: ProcessReader,
|
|
162
|
+
/** Tests replace how a process is signalled; the CLI uses process.kill. */
|
|
163
|
+
kill?: ProcessSignaller): Promise<StopResult>;
|
|
164
|
+
/**
|
|
165
|
+
* Stops the app this CLI run started (`start`'s result), without `ps` (see the file comment):
|
|
166
|
+
* SIGTERM, wait for the drain, SIGKILL after `timeoutMs` when `force`, then the pid file (when it
|
|
167
|
+
* still names this process) and a wait for the log writer. An app that already exited by itself
|
|
168
|
+
* is `{ stopped: false, stale: true }`.
|
|
169
|
+
*/
|
|
170
|
+
export declare function stopStarted(root: string, started: Pick<StartResult, 'pid' | 'logWriter'>, timeoutMs?: number, force?: boolean): Promise<StopResult>;
|
|
83
171
|
/**
|
|
84
172
|
* Prints the last `lines` lines (across app.log and its rotated files); with `follow`, keeps
|
|
85
173
|
* printing what is appended until SIGINT (or `signal`), following app.log across rotations.
|
|
@@ -93,7 +181,9 @@ export declare function foreground(root: string): Promise<number>;
|
|
|
93
181
|
/**
|
|
94
182
|
* Development loop: runs the app, restarts it (SIGTERM, drain, start) 300 ms after a change in
|
|
95
183
|
* src/, dokebi.yaml or .env. With a Vite config, Inertia views come from the Vite dev server
|
|
96
|
-
*
|
|
97
|
-
*
|
|
184
|
+
* (HMR, D-0045) when template-inertia supports it, otherwise from watch builds that restart the
|
|
185
|
+
* app. With template-inertia 0.4.0 that Vite is one host process beside the app, shared by every
|
|
186
|
+
* cluster worker and kept across app restarts (D-0045 addendum 3); older ones start it inside the app.
|
|
187
|
+
* The first Ctrl+C drains everything; a second one kills.
|
|
98
188
|
*/
|
|
99
189
|
export declare function dev(root: string, say: (line: string) => void): Promise<number>;
|