@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,21 @@
|
|
|
1
|
+
/** What decides which Host names the app answers. */
|
|
2
|
+
export interface AppHosts {
|
|
3
|
+
/** A plugin that hosts apps (@dokebi/app) is installed: the Host header picks the app. */
|
|
4
|
+
readonly hosted: boolean;
|
|
5
|
+
/** The configured domains of that plugin (`options.<host>.domains` keys), in order. */
|
|
6
|
+
readonly domains: readonly string[];
|
|
7
|
+
/** A key of domains answers unknown hosts (`unknownDomain`, not `reject`). */
|
|
8
|
+
readonly answersUnknown: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Whether a host name or IP resolves to this machine (loopback). */
|
|
11
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The URLs to show for the listening addresses `listen` (from the web listening line or
|
|
14
|
+
* dokebi.yaml). Without an app host: the listening addresses (a wildcard shown as 127.0.0.1).
|
|
15
|
+
* With one: `<scheme>://<domain>:<port>` for each loopback domain, plus the listening addresses
|
|
16
|
+
* when unknown hosts are answered. Empty when only public domains are configured (they answer
|
|
17
|
+
* through DNS or a proxy, not on this machine by name).
|
|
18
|
+
*/
|
|
19
|
+
export declare function appUrls(listen: readonly string[], hosts: AppHosts): string[];
|
|
20
|
+
/** The app host settings of a project; `hosted: false` when no plugin hosts apps. */
|
|
21
|
+
export declare function readAppHosts(root: string): Promise<AppHosts>;
|
package/dist/app-urls.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Addresses the app really answers (AI-M03). @dokebi/web listens on an IP (`http://127.0.0.1:3000`),
|
|
2
|
+
// but with a plugin that hosts apps (@dokebi/app, D-0070) the Host header picks the app: a
|
|
3
|
+
// request for `127.0.0.1` gets 404 unless that IP is a configured domain or `unknownDomain`
|
|
4
|
+
// names an app. `start`, `verify` and `dev` therefore report `http://<domain>:<port>` for every
|
|
5
|
+
// configured domain that resolves to this machine (`localhost`, `*.localhost`, `127.x.x.x`) and
|
|
6
|
+
// keep the listening address apart (`listen`).
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { CONFIG_FILE, parseProjectConfig } from './configuration.js';
|
|
10
|
+
import { debug, describeError, isMissing } from './diagnostics.js';
|
|
11
|
+
import { parseObject } from './filesystem.js';
|
|
12
|
+
import { findHost } from './manifest.js';
|
|
13
|
+
import { readState } from './project-state.js';
|
|
14
|
+
/** Listening addresses that accept connections from this machine on every loopback name. */
|
|
15
|
+
const WILDCARD = new Set(['0.0.0.0', '::', '[::]']);
|
|
16
|
+
/** Whether a host name or IP resolves to this machine (loopback). */
|
|
17
|
+
export function isLoopbackHost(host) {
|
|
18
|
+
const name = host.toLowerCase().replace(/^\[|\]$/g, '');
|
|
19
|
+
return (name === 'localhost' ||
|
|
20
|
+
name.endsWith('.localhost') ||
|
|
21
|
+
name === '::1' ||
|
|
22
|
+
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name));
|
|
23
|
+
}
|
|
24
|
+
/** A listening URL with a wildcard address replaced by 127.0.0.1 (where this machine reaches it). */
|
|
25
|
+
function reachable(url) {
|
|
26
|
+
const host = WILDCARD.has(url.hostname) ? '127.0.0.1' : url.hostname;
|
|
27
|
+
return `${url.protocol}//${host}${port(url)}`;
|
|
28
|
+
}
|
|
29
|
+
/** `:<port>`, or nothing for the scheme's default port (URL drops it). */
|
|
30
|
+
const port = (url) => (url.port ? `:${url.port}` : '');
|
|
31
|
+
/**
|
|
32
|
+
* The URLs to show for the listening addresses `listen` (from the web listening line or
|
|
33
|
+
* dokebi.yaml). Without an app host: the listening addresses (a wildcard shown as 127.0.0.1).
|
|
34
|
+
* With one: `<scheme>://<domain>:<port>` for each loopback domain, plus the listening addresses
|
|
35
|
+
* when unknown hosts are answered. Empty when only public domains are configured (they answer
|
|
36
|
+
* through DNS or a proxy, not on this machine by name).
|
|
37
|
+
*/
|
|
38
|
+
export function appUrls(listen, hosts) {
|
|
39
|
+
const parsed = [];
|
|
40
|
+
for (const item of listen) {
|
|
41
|
+
try {
|
|
42
|
+
parsed.push(new URL(item));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// listeningUrls only keeps http(s) URLs; anything else is not an address to show.
|
|
46
|
+
debug('Skipped a listening address that is not a URL', {
|
|
47
|
+
url: item,
|
|
48
|
+
error: describeError(error),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const direct = [...new Set(parsed.map(reachable))];
|
|
53
|
+
if (!hosts.hosted)
|
|
54
|
+
return direct;
|
|
55
|
+
// Loopback names reach a server that listens on loopback or on every address.
|
|
56
|
+
const endpoints = new Map();
|
|
57
|
+
for (const url of parsed)
|
|
58
|
+
if (WILDCARD.has(url.hostname) || isLoopbackHost(url.hostname))
|
|
59
|
+
endpoints.set(`${url.protocol}${url.port}`, url);
|
|
60
|
+
const urls = [];
|
|
61
|
+
for (const domain of hosts.domains)
|
|
62
|
+
if (isLoopbackHost(domain))
|
|
63
|
+
for (const url of endpoints.values())
|
|
64
|
+
urls.push(`${url.protocol}//${domain}${port(url)}`);
|
|
65
|
+
if (hosts.answersUnknown)
|
|
66
|
+
urls.push(...direct);
|
|
67
|
+
return [...new Set(urls)];
|
|
68
|
+
}
|
|
69
|
+
/** The app host settings of a project; `hosted: false` when no plugin hosts apps. */
|
|
70
|
+
export async function readAppHosts(root) {
|
|
71
|
+
const none = { hosted: false, domains: [], answersUnknown: false };
|
|
72
|
+
let stateText;
|
|
73
|
+
try {
|
|
74
|
+
stateText = await readFile(join(root, '.dokebi/state.json'), 'utf8');
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// Not a project (yet): there is no app host.
|
|
78
|
+
if (isMissing(error))
|
|
79
|
+
return none;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
const state = readState(parseObject(stateText, 'state'));
|
|
83
|
+
const host = findHost(state.plugins, 'apps');
|
|
84
|
+
if (!host)
|
|
85
|
+
return none;
|
|
86
|
+
const options = parseProjectConfig(await readFile(join(root, CONFIG_FILE), 'utf8')).options;
|
|
87
|
+
const settings = (options[host.id] ?? {});
|
|
88
|
+
const domains = Object.keys(settings.domains ?? {});
|
|
89
|
+
const answersUnknown = typeof settings.unknownDomain === 'string' &&
|
|
90
|
+
settings.unknownDomain !== 'reject' &&
|
|
91
|
+
domains.includes(settings.unknownDomain);
|
|
92
|
+
debug('App hosts read', { host: host.id, domains, answersUnknown });
|
|
93
|
+
return { hosted: true, domains, answersUnknown };
|
|
94
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Symbol pixels, 20×20: one row per pixel row, 'rrggbb' per pixel, '------' transparent. */
|
|
2
|
+
export declare const SYMBOL_PIXELS: readonly string[];
|
|
3
|
+
/** Wordmark gradient stops, left to right, sampled from the symbol. */
|
|
4
|
+
export declare const GRADIENT: readonly (readonly [number, number, number])[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// GENERATED by scripts/brand/render-logo.mjs from packages/cli/brand/symbol.png. Do not edit.
|
|
2
|
+
// Regenerate: node scripts/brand/render-logo.mjs (npm run check fails when this file is stale).
|
|
3
|
+
/** Symbol pixels, 20×20: one row per pixel row, 'rrggbb' per pixel, '------' transparent. */
|
|
4
|
+
export const SYMBOL_PIXELS = [
|
|
5
|
+
'------ ------ ------ ------ ------ ------ 0074fc ------ ------ ------ ------ ------ ------ ------ b912fc ------ ------ ------ ------ ------',
|
|
6
|
+
'------ ------ ------ ------ ------ 00a3fc 006dfc ------ ------ ------ ------ ------ ------ ------ 9728fb b121fb aa00fc ------ ------ ------',
|
|
7
|
+
'------ ------ ------ ------ 00a9fc 0087fc 005afc ------ ------ ------ ------ ------ ------ 3810fa 5a1bfa 6f17fb 7e00fb ------ ------ ------',
|
|
8
|
+
'------ ------ ------ 00a0fb 0088fb 0060fc 0037fc 0009fc ------ ------ ------ ------ ------ 0900fa 1600fb 2900fb 4000fb ------ ------ ------',
|
|
9
|
+
'------ ------ ------ 0088fb 005dfb 0030fc ------ ------ ------ 1845fb 535bfb 8271fb ad8bfb c99cfb 603bfb 0000fb 0a00fb ------ ------ ------',
|
|
10
|
+
'------ ------ ------ 0052fb 002afb ------ 005dfc 0048fc 003ffb 214cfb 495bfa 716cfa 9e87fa cda7fb e4acfb be6efc ------ ------ ------ ------',
|
|
11
|
+
'------ ------ ------ ------ 0073fc 0074fc 0052fc 003bfc 0046fb 184ffa 3858fa 5a64fa 8478fa b295fb cda0fb b778fb 903cfb ------ ------ ------',
|
|
12
|
+
'------ ------ ------ 008dfc 008cfc 0078fc 0033fc 003dfb 0047fb 0e4efa 2955fa 475dfa 6f6dfb 9e88fb ba95fb a873fa 8745fb 641bfb ------ ------',
|
|
13
|
+
'------ ------ 00a1fc 00a2fc 0097fc 005afc 0032fc 003bfb 0042fb 2863fb 88a1fc fafafc fafafc fafafc 1806fc 310ffc 4c18fb 561bfc 4007fc ------',
|
|
14
|
+
'------ ------ 00b5fc 00a9fc 0098fc 0043fc 002ffc 0045fc fafafc fafafc fafafc fafafc fafafc fafafc 6250fc 1000fc 2800fc 4000fc 4b00fc ------',
|
|
15
|
+
'------ 00c2fc 00bbfc 00a8fc 0094fc 003cfc 003efc fafafc fafafc fafafc fafafc fafafc fafafc fafafc fafafc 3300fb 5000fb 6f01fb 8500fc ------',
|
|
16
|
+
'------ 00cffc 00bbfc 00a7fc 0090fc 0044fc fafafc fafafc 012fc3 fafafc fafafc 012fc6 fafafc fafafc fafafc 4a02fb 6707fb 840afb 9807fb ------',
|
|
17
|
+
'------ 00d4fc 00bcfc 00a7fc 0091fc 1195fc fafafc fafafc 012fc3 fafafc fafafc 012fc6 fafafc fafafc 4a26fb 4707fa 640bfa 7e0dfa 9108fb ------',
|
|
18
|
+
'------ 00d6fc 00c0fc 00abfc 0096fc 73cdfc fafafc fafafc fafafc fafafc fafafc fafafc fafafc 3330fc 2105fb 3b07fa 5608fa 6f09fa 7d00fb ------',
|
|
19
|
+
'------ 00d8fc 00c5fc 00b3fc 009efc b1e7fc fafafc fafafc fafafc 0044fc 000bfc 000afc 0008fc 0007fc 1506fb 2d05fb 4405fa 5700fa ------ ------',
|
|
20
|
+
'------ 00d5fc 00cafc 00b7fc 00b5fc c2effc fafafc fafafc 0c50fc 000dfc 000afc 0009fc 0009fc 0008fb 0804fb 1d00fb 2e00fb 3b00fb ------ ------',
|
|
21
|
+
'------ ------ 00c3fc 00b4fc 63d2fc bdeafc fafafc bed1fc 003afc 000dfc 000afc 000afc 0009fc 0007fc 0000fb 0a00fb 1600fb ------ ------ ------',
|
|
22
|
+
'------ ------ ------ 3dc6fc 74ccfc a8d9fc c0dbfc abc5fc 4579fc 001cfc 000bfc 0009fc 0007fc 0000fc 0000fb ------ ------ ------ ------ ------',
|
|
23
|
+
'------ ------ ------ ------ 80d7fc 95ccfc a3c4fc 97affc 6e88fc 3557fc ------ ------ ------ ------ ------ ------ ------ ------ ------ ------',
|
|
24
|
+
'------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------',
|
|
25
|
+
];
|
|
26
|
+
/** Wordmark gradient stops, left to right, sampled from the symbol. */
|
|
27
|
+
export const GRADIENT = [
|
|
28
|
+
[1, 212, 251],
|
|
29
|
+
[38, 91, 251],
|
|
30
|
+
[140, 58, 251],
|
|
31
|
+
];
|
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type ColourDepth = 'truecolor' | '256' | 'none';
|
|
2
|
+
/** The stream facts the banner needs (a tty.WriteStream has all of them). */
|
|
3
|
+
export interface BannerStream {
|
|
4
|
+
isTTY?: boolean;
|
|
5
|
+
columns?: number;
|
|
6
|
+
getColorDepth?: (env?: NodeJS.ProcessEnv) => number;
|
|
7
|
+
write(text: string): unknown;
|
|
8
|
+
}
|
|
9
|
+
/** Below this many columns only the symbol and the title line; below NARROW only the title. */
|
|
10
|
+
export declare const FULL_WIDTH = 70;
|
|
11
|
+
export declare const NARROW_WIDTH = 30;
|
|
12
|
+
/** Why the banner is off for this stream, or undefined when it shows. */
|
|
13
|
+
export declare function bannerBlocked(stream: BannerStream, json: boolean, env?: NodeJS.ProcessEnv): string | undefined;
|
|
14
|
+
/** truecolor with COLORTERM=truecolor/24bit (or Node's own detection), 256 colours, or none. */
|
|
15
|
+
export declare function colourDepth(stream: BannerStream, env?: NodeJS.ProcessEnv): ColourDepth;
|
|
16
|
+
/**
|
|
17
|
+
* The banner text for a colour depth and terminal width (ends with a blank line). Without
|
|
18
|
+
* colour it is the one plain title line.
|
|
19
|
+
*/
|
|
20
|
+
export declare function renderBanner(depth: ColourDepth, columns: number, version?: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Writes the banner to `stream` (stderr: stdout carries results) when it may show there.
|
|
23
|
+
* Returns whether it was written.
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeBanner(stream: BannerStream, json: boolean, env?: NodeJS.ProcessEnv): boolean;
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The start-screen banner: the Dokebi symbol (pre-rendered half-block art, banner-art.ts) beside
|
|
3
|
+
* a gradient «DOKEBI» wordmark, then `Dokebi CLI v<version>` and one hint line. People only: it
|
|
4
|
+
* goes to stderr, and only on a TTY without --json, CI, NO_COLOR, TERM=dumb or
|
|
5
|
+
* DOKEBI_NO_BANNER, so stdout and every machine-readable output stay byte-identical.
|
|
6
|
+
*/
|
|
7
|
+
import { GRADIENT, SYMBOL_PIXELS } from './banner-art.js';
|
|
8
|
+
import { debug } from './diagnostics.js';
|
|
9
|
+
import { CLI_VERSION } from './output.js';
|
|
10
|
+
/** «DOKEBI» in the ANSI Shadow block font: `█` is the letter, the box lines are its shadow. */
|
|
11
|
+
const WORDMARK = [
|
|
12
|
+
'██████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██╗',
|
|
13
|
+
'██╔══██╗██╔═══██╗██║ ██╔╝██╔════╝██╔══██╗██║',
|
|
14
|
+
'██║ ██║██║ ██║█████╔╝ █████╗ ██████╔╝██║',
|
|
15
|
+
'██║ ██║██║ ██║██╔═██╗ ██╔══╝ ██╔══██╗██║',
|
|
16
|
+
'██████╔╝╚██████╔╝██║ ██╗███████╗██████╔╝██║',
|
|
17
|
+
'╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═╝',
|
|
18
|
+
];
|
|
19
|
+
const HINT = 'New here? Run: dokebi guide · Help: dokebi --help';
|
|
20
|
+
const GAP = 3;
|
|
21
|
+
/** Below this many columns only the symbol and the title line; below NARROW only the title. */
|
|
22
|
+
export const FULL_WIDTH = 70;
|
|
23
|
+
export const NARROW_WIDTH = 30;
|
|
24
|
+
const RESET = '\x1b[0m';
|
|
25
|
+
const BOLD_WHITE = '\x1b[1;97m';
|
|
26
|
+
const DIM = '\x1b[2m';
|
|
27
|
+
/** A set environment flag: present, non-empty and not `0`/`false`. */
|
|
28
|
+
function flag(value) {
|
|
29
|
+
return value !== undefined && value !== '' && value !== '0' && value.toLowerCase() !== 'false';
|
|
30
|
+
}
|
|
31
|
+
/** Why the banner is off for this stream, or undefined when it shows. */
|
|
32
|
+
export function bannerBlocked(stream, json, env = process.env) {
|
|
33
|
+
if (json)
|
|
34
|
+
return '--json';
|
|
35
|
+
if (!stream.isTTY)
|
|
36
|
+
return 'not a TTY';
|
|
37
|
+
if (flag(env.DOKEBI_NO_BANNER))
|
|
38
|
+
return 'DOKEBI_NO_BANNER';
|
|
39
|
+
if (flag(env.CI))
|
|
40
|
+
return 'CI';
|
|
41
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
|
|
42
|
+
return 'NO_COLOR';
|
|
43
|
+
if (env.TERM === 'dumb')
|
|
44
|
+
return 'TERM=dumb';
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** truecolor with COLORTERM=truecolor/24bit (or Node's own detection), 256 colours, or none. */
|
|
48
|
+
export function colourDepth(stream, env = process.env) {
|
|
49
|
+
const colorterm = env.COLORTERM?.toLowerCase();
|
|
50
|
+
if (colorterm === 'truecolor' || colorterm === '24bit')
|
|
51
|
+
return 'truecolor';
|
|
52
|
+
const depth = stream.getColorDepth?.(env) ?? 1;
|
|
53
|
+
return depth >= 24 ? 'truecolor' : depth >= 8 ? '256' : 'none';
|
|
54
|
+
}
|
|
55
|
+
/** Nearest xterm-256 index: the 6×6×6 cube or the 24-step grey ramp. */
|
|
56
|
+
function to256([r, g, b]) {
|
|
57
|
+
const levels = [0, 95, 135, 175, 215, 255];
|
|
58
|
+
const nearest = (value) => levels.reduce((best, level, index) => Math.abs(level - value) < Math.abs(levels[best] - value) ? index : best, 0);
|
|
59
|
+
const [ri, gi, bi] = [nearest(r), nearest(g), nearest(b)];
|
|
60
|
+
const cube = [levels[ri], levels[gi], levels[bi]];
|
|
61
|
+
const grey = Math.max(0, Math.min(23, Math.round(((r + g + b) / 3 - 8) / 10)));
|
|
62
|
+
const greyValue = 8 + grey * 10;
|
|
63
|
+
const error = (c) => (c[0] - r) ** 2 + (c[1] - g) ** 2 + (c[2] - b) ** 2;
|
|
64
|
+
return error([greyValue, greyValue, greyValue]) < error(cube)
|
|
65
|
+
? 232 + grey
|
|
66
|
+
: 16 + 36 * ri + 6 * gi + bi;
|
|
67
|
+
}
|
|
68
|
+
function colour(rgb, depth, background = false) {
|
|
69
|
+
if (depth === 'truecolor')
|
|
70
|
+
return `\x1b[${background ? 48 : 38};2;${rgb.join(';')}m`;
|
|
71
|
+
return `\x1b[${background ? 48 : 38};5;${to256(rgb)}m`;
|
|
72
|
+
}
|
|
73
|
+
function parsePixel(value) {
|
|
74
|
+
if (value === '------')
|
|
75
|
+
return undefined;
|
|
76
|
+
const n = Number.parseInt(value, 16);
|
|
77
|
+
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The symbol as text rows of equal width: each cell is two pixels (▀ top in the foreground,
|
|
81
|
+
* bottom in the background; ▄ or █ when one colour). Codes are written only when they change,
|
|
82
|
+
* and the transparent tail of a row comes after the reset (so it can be trimmed).
|
|
83
|
+
*/
|
|
84
|
+
function symbolRows(depth) {
|
|
85
|
+
const pixels = SYMBOL_PIXELS.map((row) => row.split(' ').map(parsePixel));
|
|
86
|
+
const rows = [];
|
|
87
|
+
for (let y = 0; y < pixels.length; y += 2) {
|
|
88
|
+
const top = pixels[y] ?? [];
|
|
89
|
+
const bottom = pixels[y + 1] ?? [];
|
|
90
|
+
let line = '';
|
|
91
|
+
let pending = 0;
|
|
92
|
+
let state = RESET;
|
|
93
|
+
let background = false;
|
|
94
|
+
for (let x = 0; x < top.length; x++) {
|
|
95
|
+
const upper = top[x];
|
|
96
|
+
const lower = bottom[x];
|
|
97
|
+
if (!upper && !lower) {
|
|
98
|
+
pending++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
let code;
|
|
102
|
+
let char;
|
|
103
|
+
if (upper && lower && upper.join() !== lower.join()) {
|
|
104
|
+
code = `${colour(upper, depth)}${colour(lower, depth, true)}`;
|
|
105
|
+
char = '▀';
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
code = `${RESET}${colour((upper ?? lower), depth)}`;
|
|
109
|
+
char = !lower ? '▀' : !upper ? '▄' : '█';
|
|
110
|
+
}
|
|
111
|
+
if (pending) {
|
|
112
|
+
// Gaps must not carry the previous cell's background colour.
|
|
113
|
+
if (background) {
|
|
114
|
+
line += RESET;
|
|
115
|
+
state = RESET;
|
|
116
|
+
background = false;
|
|
117
|
+
}
|
|
118
|
+
line += ' '.repeat(pending);
|
|
119
|
+
pending = 0;
|
|
120
|
+
}
|
|
121
|
+
if (code !== state)
|
|
122
|
+
line += code;
|
|
123
|
+
state = code;
|
|
124
|
+
background = char === '▀' && Boolean(upper && lower && upper.join() !== lower.join());
|
|
125
|
+
line += char;
|
|
126
|
+
}
|
|
127
|
+
rows.push(`${line}${RESET}${' '.repeat(pending)}`);
|
|
128
|
+
}
|
|
129
|
+
return rows;
|
|
130
|
+
}
|
|
131
|
+
/** Linear interpolation across the gradient stops, t in [0, 1]. */
|
|
132
|
+
function gradientAt(t) {
|
|
133
|
+
const span = GRADIENT.length - 1;
|
|
134
|
+
const index = Math.min(span - 1, Math.floor(t * span));
|
|
135
|
+
const local = t * span - index;
|
|
136
|
+
const from = GRADIENT[index];
|
|
137
|
+
const to = GRADIENT[index + 1];
|
|
138
|
+
return [0, 1, 2].map((i) => Math.round(from[i] + (to[i] - from[i]) * local));
|
|
139
|
+
}
|
|
140
|
+
/** The wordmark with a horizontal gradient; shadow strokes are a darker shade of the same. */
|
|
141
|
+
function wordmarkRows(depth) {
|
|
142
|
+
const width = Math.max(...WORDMARK.map((row) => [...row].length));
|
|
143
|
+
return WORDMARK.map((row) => {
|
|
144
|
+
let line = '';
|
|
145
|
+
let last = '';
|
|
146
|
+
[...row].forEach((char, x) => {
|
|
147
|
+
if (char === ' ') {
|
|
148
|
+
line += ' ';
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const base = gradientAt(x / (width - 1));
|
|
152
|
+
const rgb = char === '█' ? base : base.map((v) => Math.round(v * 0.5));
|
|
153
|
+
const code = colour(rgb, depth);
|
|
154
|
+
if (code !== last)
|
|
155
|
+
line += code;
|
|
156
|
+
last = code;
|
|
157
|
+
line += char;
|
|
158
|
+
});
|
|
159
|
+
return line.trimEnd() + RESET;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function titleLine(depth, version) {
|
|
163
|
+
return depth === 'none'
|
|
164
|
+
? `Dokebi CLI v${version}`
|
|
165
|
+
: `${BOLD_WHITE}Dokebi CLI${RESET} ${colour([140, 140, 150], depth)}v${version}${RESET}`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The banner text for a colour depth and terminal width (ends with a blank line). Without
|
|
169
|
+
* colour it is the one plain title line.
|
|
170
|
+
*/
|
|
171
|
+
export function renderBanner(depth, columns, version = CLI_VERSION) {
|
|
172
|
+
const title = titleLine(depth, version);
|
|
173
|
+
if (depth === 'none' || columns < NARROW_WIDTH)
|
|
174
|
+
return `${title}\n`;
|
|
175
|
+
const symbol = symbolRows(depth);
|
|
176
|
+
if (columns < FULL_WIDTH)
|
|
177
|
+
return `\n${symbol.map((line) => line.trimEnd()).join('\n')}\n\n${title}\n\n`;
|
|
178
|
+
const word = wordmarkRows(depth);
|
|
179
|
+
const offset = Math.floor((symbol.length - word.length) / 2);
|
|
180
|
+
const rows = symbol.map((line, index) => {
|
|
181
|
+
const mark = word[index - offset];
|
|
182
|
+
return mark ? `${line}${' '.repeat(GAP)}${mark}` : line;
|
|
183
|
+
});
|
|
184
|
+
return `\n${rows.join('\n')}\n\n${title}\n${DIM}${HINT}${RESET}\n\n`;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Writes the banner to `stream` (stderr: stdout carries results) when it may show there.
|
|
188
|
+
* Returns whether it was written.
|
|
189
|
+
*/
|
|
190
|
+
export function writeBanner(stream, json, env = process.env) {
|
|
191
|
+
const blocked = bannerBlocked(stream, json, env);
|
|
192
|
+
if (blocked) {
|
|
193
|
+
debug('Banner skipped', { reason: blocked });
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const depth = colourDepth(stream, env);
|
|
197
|
+
// A pty with no window size reports 0 columns: treat it as unknown.
|
|
198
|
+
const columns = stream.columns || 80;
|
|
199
|
+
debug('Banner shown', { depth, columns });
|
|
200
|
+
stream.write(renderBanner(depth, columns));
|
|
201
|
+
return true;
|
|
202
|
+
}
|
package/dist/bin.d.ts
CHANGED