@aragon-agent/cli 0.4.3 → 0.5.1
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/CHANGELOG.md +131 -0
- package/README.md +227 -3
- package/dist/agent/agent-mode.d.ts +53 -0
- package/dist/agent/agent-mode.js +69 -0
- package/dist/agent/controller.d.ts +69 -0
- package/dist/agent/controller.js +154 -1
- package/dist/agent/plan-prompt.d.ts +18 -0
- package/dist/agent/plan-prompt.js +93 -0
- package/dist/agent/reducer.d.ts +14 -1
- package/dist/agent/reducer.js +9 -0
- package/dist/agent/system-prompt.d.ts +15 -0
- package/dist/agent/system-prompt.js +8 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +183 -20
- package/dist/commands/builtins.js +60 -0
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +71 -4
- package/dist/config/app-paths.js +139 -8
- package/dist/config/cli-commands.d.ts +43 -0
- package/dist/config/cli-commands.js +196 -0
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.js +45 -1
- package/dist/config/load.d.ts +8 -0
- package/dist/config/load.js +59 -2
- package/dist/config/migrate-home.d.ts +63 -0
- package/dist/config/migrate-home.js +165 -0
- package/dist/config/migrate-legacy-state.d.ts +10 -0
- package/dist/config/migrate-legacy-state.js +15 -6
- package/dist/config/schema.d.ts +61 -0
- package/dist/config/schema.js +83 -0
- package/dist/config/store.d.ts +30 -12
- package/dist/config/store.js +47 -35
- package/dist/logging/cli-commands.d.ts +25 -0
- package/dist/logging/cli-commands.js +221 -0
- package/dist/logging/file-sink.d.ts +114 -0
- package/dist/logging/file-sink.js +326 -0
- package/dist/logging/install.d.ts +163 -0
- package/dist/logging/install.js +302 -0
- package/dist/logging/levels.d.ts +40 -0
- package/dist/logging/levels.js +59 -0
- package/dist/logging/logger.d.ts +110 -0
- package/dist/logging/logger.js +237 -0
- package/dist/logging/redact.d.ts +28 -0
- package/dist/logging/redact.js +90 -0
- package/dist/logging/secret-registry.d.ts +30 -0
- package/dist/logging/secret-registry.js +55 -0
- package/dist/skills/paths.d.ts +2 -4
- package/dist/skills/paths.js +7 -9
- package/dist/tools/human-input.d.ts +179 -0
- package/dist/tools/human-input.js +227 -0
- package/dist/tools/index.d.ts +35 -3
- package/dist/tools/index.js +96 -9
- package/dist/tools/plan-tools.d.ts +64 -0
- package/dist/tools/plan-tools.js +198 -0
- package/dist/ui/App.d.ts +4 -1
- package/dist/ui/App.js +182 -11
- package/dist/ui/Composer.d.ts +4 -1
- package/dist/ui/Composer.js +26 -7
- package/dist/ui/ModeChip.d.ts +29 -0
- package/dist/ui/ModeChip.js +15 -0
- package/dist/ui/PromptInput.d.ts +8 -1
- package/dist/ui/PromptInput.js +12 -2
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +10 -2
- package/dist/ui/entries/ToolPreview.d.ts +2 -0
- package/dist/ui/entries/ToolPreview.js +64 -0
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +8 -0
- package/dist/ui/layout/wrap-rows.d.ts +24 -0
- package/dist/ui/layout/wrap-rows.js +74 -0
- package/dist/ui/overlays/HelpOverlay.js +13 -0
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +50 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +117 -0
- package/dist/ui/overlays/QuestionOverlay.d.ts +30 -0
- package/dist/ui/overlays/QuestionOverlay.js +193 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/use-startup-notices.d.ts +15 -0
- package/dist/ui/use-startup-notices.js +38 -0
- package/package.json +3 -2
- package/runtime/insecure-tls-warning.cjs +62 -0
- package/runtime/insecure-tls-warning.d.cts +5 -0
package/dist/config/store.js
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config store — read/atomic-write the user config JSON
|
|
3
|
-
*
|
|
4
|
-
* `0600` permissions on POSIX (spec §3.5 / §6.1).
|
|
2
|
+
* Config store — read/atomic-write the user config JSON. Paths come from
|
|
3
|
+
* `app-paths.ts`; nothing here does its own path arithmetic. The file may hold
|
|
4
|
+
* secrets, so it is written with `0600` permissions on POSIX (spec §3.5 / §6.1).
|
|
5
|
+
*
|
|
6
|
+
* There is deliberately no `getConfigDir()` / `getSkillsDir()` here any more.
|
|
7
|
+
* The first would now return the entire user root under a name that says
|
|
8
|
+
* otherwise (see the test-isolation contract in `app-paths.ts`); the second was
|
|
9
|
+
* a second name for `skills/paths.ts::getUserSkillsDir()`, which is precisely
|
|
10
|
+
* the drift the header of that module warns about.
|
|
5
11
|
*/
|
|
6
12
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, chmodSync } from 'node:fs';
|
|
7
|
-
import { dirname
|
|
13
|
+
import { dirname } from 'node:path';
|
|
8
14
|
import process from 'node:process';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
export
|
|
15
|
-
return paths.config;
|
|
16
|
-
}
|
|
17
|
-
export function getConfigPath() {
|
|
18
|
-
return join(paths.config, 'config.json');
|
|
19
|
-
}
|
|
20
|
-
export function getSessionsDir() {
|
|
21
|
-
return join(paths.data, 'sessions');
|
|
22
|
-
}
|
|
23
|
-
/** User-scope skills root — `aragon skills install`'s default target. */
|
|
24
|
-
export function getSkillsDir() {
|
|
25
|
-
return join(paths.data, 'skills');
|
|
26
|
-
}
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Read
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
15
|
+
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
|
+
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
+
import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampSkillsConfig, } from './schema.js';
|
|
18
|
+
// Re-exported so the many existing importers of `store.js` keep working; the
|
|
19
|
+
// definitions live in `app-paths.ts` because they are pure path arithmetic.
|
|
20
|
+
export { getConfigPath, getSessionsDir };
|
|
30
21
|
/**
|
|
31
|
-
* Read the persisted config file.
|
|
32
|
-
*
|
|
22
|
+
* Read the persisted config file.
|
|
23
|
+
*
|
|
24
|
+
* A corrupt config file still must never crash the CLI — the fallback to
|
|
25
|
+
* defaults is unchanged. What changed is that it is no longer SILENT: a single
|
|
26
|
+
* stray comma used to reset the model, theme, timeouts, skills and (as far as
|
|
27
|
+
* the user could tell) the API key, with nothing anywhere saying why. Now the
|
|
28
|
+
* failure travels out of band in `parseError`, and `loadConfig()` turns it into
|
|
29
|
+
* a log record plus one visible line. Hand-editing this file is a supported
|
|
30
|
+
* workflow, so the failure mode had to become visible.
|
|
33
31
|
*/
|
|
34
32
|
export function readConfigFile() {
|
|
35
33
|
const file = getConfigPath();
|
|
36
34
|
if (!existsSync(file))
|
|
37
|
-
return null;
|
|
35
|
+
return { config: null };
|
|
38
36
|
try {
|
|
39
37
|
const raw = readFileSync(file, 'utf-8');
|
|
40
38
|
const parsed = JSON.parse(raw);
|
|
41
39
|
if (parsed && typeof parsed === 'object') {
|
|
42
|
-
return parsed;
|
|
40
|
+
return { config: parsed };
|
|
43
41
|
}
|
|
44
|
-
return null;
|
|
42
|
+
return { config: null, parseError: `${file} does not contain a JSON object` };
|
|
45
43
|
}
|
|
46
|
-
catch {
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
config: null,
|
|
47
|
+
parseError: `${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
48
|
+
};
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
@@ -82,7 +82,7 @@ export function writeConfigFile(config) {
|
|
|
82
82
|
* Unlike `readConfigFile`, this always returns a complete `PersistedConfig`.
|
|
83
83
|
*/
|
|
84
84
|
export function loadPersistedConfig() {
|
|
85
|
-
const partial = readConfigFile() ?? {};
|
|
85
|
+
const partial = readConfigFile().config ?? {};
|
|
86
86
|
return {
|
|
87
87
|
...DEFAULT_CONFIG,
|
|
88
88
|
...partial,
|
|
@@ -93,6 +93,8 @@ export function loadPersistedConfig() {
|
|
|
93
93
|
// `skills` is the second nested object in this file and needs the same
|
|
94
94
|
// treatment `apiKeys` gets — see the note on updatePersistedConfig below.
|
|
95
95
|
skills: clampSkillsConfig({ ...DEFAULT_CONFIG.skills, ...(partial.skills ?? {}) }),
|
|
96
|
+
// `log` is the third. Same one-level rule, same merge.
|
|
97
|
+
log: clampLogConfig({ ...DEFAULT_CONFIG.log, ...(partial.log ?? {}) }),
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
@@ -110,9 +112,15 @@ export function loadPersistedConfig() {
|
|
|
110
112
|
*
|
|
111
113
|
* `SkillsConfig` is therefore capped at ONE level of nesting (scalars and
|
|
112
114
|
* string arrays only). A third level requires replacing this hand-written merge
|
|
113
|
-
* with a real deep-merge utility first, or the same bug returns.
|
|
115
|
+
* with a real deep-merge utility first, or the same bug returns. `LogConfig` is
|
|
116
|
+
* a third SECTION, but it is still one level deep (scalars only), so the same
|
|
117
|
+
* hand-written merge covers it — and it must stay that way for that reason.
|
|
114
118
|
*/
|
|
115
119
|
export function updatePersistedConfig(patch) {
|
|
120
|
+
// Registration site 5. Whatever route a key took to get here, this is the one
|
|
121
|
+
// function that writes it to disk, so this is the last place it can be added
|
|
122
|
+
// to the redactor's backstop before it also starts appearing in log records.
|
|
123
|
+
registerSecretsFrom(patch.apiKeys);
|
|
116
124
|
const current = loadPersistedConfig();
|
|
117
125
|
const merged = {
|
|
118
126
|
...current,
|
|
@@ -120,6 +128,10 @@ export function updatePersistedConfig(patch) {
|
|
|
120
128
|
version: CONFIG_VERSION,
|
|
121
129
|
apiKeys: { ...current.apiKeys, ...(patch.apiKeys ?? {}) },
|
|
122
130
|
skills: clampSkillsConfig({ ...current.skills, ...(patch.skills ?? {}) }),
|
|
131
|
+
// Without this line, changing the log level from the settings screen would
|
|
132
|
+
// replace the whole section and quietly revert `maxFiles`, `previewChars`
|
|
133
|
+
// and — worst of all — `redactSecrets`.
|
|
134
|
+
log: clampLogConfig({ ...current.log, ...(patch.log ?? {}) }),
|
|
123
135
|
};
|
|
124
136
|
writeConfigFile(merged);
|
|
125
137
|
return merged;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aragon logs path | list | tail | clear | open` — the non-interactive surface.
|
|
3
|
+
*
|
|
4
|
+
* Exit codes follow the existing convention: 0 success · 1 run-time failure ·
|
|
5
|
+
* 2 usage error.
|
|
6
|
+
*
|
|
7
|
+
* THE WHOLE GROUP IS EXEMPT from the "every subcommand records one info line"
|
|
8
|
+
* rule, and that is a correctness requirement rather than tidiness. Recording a
|
|
9
|
+
* line opens today's log file, and Node opens files without
|
|
10
|
+
* `FILE_SHARE_DELETE`; on Windows — the platform this feature was asked for —
|
|
11
|
+
* `aragon logs clear --yes` would then be unable to delete the very file its own
|
|
12
|
+
* announcement had just created. These commands are the log's operations
|
|
13
|
+
* surface; giving them observable side effects on the log is backwards.
|
|
14
|
+
*/
|
|
15
|
+
import { type CliFlags } from '../config/load.js';
|
|
16
|
+
export interface LogsCliOptions {
|
|
17
|
+
/** `-n <N>` — how many trailing lines `tail` prints (default 100). */
|
|
18
|
+
lines?: string;
|
|
19
|
+
follow?: boolean;
|
|
20
|
+
level?: string;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
/** Required by `clear`; without it the command refuses. */
|
|
23
|
+
yes?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function runLogsCommand(subcommand: string, opts: LogsCliOptions, flags: CliFlags): Promise<number>;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aragon logs path | list | tail | clear | open` — the non-interactive surface.
|
|
3
|
+
*
|
|
4
|
+
* Exit codes follow the existing convention: 0 success · 1 run-time failure ·
|
|
5
|
+
* 2 usage error.
|
|
6
|
+
*
|
|
7
|
+
* THE WHOLE GROUP IS EXEMPT from the "every subcommand records one info line"
|
|
8
|
+
* rule, and that is a correctness requirement rather than tidiness. Recording a
|
|
9
|
+
* line opens today's log file, and Node opens files without
|
|
10
|
+
* `FILE_SHARE_DELETE`; on Windows — the platform this feature was asked for —
|
|
11
|
+
* `aragon logs clear --yes` would then be unable to delete the very file its own
|
|
12
|
+
* announcement had just created. These commands are the log's operations
|
|
13
|
+
* surface; giving them observable side effects on the log is backwards.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, rmSync, statSync, unwatchFile, watchFile, } from 'node:fs';
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import process from 'node:process';
|
|
19
|
+
import { getLogsDir } from '../config/app-paths.js';
|
|
20
|
+
import { loadConfig } from '../config/load.js';
|
|
21
|
+
import { isLogFileName } from './file-sink.js';
|
|
22
|
+
import { getLogger } from './logger.js';
|
|
23
|
+
import { LOG_LEVELS, isLogLevel } from './levels.js';
|
|
24
|
+
const EXIT_OK = 0;
|
|
25
|
+
const EXIT_RUNTIME = 1;
|
|
26
|
+
const EXIT_USAGE = 2;
|
|
27
|
+
const DEFAULT_TAIL_LINES = 100;
|
|
28
|
+
/** `watchFile` poll interval for `--follow` (Q2). */
|
|
29
|
+
const FOLLOW_POLL_MS = 500;
|
|
30
|
+
/** The effective log directory for this run (flag › env › file › default). */
|
|
31
|
+
function resolveLogsDir(flags) {
|
|
32
|
+
const configured = loadConfig(flags).log.dir.trim();
|
|
33
|
+
return configured.length > 0 ? configured : getLogsDir();
|
|
34
|
+
}
|
|
35
|
+
function listLogFiles(dir) {
|
|
36
|
+
if (!existsSync(dir))
|
|
37
|
+
return [];
|
|
38
|
+
try {
|
|
39
|
+
return readdirSync(dir)
|
|
40
|
+
.filter(isLogFileName)
|
|
41
|
+
.sort();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function runLogsCommand(subcommand, opts, flags) {
|
|
48
|
+
const dir = resolveLogsDir(flags);
|
|
49
|
+
switch (subcommand) {
|
|
50
|
+
case 'path':
|
|
51
|
+
return runPath(dir);
|
|
52
|
+
case 'list':
|
|
53
|
+
return runList(dir);
|
|
54
|
+
case 'tail':
|
|
55
|
+
return runTail(dir, opts);
|
|
56
|
+
case 'clear':
|
|
57
|
+
return runClear(dir, opts);
|
|
58
|
+
case 'open':
|
|
59
|
+
return runOpen(dir);
|
|
60
|
+
default:
|
|
61
|
+
process.stderr.write(`Unknown logs subcommand "${subcommand}". Try: path | list | tail | clear | open.\n`);
|
|
62
|
+
return EXIT_USAGE;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function runPath(dir) {
|
|
66
|
+
const logger = getLogger();
|
|
67
|
+
process.stdout.write(`${logger.currentLogPath()}\n`);
|
|
68
|
+
process.stdout.write(`directory: ${dir}\n`);
|
|
69
|
+
// `debug` and `trace` write prompt and tool output verbatim; a user about to
|
|
70
|
+
// attach a log to a bug report should be told that here, not discover it.
|
|
71
|
+
process.stdout.write('level: ' + logger.level + ' (debug/trace record prompt and tool content)\n');
|
|
72
|
+
if (logger.sinkDisabled) {
|
|
73
|
+
process.stdout.write(`WARNING: logging is disabled after write failures: ${logger.sinkError}\n`);
|
|
74
|
+
}
|
|
75
|
+
return EXIT_OK;
|
|
76
|
+
}
|
|
77
|
+
function runList(dir) {
|
|
78
|
+
const files = listLogFiles(dir);
|
|
79
|
+
if (files.length === 0) {
|
|
80
|
+
process.stdout.write(`No log files in ${dir}.\n`);
|
|
81
|
+
return EXIT_OK;
|
|
82
|
+
}
|
|
83
|
+
for (const name of files) {
|
|
84
|
+
try {
|
|
85
|
+
const stat = statSync(join(dir, name));
|
|
86
|
+
process.stdout.write(`${name} ${stat.size} bytes ${stat.mtime.toISOString()}\n`);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
process.stdout.write(`${name} (unreadable)\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return EXIT_OK;
|
|
93
|
+
}
|
|
94
|
+
/** Render one JSONL record as a human-readable line; pass unparseable ones through. */
|
|
95
|
+
function renderRecord(line) {
|
|
96
|
+
try {
|
|
97
|
+
const record = JSON.parse(line);
|
|
98
|
+
const data = record.data ? ` ${JSON.stringify(record.data)}` : '';
|
|
99
|
+
const level = (record.lv ?? '?').padEnd(5);
|
|
100
|
+
return `${record.ts ?? ''} ${level} ${record.scope ?? '-'}: ${record.msg ?? ''}${data}`;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return line;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function passesLevel(line, min) {
|
|
107
|
+
if (!min)
|
|
108
|
+
return true;
|
|
109
|
+
try {
|
|
110
|
+
const record = JSON.parse(line);
|
|
111
|
+
if (!isLogLevel(record.lv))
|
|
112
|
+
return true;
|
|
113
|
+
return LOG_LEVELS[record.lv] <= LOG_LEVELS[min];
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function printLines(lines, opts, min) {
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
if (line.trim().length === 0)
|
|
122
|
+
continue;
|
|
123
|
+
if (!passesLevel(line, min))
|
|
124
|
+
continue;
|
|
125
|
+
process.stdout.write(`${opts.json ? line : renderRecord(line)}\n`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function runTail(dir, opts) {
|
|
129
|
+
const files = listLogFiles(dir);
|
|
130
|
+
const target = files.length > 0 ? join(dir, files[files.length - 1]) : null;
|
|
131
|
+
if (!target) {
|
|
132
|
+
process.stdout.write(`No log files in ${dir}.\n`);
|
|
133
|
+
return EXIT_OK;
|
|
134
|
+
}
|
|
135
|
+
if (opts.level !== undefined && !isLogLevel(opts.level)) {
|
|
136
|
+
process.stderr.write(`Unknown level "${opts.level}".\n`);
|
|
137
|
+
return EXIT_USAGE;
|
|
138
|
+
}
|
|
139
|
+
const min = opts.level;
|
|
140
|
+
const count = Number.parseInt(opts.lines ?? '', 10);
|
|
141
|
+
const wanted = Number.isFinite(count) && count > 0 ? count : DEFAULT_TAIL_LINES;
|
|
142
|
+
let consumed = 0;
|
|
143
|
+
try {
|
|
144
|
+
const raw = readFileSync(target, 'utf-8');
|
|
145
|
+
consumed = raw.length;
|
|
146
|
+
printLines(raw.split('\n').slice(-wanted), opts, min);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
process.stderr.write(`Could not read ${target}: ${err instanceof Error ? err.message : err}\n`);
|
|
150
|
+
return EXIT_RUNTIME;
|
|
151
|
+
}
|
|
152
|
+
if (!opts.follow)
|
|
153
|
+
return EXIT_OK;
|
|
154
|
+
// `watchFile` (polling) rather than `fs.watch`: these files are ROTATED by
|
|
155
|
+
// rename, which is exactly the case `fs.watch` reports inconsistently across
|
|
156
|
+
// platforms. Half a second of latency is invisible to someone reading a log.
|
|
157
|
+
watchFile(target, { interval: FOLLOW_POLL_MS }, () => {
|
|
158
|
+
try {
|
|
159
|
+
const raw = readFileSync(target, 'utf-8');
|
|
160
|
+
if (raw.length < consumed)
|
|
161
|
+
consumed = 0; // truncated or rotated underneath us
|
|
162
|
+
printLines(raw.slice(consumed).split('\n'), opts, min);
|
|
163
|
+
consumed = raw.length;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// The file may vanish mid-rotation; the next poll picks the new one up.
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
process.on('SIGINT', () => unwatchFile(target));
|
|
170
|
+
return EXIT_OK;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Delete every log file.
|
|
174
|
+
*
|
|
175
|
+
* Closes this process's own sink first — otherwise the day's file is held open
|
|
176
|
+
* by the very command trying to remove it. Failures are reported PER FILE
|
|
177
|
+
* rather than aborting: another `aragon` window holding one file open should
|
|
178
|
+
* not stop the other nine from being cleared.
|
|
179
|
+
*/
|
|
180
|
+
function runClear(dir, opts) {
|
|
181
|
+
if (!opts.yes) {
|
|
182
|
+
process.stderr.write('Refusing to delete log files without --yes.\n');
|
|
183
|
+
return EXIT_USAGE;
|
|
184
|
+
}
|
|
185
|
+
getLogger().closeSink();
|
|
186
|
+
const files = listLogFiles(dir);
|
|
187
|
+
const blocked = [];
|
|
188
|
+
let removed = 0;
|
|
189
|
+
for (const name of files) {
|
|
190
|
+
try {
|
|
191
|
+
rmSync(join(dir, name), { force: true });
|
|
192
|
+
removed += 1;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
blocked.push(name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
process.stdout.write(`Deleted ${removed} log file(s).\n`);
|
|
199
|
+
if (blocked.length === 0)
|
|
200
|
+
return EXIT_OK;
|
|
201
|
+
process.stderr.write(`In use, not deleted: ${blocked.join(', ')}\n`);
|
|
202
|
+
return EXIT_RUNTIME;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Open the log directory in the platform file manager.
|
|
206
|
+
*
|
|
207
|
+
* A convenience, so a failure degrades to printing the path and still exits 0 —
|
|
208
|
+
* being unable to launch a file manager is not an error in a logging command.
|
|
209
|
+
*/
|
|
210
|
+
function runOpen(dir) {
|
|
211
|
+
const opener = process.platform === 'win32' ? 'explorer' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
212
|
+
try {
|
|
213
|
+
const child = spawn(opener, [dir], { stdio: 'ignore', detached: true });
|
|
214
|
+
child.on('error', () => process.stdout.write(`${dir}\n`));
|
|
215
|
+
child.unref();
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
process.stdout.write(`${dir}\n`);
|
|
219
|
+
}
|
|
220
|
+
return EXIT_OK;
|
|
221
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file sink — a memory queue in front of ONE synchronous append fd.
|
|
3
|
+
*
|
|
4
|
+
* WHY NOT `createWriteStream` (design P1-4). Three reasons, each fatal on its
|
|
5
|
+
* own:
|
|
6
|
+
* - a stream's internal buffer is DISCARDED by `process.exit()`, and the
|
|
7
|
+
* records still sitting in it during a crash are the ones worth having;
|
|
8
|
+
* - an exit-path `appendFileSync` plus a steady-state stream are two writers
|
|
9
|
+
* on one file with no defined ordering between them;
|
|
10
|
+
* - a stream reports write failures through an async `'error'` event, which
|
|
11
|
+
* THROWS when nobody is listening — so "make the log directory read-only"
|
|
12
|
+
* would take the whole CLI down instead of degrading.
|
|
13
|
+
*
|
|
14
|
+
* With `openSync` + `writeSync` every failure is a synchronous throw one
|
|
15
|
+
* `try/catch` away, and `flushSync()` and the steady-state flush are literally
|
|
16
|
+
* the same code path. `O_APPEND` keeps concurrent `aragon` processes from
|
|
17
|
+
* truncating each other, and one batched `writeSync` stays far below any size
|
|
18
|
+
* that could tear a line.
|
|
19
|
+
*/
|
|
20
|
+
export interface FileSinkOptions {
|
|
21
|
+
dir: string;
|
|
22
|
+
maxFileBytes: number;
|
|
23
|
+
maxFiles: number;
|
|
24
|
+
/**
|
|
25
|
+
* Renders the "records were dropped" warning line. Not a persisted setting —
|
|
26
|
+
* the sink handles LINES and has no session id or clock of its own, so the
|
|
27
|
+
* logger supplies the rendering and the sink decides when it is needed.
|
|
28
|
+
*/
|
|
29
|
+
formatDropNotice?: (count: number) => string;
|
|
30
|
+
/** Called once, when the sink gives up on writing. */
|
|
31
|
+
onFailure?: (reason: string) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `YYYY-MM-DD` in LOCAL time.
|
|
35
|
+
*
|
|
36
|
+
* Local, not UTC, and the record timestamps use the same clock deliberately: a
|
|
37
|
+
* user west of UTC would otherwise find an evening's records filed under the
|
|
38
|
+
* previous local day and stamped with the next one, which makes "open today's
|
|
39
|
+
* log" a lie twice over.
|
|
40
|
+
*/
|
|
41
|
+
export declare function localDateStamp(date?: Date): string;
|
|
42
|
+
/**
|
|
43
|
+
* Delete the oldest files until at most `maxFiles` remain.
|
|
44
|
+
*
|
|
45
|
+
* A STANDALONE STEP, called on three occasions — first write of the process,
|
|
46
|
+
* midnight roll-over, and size rotation — because retention that only ran
|
|
47
|
+
* during size rotation would never fire for the many users who never reach
|
|
48
|
+
* `maxFileBytes`: one file per day, forever, under a setting that says 10.
|
|
49
|
+
*
|
|
50
|
+
* Never throws. On Windows another `aragon` process may hold a handle on a file
|
|
51
|
+
* we would like to remove; that is a reason to skip it, not to fail a write.
|
|
52
|
+
*/
|
|
53
|
+
export declare function enforceRetention(dir: string, maxFiles: number): void;
|
|
54
|
+
export declare class FileSink {
|
|
55
|
+
private options;
|
|
56
|
+
private queue;
|
|
57
|
+
private dropped;
|
|
58
|
+
private fd;
|
|
59
|
+
private currentFile;
|
|
60
|
+
private currentDay;
|
|
61
|
+
private currentSize;
|
|
62
|
+
private timer;
|
|
63
|
+
private failures;
|
|
64
|
+
private failed;
|
|
65
|
+
private failureReason;
|
|
66
|
+
private retentionRun;
|
|
67
|
+
constructor(options: FileSinkOptions);
|
|
68
|
+
get disabled(): boolean;
|
|
69
|
+
get lastError(): string | undefined;
|
|
70
|
+
/** The file records are going to right now (may not exist until first write). */
|
|
71
|
+
get currentPath(): string;
|
|
72
|
+
/** Enqueue one already-serialised line (including its trailing newline). */
|
|
73
|
+
write(line: string): void;
|
|
74
|
+
/** Threshold/timer flush. Identical to `flushSync` — see the module header. */
|
|
75
|
+
flush(): void;
|
|
76
|
+
/** Idempotent: safe to call from `exit`, a signal handler and a crash handler. */
|
|
77
|
+
flushSync(): void;
|
|
78
|
+
close(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Apply new settings. Changing the directory closes the current fd so the next
|
|
81
|
+
* write lazily opens the new one; the sink never eagerly creates a directory
|
|
82
|
+
* the user may never write a record into.
|
|
83
|
+
*/
|
|
84
|
+
reconfigure(options: FileSinkOptions): void;
|
|
85
|
+
private takeBatch;
|
|
86
|
+
/** Open (or roll) the target file so `incomingBytes` can be appended to it. */
|
|
87
|
+
private prepareTarget;
|
|
88
|
+
private openTarget;
|
|
89
|
+
/**
|
|
90
|
+
* flush → close → rename → open → retention, in that order.
|
|
91
|
+
*
|
|
92
|
+
* `closeSync` MUST precede `renameSync`: on Windows renaming a file the
|
|
93
|
+
* process still holds a handle on fails with EPERM. A failed rename is
|
|
94
|
+
* swallowed and writing continues into the original file — two instances
|
|
95
|
+
* rotating at once should cost an extra small file, not the log itself.
|
|
96
|
+
*/
|
|
97
|
+
private rotateBySize;
|
|
98
|
+
/** Highest existing `aragon-<day>.<n>.log` index, plus one. */
|
|
99
|
+
private nextRotationIndex;
|
|
100
|
+
private closeFd;
|
|
101
|
+
/**
|
|
102
|
+
* Logging failure must never become CLI failure, and must never write to the
|
|
103
|
+
* terminal itself (invariant I-4 — a stray line permanently misaligns the
|
|
104
|
+
* full-screen frame). The owner is told once through `onFailure`, which routes
|
|
105
|
+
* to a toast when the TUI is mounted and to stderr when it is not.
|
|
106
|
+
*/
|
|
107
|
+
private recordFailure;
|
|
108
|
+
private startTimer;
|
|
109
|
+
private stopTimer;
|
|
110
|
+
}
|
|
111
|
+
/** Exposed for the CLI's `logs` commands, which list the same file family. */
|
|
112
|
+
export declare function isLogFileName(name: string): boolean;
|
|
113
|
+
/** The log file the CURRENT day would use inside a log directory. */
|
|
114
|
+
export declare function currentLogFileName(date?: Date): string;
|