@3minapi/cli 0.0.0-stage → 0.22.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/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/bin.js +54 -0
- package/dist/cli.js +241 -0
- package/dist/commands/auth.js +573 -0
- package/dist/commands/common.js +48 -0
- package/dist/commands/dashboard.js +298 -0
- package/dist/commands/delete.js +167 -0
- package/dist/commands/doctor.js +326 -0
- package/dist/commands/guide.js +84 -0
- package/dist/commands/index.js +28 -0
- package/dist/commands/init.js +248 -0
- package/dist/commands/logs.js +154 -0
- package/dist/commands/new.js +109 -0
- package/dist/commands/paths.js +64 -0
- package/dist/commands/reload.js +32 -0
- package/dist/commands/results.js +445 -0
- package/dist/commands/start.js +443 -0
- package/dist/commands/stop.js +68 -0
- package/dist/commands/tasks.js +93 -0
- package/dist/commands/worker.js +178 -0
- package/dist/core/api.js +132 -0
- package/dist/core/config.js +188 -0
- package/dist/core/control-db.js +462 -0
- package/dist/core/credentials.js +75 -0
- package/dist/core/domain/constants.js +121 -0
- package/dist/core/domain/contract.js +102 -0
- package/dist/core/domain/errors.js +57 -0
- package/dist/core/domain/index.js +3 -0
- package/dist/core/domain/types.js +1 -0
- package/dist/core/error-report.js +103 -0
- package/dist/core/host/detach.js +49 -0
- package/dist/core/host/live-connection.js +270 -0
- package/dist/core/host/live-receiver.js +264 -0
- package/dist/core/host/module.js +1 -0
- package/dist/core/host/poller.js +113 -0
- package/dist/core/host/pull-step.js +54 -0
- package/dist/core/host/receiver.js +1 -0
- package/dist/core/host/reload.js +34 -0
- package/dist/core/host/runner-lock.js +245 -0
- package/dist/core/host/session.js +84 -0
- package/dist/core/host/usage.js +20 -0
- package/dist/core/log-read.js +59 -0
- package/dist/core/logger.js +206 -0
- package/dist/core/names.js +27 -0
- package/dist/core/network.js +10 -0
- package/dist/core/open-browser.js +26 -0
- package/dist/core/output.js +68 -0
- package/dist/core/paths.js +26 -0
- package/dist/core/project.js +85 -0
- package/dist/core/prompt.js +140 -0
- package/dist/core/redact.js +45 -0
- package/dist/core/secure-fs.js +50 -0
- package/dist/core/text-file.js +12 -0
- package/dist/core/text.js +27 -0
- package/dist/core/time.js +84 -0
- package/dist/core/update-notice.js +142 -0
- package/dist/core/version.js +16 -0
- package/dist/core/warnings.js +10 -0
- package/dist/help.js +144 -0
- package/dist/modules/dashboard/assets.js +411 -0
- package/dist/modules/dashboard/data.js +565 -0
- package/dist/modules/dashboard/guard.js +75 -0
- package/dist/modules/dashboard/lock.js +93 -0
- package/dist/modules/dashboard/server.js +295 -0
- package/dist/modules/dashboard/views/html.js +114 -0
- package/dist/modules/dashboard/views/layout.js +59 -0
- package/dist/modules/dashboard/views/logs.js +122 -0
- package/dist/modules/dashboard/views/overview.js +316 -0
- package/dist/modules/dashboard/views/runs.js +486 -0
- package/dist/modules/dashboard/views/workers.js +157 -0
- package/dist/modules/workers/builtin/host-identity.js +59 -0
- package/dist/modules/workers/builtin/results-worker.js +37 -0
- package/dist/modules/workers/local-server.js +244 -0
- package/dist/modules/workers/module.js +98 -0
- package/dist/modules/workers/reload-result.js +16 -0
- package/dist/modules/workers/repeat-scheduler.js +165 -0
- package/dist/modules/workers/results/missing-paths.js +66 -0
- package/dist/modules/workers/results/query.js +175 -0
- package/dist/modules/workers/results/reader-input.js +134 -0
- package/dist/modules/workers/results/reader.js +430 -0
- package/dist/modules/workers/results/results-child.js +64 -0
- package/dist/modules/workers/results/shape.js +212 -0
- package/dist/modules/workers/results/store.js +336 -0
- package/dist/modules/workers/results/substitute.js +30 -0
- package/dist/modules/workers/runtime/env.js +43 -0
- package/dist/modules/workers/runtime/error-chain.js +109 -0
- package/dist/modules/workers/runtime/executor.js +289 -0
- package/dist/modules/workers/runtime/inspect.js +53 -0
- package/dist/modules/workers/runtime/protocol.js +1 -0
- package/dist/modules/workers/runtime/resolve.js +30 -0
- package/dist/modules/workers/runtime/schema.js +85 -0
- package/dist/modules/workers/runtime/thread.js +117 -0
- package/dist/modules/workers/start.js +265 -0
- package/dist/modules/workers/sync.js +241 -0
- package/dist/modules/workers/task-runner.js +892 -0
- package/dist/modules/workers/template.js +100 -0
- package/dist/modules/workers/worker-set.js +122 -0
- package/dist/modules/workers/workers.js +66 -0
- package/package.json +48 -5
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function shouldUseColor({ flag, env, isTTY }) {
|
|
2
|
+
if (flag !== undefined)
|
|
3
|
+
return flag;
|
|
4
|
+
if (env.NO_COLOR)
|
|
5
|
+
return false;
|
|
6
|
+
if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '')
|
|
7
|
+
return env.FORCE_COLOR !== '0';
|
|
8
|
+
if (env.TERM === 'dumb')
|
|
9
|
+
return false;
|
|
10
|
+
return isTTY;
|
|
11
|
+
}
|
|
12
|
+
function style(enabled) {
|
|
13
|
+
const sgr = (open, close) => enabled ? (t) => `\x1b[${open}m${t}\x1b[${close}m` : (t) => t;
|
|
14
|
+
return {
|
|
15
|
+
bold: sgr(1, 22),
|
|
16
|
+
dim: sgr(2, 22),
|
|
17
|
+
red: sgr(31, 39),
|
|
18
|
+
yellow: sgr(33, 39),
|
|
19
|
+
green: sgr(32, 39)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export class Output {
|
|
23
|
+
json;
|
|
24
|
+
out;
|
|
25
|
+
err;
|
|
26
|
+
stdout;
|
|
27
|
+
stderr;
|
|
28
|
+
wrote = false;
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
this.json = opts.json;
|
|
31
|
+
this.stdout = opts.stdout;
|
|
32
|
+
this.stderr = opts.stderr;
|
|
33
|
+
const color = (s) => shouldUseColor({ flag: opts.color, env: opts.env, isTTY: s.isTTY === true });
|
|
34
|
+
this.out = style(!opts.json && color(opts.stdout));
|
|
35
|
+
this.err = style(color(opts.stderr));
|
|
36
|
+
}
|
|
37
|
+
result(data, text) {
|
|
38
|
+
if (this.json) {
|
|
39
|
+
this.writeOut(JSON.stringify(data, null, 2) + '\n');
|
|
40
|
+
}
|
|
41
|
+
else if (text !== undefined) {
|
|
42
|
+
this.writeOut(text.endsWith('\n') ? text : text + '\n');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
print(text) {
|
|
46
|
+
this.writeOut(text.endsWith('\n') ? text : text + '\n');
|
|
47
|
+
}
|
|
48
|
+
errorResult(doc) {
|
|
49
|
+
if (this.json && !this.wrote)
|
|
50
|
+
this.writeOut(JSON.stringify(doc, null, 2) + '\n');
|
|
51
|
+
}
|
|
52
|
+
writeOut(text) {
|
|
53
|
+
this.wrote = true;
|
|
54
|
+
this.stdout.write(text);
|
|
55
|
+
}
|
|
56
|
+
prompt(text) {
|
|
57
|
+
this.stderr.write(text);
|
|
58
|
+
}
|
|
59
|
+
info(message) {
|
|
60
|
+
this.stderr.write(message + '\n');
|
|
61
|
+
}
|
|
62
|
+
warn(message) {
|
|
63
|
+
this.stderr.write(`${this.err.yellow('warning:')} ${message}\n`);
|
|
64
|
+
}
|
|
65
|
+
error(message) {
|
|
66
|
+
this.stderr.write(`${this.err.red('error:')} ${message}\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { APP_DIR_NAME, ENV } from "./domain/index.js";
|
|
3
|
+
export function resolveDirs({ platform, env, home }) {
|
|
4
|
+
const p = platform === 'win32' ? path.win32 : path.posix;
|
|
5
|
+
const override = absolute(p, env[ENV.home]);
|
|
6
|
+
if (override) {
|
|
7
|
+
return { config: override, data: override, logs: p.join(override, 'logs') };
|
|
8
|
+
}
|
|
9
|
+
if (platform === 'win32') {
|
|
10
|
+
const roaming = absolute(p, env.APPDATA) ?? p.join(home, 'AppData', 'Roaming');
|
|
11
|
+
const local = absolute(p, env.LOCALAPPDATA) ?? p.join(home, 'AppData', 'Local');
|
|
12
|
+
const data = p.join(local, APP_DIR_NAME);
|
|
13
|
+
return { config: p.join(roaming, APP_DIR_NAME), data, logs: p.join(data, 'logs') };
|
|
14
|
+
}
|
|
15
|
+
if (platform === 'darwin') {
|
|
16
|
+
const support = p.join(home, 'Library', 'Application Support', APP_DIR_NAME);
|
|
17
|
+
return { config: support, data: support, logs: p.join(home, 'Library', 'Logs', APP_DIR_NAME) };
|
|
18
|
+
}
|
|
19
|
+
const configHome = absolute(p, env.XDG_CONFIG_HOME) ?? p.join(home, '.config');
|
|
20
|
+
const stateHome = absolute(p, env.XDG_STATE_HOME) ?? p.join(home, '.local', 'state');
|
|
21
|
+
const data = p.join(stateHome, APP_DIR_NAME);
|
|
22
|
+
return { config: p.join(configHome, APP_DIR_NAME), data, logs: p.join(data, 'logs') };
|
|
23
|
+
}
|
|
24
|
+
function absolute(p, value) {
|
|
25
|
+
return value && p.isAbsolute(value) ? value : undefined;
|
|
26
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { findProjectFile } from "./config.js";
|
|
4
|
+
import { BIN, DEFAULT_PROJECT_DIR, PROJECT_CONFIG_FILE, PROJECT_RECORD_FILE } from "./domain/index.js";
|
|
5
|
+
import { writeSecretFile } from "./secure-fs.js";
|
|
6
|
+
import { shellArg } from "./text.js";
|
|
7
|
+
export function projectRecordPath(dataDir) {
|
|
8
|
+
return path.join(dataDir, PROJECT_RECORD_FILE);
|
|
9
|
+
}
|
|
10
|
+
export function readProjectRecord(dataDir) {
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(readFileSync(projectRecordPath(dataDir), 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const root = parsed?.root;
|
|
19
|
+
if (typeof root !== 'string' || !path.isAbsolute(root))
|
|
20
|
+
return null;
|
|
21
|
+
return { root, recordedAt: String(parsed.recordedAt ?? '') };
|
|
22
|
+
}
|
|
23
|
+
export function writeProjectRecord(dataDir, root, now) {
|
|
24
|
+
const body = { root, recordedAt: new Date(now).toISOString() };
|
|
25
|
+
writeSecretFile(projectRecordPath(dataDir), JSON.stringify(body, null, '\t') + '\n');
|
|
26
|
+
}
|
|
27
|
+
export function defaultProjectRoot(home) {
|
|
28
|
+
return path.join(home, DEFAULT_PROJECT_DIR);
|
|
29
|
+
}
|
|
30
|
+
export function initHint(home) {
|
|
31
|
+
const where = home === undefined ? `~/${DEFAULT_PROJECT_DIR}` : defaultProjectRoot(home);
|
|
32
|
+
return `Run '${BIN} init' (creates ${where}) or '${BIN} init <folder>'.`;
|
|
33
|
+
}
|
|
34
|
+
export function goneHint(root, home) {
|
|
35
|
+
return (`this host's project ${root} no longer exists. If you moved it, run any ${BIN} command inside ` +
|
|
36
|
+
`the new folder (e.g. '${BIN} status') and it is picked up. Otherwise: ${initHint(home)}`);
|
|
37
|
+
}
|
|
38
|
+
export function projectAlive(root) {
|
|
39
|
+
return existsSync(path.join(root, PROJECT_CONFIG_FILE));
|
|
40
|
+
}
|
|
41
|
+
export function samePath(a, b, platform) {
|
|
42
|
+
const norm = (p) => {
|
|
43
|
+
let real = path.resolve(p);
|
|
44
|
+
try {
|
|
45
|
+
real = realpathSync.native(real);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
}
|
|
49
|
+
return platform === 'darwin' || platform === 'win32' ? real.toLowerCase() : real;
|
|
50
|
+
};
|
|
51
|
+
return norm(a) === norm(b);
|
|
52
|
+
}
|
|
53
|
+
export function resolveProject(input) {
|
|
54
|
+
const recorded = readProjectRecord(input.dataDir);
|
|
55
|
+
const unreadable = recorded === null && existsSync(projectRecordPath(input.dataDir))
|
|
56
|
+
? `could not read ${projectRecordPath(input.dataDir)} (this host's project record). `
|
|
57
|
+
: '';
|
|
58
|
+
const found = findProjectFile(input.cwd);
|
|
59
|
+
if (recorded && projectAlive(recorded.root)) {
|
|
60
|
+
const notices = [];
|
|
61
|
+
if (found && !samePath(path.dirname(found), recorded.root, input.platform)) {
|
|
62
|
+
notices.push(`ignoring ${found} — this host's project is ${recorded.root}. ` +
|
|
63
|
+
`Run '${BIN} init ${shellArg(path.dirname(found))} --move' to make it the project instead.`);
|
|
64
|
+
}
|
|
65
|
+
return { projectFile: path.join(recorded.root, PROJECT_CONFIG_FILE), record: null, notices };
|
|
66
|
+
}
|
|
67
|
+
if (!found) {
|
|
68
|
+
const notices = recorded
|
|
69
|
+
? [goneHint(recorded.root, input.home)]
|
|
70
|
+
: unreadable
|
|
71
|
+
? [`${unreadable}${initHint(input.home)}`]
|
|
72
|
+
: [];
|
|
73
|
+
return { projectFile: null, record: null, notices };
|
|
74
|
+
}
|
|
75
|
+
const root = path.dirname(found);
|
|
76
|
+
return {
|
|
77
|
+
projectFile: found,
|
|
78
|
+
record: root,
|
|
79
|
+
notices: [
|
|
80
|
+
recorded
|
|
81
|
+
? `this host's project ${recorded.root} no longer exists — now using ${root}.`
|
|
82
|
+
: `${unreadable}this host's project is now ${root} — commands use it from any folder.`
|
|
83
|
+
]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { CliError, ExitCode } from "./domain/index.js";
|
|
2
|
+
const MASK = '*';
|
|
3
|
+
export async function readSecret(stdin, out, label) {
|
|
4
|
+
if (!stdin.isTTY || !stdin.setRawMode || !stdin.on || !stdin.off) {
|
|
5
|
+
throw new CliError('no terminal to prompt on. Pipe the token in with --with-token.');
|
|
6
|
+
}
|
|
7
|
+
const on = stdin.on.bind(stdin);
|
|
8
|
+
const off = stdin.off.bind(stdin);
|
|
9
|
+
out.prompt(label);
|
|
10
|
+
const listeners = [];
|
|
11
|
+
try {
|
|
12
|
+
stdin.setRawMode(true);
|
|
13
|
+
stdin.setEncoding?.('utf8');
|
|
14
|
+
stdin.resume?.();
|
|
15
|
+
return await new Promise((resolve, reject) => {
|
|
16
|
+
let value = '';
|
|
17
|
+
const skip = new EscapeSkipper();
|
|
18
|
+
const onData = (chunk) => {
|
|
19
|
+
const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
20
|
+
for (const ch of text) {
|
|
21
|
+
if (skip.consume(ch))
|
|
22
|
+
continue;
|
|
23
|
+
if (ch === '\r' || ch === '\n' || ch === '\u0004')
|
|
24
|
+
return resolve(value);
|
|
25
|
+
if (ch === '\u0003')
|
|
26
|
+
return reject(new CliError('cancelled', ExitCode.Interrupted));
|
|
27
|
+
if (ch === '\u007f' || ch === '\b') {
|
|
28
|
+
if (value !== '') {
|
|
29
|
+
value = value.slice(0, -1);
|
|
30
|
+
out.prompt('\b \b');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (ch === '\u0015') {
|
|
34
|
+
out.prompt('\b \b'.repeat(value.length));
|
|
35
|
+
value = '';
|
|
36
|
+
}
|
|
37
|
+
else if (ch >= ' ') {
|
|
38
|
+
value += ch;
|
|
39
|
+
out.prompt(MASK);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const onEnd = () => reject(new CliError('input closed before the token was entered'));
|
|
44
|
+
const onError = (err) => reject(new CliError(`cannot read input: ${err.message}`));
|
|
45
|
+
listeners.push(['data', onData], ['end', onEnd], ['error', onError]);
|
|
46
|
+
for (const [event, listener] of listeners)
|
|
47
|
+
on(event, listener);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
for (const [event, listener] of listeners)
|
|
52
|
+
off(event, listener);
|
|
53
|
+
stdin.setRawMode(false);
|
|
54
|
+
stdin.pause?.();
|
|
55
|
+
out.prompt('\n');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
class EscapeSkipper {
|
|
59
|
+
state = 'none';
|
|
60
|
+
consume(ch) {
|
|
61
|
+
switch (this.state) {
|
|
62
|
+
case 'none':
|
|
63
|
+
if (ch !== '\u001b')
|
|
64
|
+
return false;
|
|
65
|
+
this.state = 'esc';
|
|
66
|
+
return true;
|
|
67
|
+
case 'esc':
|
|
68
|
+
this.state = ch === '[' ? 'csi' : ch === 'O' ? 'ss3' : 'none';
|
|
69
|
+
return true;
|
|
70
|
+
case 'csi': {
|
|
71
|
+
const code = ch.charCodeAt(0);
|
|
72
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
73
|
+
this.state = 'none';
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
case 'ss3':
|
|
77
|
+
this.state = 'none';
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function confirm(stdin, out, question) {
|
|
83
|
+
if (!stdin.isTTY || !stdin.setRawMode || !stdin.on || !stdin.off) {
|
|
84
|
+
throw new CliError('no terminal to ask for confirmation. Pass --yes to proceed.');
|
|
85
|
+
}
|
|
86
|
+
const on = stdin.on.bind(stdin);
|
|
87
|
+
const off = stdin.off.bind(stdin);
|
|
88
|
+
out.prompt(`${question} [y/N] `);
|
|
89
|
+
const listeners = [];
|
|
90
|
+
try {
|
|
91
|
+
stdin.setRawMode(true);
|
|
92
|
+
stdin.setEncoding?.('utf8');
|
|
93
|
+
stdin.resume?.();
|
|
94
|
+
const answer = await new Promise((resolve, reject) => {
|
|
95
|
+
let value = '';
|
|
96
|
+
const skip = new EscapeSkipper();
|
|
97
|
+
const onData = (chunk) => {
|
|
98
|
+
const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
99
|
+
for (const ch of text) {
|
|
100
|
+
if (skip.consume(ch))
|
|
101
|
+
continue;
|
|
102
|
+
if (ch === '\r' || ch === '\n' || ch === '\u0004')
|
|
103
|
+
return resolve(value);
|
|
104
|
+
if (ch === '\u0003')
|
|
105
|
+
return reject(new CliError('cancelled', ExitCode.Interrupted));
|
|
106
|
+
if (ch === '\u007f' || ch === '\b') {
|
|
107
|
+
if (value !== '') {
|
|
108
|
+
value = value.slice(0, -1);
|
|
109
|
+
out.prompt('\b \b');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (ch >= ' ') {
|
|
113
|
+
value += ch;
|
|
114
|
+
out.prompt(ch);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const onEnd = () => reject(new CliError('input closed before an answer was given'));
|
|
119
|
+
const onError = (err) => reject(new CliError(`cannot read input: ${err.message}`));
|
|
120
|
+
listeners.push(['data', onData], ['end', onEnd], ['error', onError]);
|
|
121
|
+
for (const [event, listener] of listeners)
|
|
122
|
+
on(event, listener);
|
|
123
|
+
});
|
|
124
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
for (const [event, listener] of listeners)
|
|
128
|
+
off(event, listener);
|
|
129
|
+
stdin.setRawMode(false);
|
|
130
|
+
stdin.pause?.();
|
|
131
|
+
out.prompt('\n');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function readAllStdin(stdin) {
|
|
135
|
+
let text = '';
|
|
136
|
+
for await (const chunk of stdin) {
|
|
137
|
+
text += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
138
|
+
}
|
|
139
|
+
return text;
|
|
140
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { truncate } from "./text.js";
|
|
2
|
+
export const REDACTED = '<redacted>';
|
|
3
|
+
const SECRET_KEY = /authorization|token|secret|password|passwd|api[-_]?key|signature|cookie|credential/i;
|
|
4
|
+
const SECRET_VALUES = [
|
|
5
|
+
/tm_(?:host|live|test|mcp|task)_[0-9a-f]{8,}/gi,
|
|
6
|
+
/whsec_[A-Za-z0-9+/=]{8,}/g,
|
|
7
|
+
/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/g
|
|
8
|
+
];
|
|
9
|
+
const SECRET_ASSIGNMENTS = [
|
|
10
|
+
/(\btoken=)[^\s&#"']{8,}/gi,
|
|
11
|
+
/(\b[A-Za-z0-9_]*(?:SECRET|_KEY|TOKEN|PASSWORD)\s*=\s*)["']?[^\s"'&]{8,}["']?/gi
|
|
12
|
+
];
|
|
13
|
+
export function redactString(value) {
|
|
14
|
+
let out = value;
|
|
15
|
+
for (const pattern of SECRET_ASSIGNMENTS)
|
|
16
|
+
out = out.replace(pattern, `$1${REDACTED}`);
|
|
17
|
+
for (const pattern of SECRET_VALUES) {
|
|
18
|
+
out = out.replace(pattern, (match, scheme) => typeof scheme === 'string' && /^(Bearer|Basic)$/.test(scheme)
|
|
19
|
+
? `${scheme} ${REDACTED}`
|
|
20
|
+
: REDACTED);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
export function redact(value, seen = new WeakSet()) {
|
|
25
|
+
if (typeof value === 'string')
|
|
26
|
+
return redactString(value);
|
|
27
|
+
if (value === null || typeof value !== 'object')
|
|
28
|
+
return value;
|
|
29
|
+
if (seen.has(value))
|
|
30
|
+
return '[circular]';
|
|
31
|
+
seen.add(value);
|
|
32
|
+
if (value instanceof Error) {
|
|
33
|
+
return { name: value.name, message: redactString(value.message) };
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return value.map((v) => redact(v, seen));
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const [key, v] of Object.entries(value)) {
|
|
39
|
+
out[key] = SECRET_KEY.test(key) && v !== null && v !== undefined ? REDACTED : redact(v, seen);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
export function redactAndTruncate(value, maxChars) {
|
|
44
|
+
return truncate(redactString(value), maxChars);
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { PRIVATE_DIR_MODE, SECRET_FILE_MODE } from "./domain/index.js";
|
|
5
|
+
const isWindows = process.platform === 'win32';
|
|
6
|
+
export function ensurePrivateDir(dir) {
|
|
7
|
+
const existed = existsSync(dir);
|
|
8
|
+
mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
9
|
+
if (!existed && !isWindows)
|
|
10
|
+
chmodSync(dir, PRIVATE_DIR_MODE);
|
|
11
|
+
}
|
|
12
|
+
export function writeSecretFile(file, data) {
|
|
13
|
+
ensurePrivateDir(path.dirname(file));
|
|
14
|
+
const tmp = `${file}.${randomBytes(6).toString('hex')}.tmp`;
|
|
15
|
+
try {
|
|
16
|
+
writeFileSync(tmp, data, { mode: SECRET_FILE_MODE, flag: 'wx' });
|
|
17
|
+
if (!isWindows)
|
|
18
|
+
chmodSync(tmp, SECRET_FILE_MODE);
|
|
19
|
+
renameWithRetry(tmp, file);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
rmSync(tmp, { force: true });
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function touchSecretFile(file) {
|
|
27
|
+
ensurePrivateDir(path.dirname(file));
|
|
28
|
+
closeSync(openSync(file, 'a', SECRET_FILE_MODE));
|
|
29
|
+
}
|
|
30
|
+
export function looseMode(file) {
|
|
31
|
+
if (isWindows || !existsSync(file))
|
|
32
|
+
return null;
|
|
33
|
+
const mode = statSync(file).mode & 0o777;
|
|
34
|
+
return mode & 0o077 ? mode : null;
|
|
35
|
+
}
|
|
36
|
+
function renameWithRetry(from, to) {
|
|
37
|
+
for (let attempt = 0;; attempt++) {
|
|
38
|
+
try {
|
|
39
|
+
renameSync(from, to);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const code = err.code;
|
|
44
|
+
if (!isWindows || attempt >= 4 || !['EPERM', 'EBUSY', 'EACCES'].includes(code ?? '')) {
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (attempt + 1));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { ConfigError } from "./domain/index.js";
|
|
3
|
+
export function readUserText(file) {
|
|
4
|
+
const bytes = readFileSync(file);
|
|
5
|
+
if (bytes.length >= 2 &&
|
|
6
|
+
((bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes[0] === 0xfe && bytes[1] === 0xff))) {
|
|
7
|
+
throw new ConfigError(`${file} is saved as UTF-16. Save it as UTF-8 — in Notepad choose "UTF-8" when saving; ` +
|
|
8
|
+
`in PowerShell write it with [IO.File]::WriteAllText(path, text).`);
|
|
9
|
+
}
|
|
10
|
+
const bom = bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
|
|
11
|
+
return bytes.toString('utf8', bom ? 3 : 0);
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function truncate(value, max) {
|
|
2
|
+
return value.length <= max ? value : value.slice(0, max - 1) + '…';
|
|
3
|
+
}
|
|
4
|
+
export function stripControl(text) {
|
|
5
|
+
return text.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, '');
|
|
6
|
+
}
|
|
7
|
+
export function shellArg(value) {
|
|
8
|
+
return /^[\w+:./\\~-]+$/.test(value) ? value : `"${value}"`;
|
|
9
|
+
}
|
|
10
|
+
export function oneLine(value) {
|
|
11
|
+
return value.replace(/\s*\n\s*/g, ' ');
|
|
12
|
+
}
|
|
13
|
+
export function formatCount(n) {
|
|
14
|
+
return n.toLocaleString('en-US');
|
|
15
|
+
}
|
|
16
|
+
export function formatBytes(n) {
|
|
17
|
+
if (n < 1024)
|
|
18
|
+
return `${n} B`;
|
|
19
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
20
|
+
let v = n / 1024;
|
|
21
|
+
let i = 0;
|
|
22
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
23
|
+
v /= 1024;
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
return `${v < 10 ? v.toFixed(1) : Math.round(v)} ${units[i]}`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
const DAY_MS = 86_400_000;
|
|
3
|
+
export function toIsoUtc(ms) {
|
|
4
|
+
return new Date(ms).toISOString();
|
|
5
|
+
}
|
|
6
|
+
export function utcDate(ms) {
|
|
7
|
+
return toIsoUtc(ms).slice(0, 10);
|
|
8
|
+
}
|
|
9
|
+
export function formatLocal(ms) {
|
|
10
|
+
const d = new Date(ms);
|
|
11
|
+
const offset = -d.getTimezoneOffset();
|
|
12
|
+
const sign = offset < 0 ? '-' : '+';
|
|
13
|
+
const abs = Math.abs(offset);
|
|
14
|
+
return (`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
15
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} ` +
|
|
16
|
+
`${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`);
|
|
17
|
+
}
|
|
18
|
+
export function relative(ms, now) {
|
|
19
|
+
const diff = ms - now;
|
|
20
|
+
const abs = Math.abs(diff);
|
|
21
|
+
const units = [
|
|
22
|
+
[DAY_MS, 'day'],
|
|
23
|
+
[3_600_000, 'hour'],
|
|
24
|
+
[60_000, 'minute']
|
|
25
|
+
];
|
|
26
|
+
let text = 'less than a minute';
|
|
27
|
+
for (const [size, name] of units) {
|
|
28
|
+
const n = Math.floor(abs / size);
|
|
29
|
+
if (n >= 1) {
|
|
30
|
+
text = `${n} ${name}${n === 1 ? '' : 's'}`;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return diff >= 0 ? `in ${text}` : `${text} ago`;
|
|
35
|
+
}
|
|
36
|
+
export function describeAt(iso, now, opts = {}) {
|
|
37
|
+
const ms = Date.parse(iso);
|
|
38
|
+
if (Number.isNaN(ms))
|
|
39
|
+
return iso;
|
|
40
|
+
return opts.local ? `${formatLocal(ms)} (${relative(ms, now)})` : relative(ms, now);
|
|
41
|
+
}
|
|
42
|
+
export function daysToMs(days) {
|
|
43
|
+
return days * DAY_MS;
|
|
44
|
+
}
|
|
45
|
+
function pad(n) {
|
|
46
|
+
return String(n).padStart(2, '0');
|
|
47
|
+
}
|
|
48
|
+
export async function sleep(ms, signal) {
|
|
49
|
+
try {
|
|
50
|
+
await delay(ms, undefined, { signal });
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err.name !== 'AbortError')
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function formatInterval(ms) {
|
|
58
|
+
return ms % 1000 === 0 ? `${ms / 1000}s` : `${ms} ms`;
|
|
59
|
+
}
|
|
60
|
+
export function maxTasksPerMinute(concurrency, intervalMs) {
|
|
61
|
+
return (concurrency * 60_000) / intervalMs;
|
|
62
|
+
}
|
|
63
|
+
export function formatThroughput(concurrency, intervalMs) {
|
|
64
|
+
const perMin = maxTasksPerMinute(concurrency, intervalMs);
|
|
65
|
+
const rate = perMin >= 1
|
|
66
|
+
? `${Math.round(perMin).toLocaleString('en-US')} tasks/min`
|
|
67
|
+
: `${Math.round(perMin * 60).toLocaleString('en-US')} tasks/hour`;
|
|
68
|
+
return `up to ${concurrency} at a time (≈ ${rate} at most)`;
|
|
69
|
+
}
|
|
70
|
+
export function parseDateArg(value) {
|
|
71
|
+
const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
72
|
+
if (day) {
|
|
73
|
+
const [, y, m, d] = day.map(Number);
|
|
74
|
+
const ms = new Date(y, m - 1, d).getTime();
|
|
75
|
+
const back = new Date(ms);
|
|
76
|
+
return back.getFullYear() === y && back.getMonth() === m - 1 && back.getDate() === d
|
|
77
|
+
? ms
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value))
|
|
81
|
+
return null;
|
|
82
|
+
const ms = Date.parse(value);
|
|
83
|
+
return Number.isNaN(ms) ? null : ms;
|
|
84
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { BIN, ENV, PACKAGE_NAME } from "./domain/index.js";
|
|
2
|
+
import { parseVersion } from "./version.js";
|
|
3
|
+
export const UPDATE_META = {
|
|
4
|
+
latest: 'cli_latest',
|
|
5
|
+
noticeAt: 'update_notice_at',
|
|
6
|
+
noticeVersion: 'update_notice_version'
|
|
7
|
+
};
|
|
8
|
+
export const UPDATE_NOTICE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
export function isReleaseVersion(value) {
|
|
10
|
+
return typeof value === 'string' && /^\d{1,6}\.\d{1,6}\.\d{1,6}$/.test(value);
|
|
11
|
+
}
|
|
12
|
+
export function isNewerVersion(a, b) {
|
|
13
|
+
const x = parseVersion(a);
|
|
14
|
+
const y = parseVersion(b);
|
|
15
|
+
if (!x || !y)
|
|
16
|
+
return false;
|
|
17
|
+
for (let i = 0; i < 3; i++) {
|
|
18
|
+
if (x[i] !== y[i])
|
|
19
|
+
return x[i] > y[i];
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
export function runsFromNpx(entry = process.argv[1]) {
|
|
24
|
+
return entry !== undefined && /[\\/]_npx[\\/]/.test(entry);
|
|
25
|
+
}
|
|
26
|
+
export function updateCommand(entry = process.argv[1]) {
|
|
27
|
+
return runsFromNpx(entry)
|
|
28
|
+
? `npx ${PACKAGE_NAME}@latest --version`
|
|
29
|
+
: `npm i -g ${PACKAGE_NAME}@latest`;
|
|
30
|
+
}
|
|
31
|
+
export function updateNote(running, entry = process.argv[1]) {
|
|
32
|
+
const npx = runsFromNpx(entry);
|
|
33
|
+
const restart = npx
|
|
34
|
+
? `stop it with '${BIN} stop' and start it again as 'npx ${PACKAGE_NAME}@latest start'`
|
|
35
|
+
: `stop it first with '${BIN} stop' and start it again after the update`;
|
|
36
|
+
const keep = npx ? ` Keep running it as 'npx ${PACKAGE_NAME}@latest <command>'.` : '';
|
|
37
|
+
if (running === null)
|
|
38
|
+
return `If '${BIN} start' is running in the background, ${restart}.${keep}`;
|
|
39
|
+
if (running)
|
|
40
|
+
return `'${BIN} start' is running — ${restart}.${keep}`;
|
|
41
|
+
return npx ? keep.trim() : null;
|
|
42
|
+
}
|
|
43
|
+
export function updateInfo(current, latest, running = false) {
|
|
44
|
+
const available = latest !== null && isNewerVersion(latest, current);
|
|
45
|
+
return {
|
|
46
|
+
current,
|
|
47
|
+
latest,
|
|
48
|
+
available,
|
|
49
|
+
command: available ? updateCommand() : null,
|
|
50
|
+
note: available ? updateNote(running) : null
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function rememberLatest(db, cli, now, log) {
|
|
54
|
+
try {
|
|
55
|
+
const latest = cli?.latest;
|
|
56
|
+
if (isReleaseVersion(latest))
|
|
57
|
+
db.setMeta(UPDATE_META.latest, latest, now);
|
|
58
|
+
else
|
|
59
|
+
db.deleteMeta(UPDATE_META.latest);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
log?.('could not record the latest CLI version', { message: err.message });
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function recordedLatest(db) {
|
|
68
|
+
const latest = db.getMeta(UPDATE_META.latest);
|
|
69
|
+
return isReleaseVersion(latest) ? latest : null;
|
|
70
|
+
}
|
|
71
|
+
export function readLatest(db, log) {
|
|
72
|
+
try {
|
|
73
|
+
return recordedLatest(db);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
log?.('could not read the latest CLI version', { message: err.message });
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function updateNoticeSuppressed(env) {
|
|
81
|
+
const on = (v) => v !== undefined && v !== '' && v !== '0' && v.toLowerCase() !== 'false';
|
|
82
|
+
return on(env[ENV.noUpdateNotice]) || env.NO_UPDATE_NOTIFIER !== undefined || on(env.CI);
|
|
83
|
+
}
|
|
84
|
+
export function updateNoticeLine(info) {
|
|
85
|
+
const line = `update available: ${BIN} ${info.current} → ${info.latest} — run: ${info.command}`;
|
|
86
|
+
return info.note ? `${line}\n${info.note}` : line;
|
|
87
|
+
}
|
|
88
|
+
export function claimUpdateNotice(db, info, now, env, log) {
|
|
89
|
+
if (!info.available || info.latest === null || updateNoticeSuppressed(env))
|
|
90
|
+
return false;
|
|
91
|
+
try {
|
|
92
|
+
const at = Number(db.getMeta(UPDATE_META.noticeAt) ?? NaN);
|
|
93
|
+
const sameVersion = db.getMeta(UPDATE_META.noticeVersion) === info.latest;
|
|
94
|
+
if (sameVersion &&
|
|
95
|
+
Number.isFinite(at) &&
|
|
96
|
+
now - at >= 0 &&
|
|
97
|
+
now - at < UPDATE_NOTICE_INTERVAL_MS) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
db.setMetas({ [UPDATE_META.noticeAt]: String(now), [UPDATE_META.noticeVersion]: info.latest }, now);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
log?.('could not record the update notice', { message: err.message });
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
export class LatestWatch {
|
|
108
|
+
opts;
|
|
109
|
+
seen = undefined;
|
|
110
|
+
written = undefined;
|
|
111
|
+
armed = false;
|
|
112
|
+
announcedAt = 0;
|
|
113
|
+
constructor(opts) {
|
|
114
|
+
this.opts = opts;
|
|
115
|
+
}
|
|
116
|
+
observe(cli) {
|
|
117
|
+
const latest = isReleaseVersion(cli?.latest) ? cli.latest : null;
|
|
118
|
+
if (latest !== this.written) {
|
|
119
|
+
const ok = rememberLatest(this.opts.db, latest === null ? undefined : { latest }, this.opts.now(), this.opts.log);
|
|
120
|
+
this.written = ok ? latest : undefined;
|
|
121
|
+
}
|
|
122
|
+
const changed = this.seen !== undefined && latest !== this.seen;
|
|
123
|
+
this.seen = latest;
|
|
124
|
+
if (!this.armed)
|
|
125
|
+
return;
|
|
126
|
+
if (changed || this.opts.now() - this.announcedAt >= UPDATE_NOTICE_INTERVAL_MS)
|
|
127
|
+
this.fire();
|
|
128
|
+
}
|
|
129
|
+
arm() {
|
|
130
|
+
this.armed = true;
|
|
131
|
+
this.fire();
|
|
132
|
+
}
|
|
133
|
+
fire() {
|
|
134
|
+
this.announcedAt = this.opts.now();
|
|
135
|
+
try {
|
|
136
|
+
this.opts.announce();
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
this.opts.log?.('could not announce the CLI update', { message: err.message });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|