@nonbot/cli 0.9.13 → 0.10.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 +273 -1
- package/dist/commands/choir.js +3 -0
- package/dist/commands/daemon.js +368 -9
- package/dist/commands/logs.js +6 -1
- package/dist/lib/activations.js +46 -6
- package/dist/lib/activity-log.js +6 -0
- package/dist/lib/choir/hub.js +116 -18
- package/dist/lib/cloud-repo.js +306 -0
- package/dist/lib/command-builders.js +32 -1
- package/dist/lib/completion.js +125 -1
- package/dist/lib/daemon-lifecycle.js +15 -0
- package/dist/lib/exit-transcript.js +74 -0
- package/dist/lib/machine.js +7 -0
- package/dist/lib/payload-validator.js +69 -0
- package/dist/lib/run-prompt.js +38 -17
- package/dist/lib/snapshot.js +14 -12
- package/dist/lib/terminal.js +10 -4
- package/dist/lib/update-check.js +99 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,21 @@ function configDir() {
|
|
|
9
9
|
return override;
|
|
10
10
|
return path.join(homedir(), '.config', 'nonbot');
|
|
11
11
|
}
|
|
12
|
+
export function shutdownIssuedAt(signal) {
|
|
13
|
+
if (!signal)
|
|
14
|
+
return null;
|
|
15
|
+
for (const v of [signal.requestedAt, signal.issuedAt]) {
|
|
16
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function shutdownPredatesDaemon(signal, daemonStartedAt) {
|
|
22
|
+
const issuedAt = shutdownIssuedAt(signal);
|
|
23
|
+
if (issuedAt === null)
|
|
24
|
+
return false;
|
|
25
|
+
return issuedAt < daemonStartedAt;
|
|
26
|
+
}
|
|
12
27
|
export function pidFilePath(dir = configDir()) {
|
|
13
28
|
return path.join(dir, 'daemon.pid');
|
|
14
29
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { captureSnapshotPane, scrubSnapshot, clampSnapshotTail, SNAPSHOT_MAX_BYTES, } from './snapshot.js';
|
|
2
|
+
import { appendActivityLog } from './activity-log.js';
|
|
3
|
+
export const EXIT_TRANSCRIPT_KIND = 'exit-transcript';
|
|
4
|
+
export const EXIT_TRANSCRIPT_LINES = 200;
|
|
5
|
+
export const EXIT_TRANSCRIPT_MAX_BYTES = SNAPSHOT_MAX_BYTES;
|
|
6
|
+
export const EXIT_TRANSCRIPT_OPT_OUT_ENV = 'NONBOT_NO_EXIT_TRANSCRIPT';
|
|
7
|
+
export function exitTranscriptDisabled(env = process.env) {
|
|
8
|
+
const v = env[EXIT_TRANSCRIPT_OPT_OUT_ENV];
|
|
9
|
+
if (typeof v !== 'string')
|
|
10
|
+
return false;
|
|
11
|
+
const t = v.trim().toLowerCase();
|
|
12
|
+
return t === '1' || t === 'true';
|
|
13
|
+
}
|
|
14
|
+
export function refreshPaneTails(opts) {
|
|
15
|
+
const now = opts.now ?? Date.now;
|
|
16
|
+
const maxLines = opts.maxLines ?? EXIT_TRANSCRIPT_LINES;
|
|
17
|
+
let refreshed = 0;
|
|
18
|
+
for (const [activationId, paneId] of opts.tracked) {
|
|
19
|
+
let raw = '';
|
|
20
|
+
try {
|
|
21
|
+
raw = captureSnapshotPane(paneId, maxLines, opts.spawnImpl);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
raw = '';
|
|
25
|
+
}
|
|
26
|
+
if (raw === '')
|
|
27
|
+
continue;
|
|
28
|
+
const { text, truncated } = clampSnapshotTail(scrubSnapshot(raw), EXIT_TRANSCRIPT_MAX_BYTES);
|
|
29
|
+
opts.tails.set(activationId, {
|
|
30
|
+
text,
|
|
31
|
+
truncated,
|
|
32
|
+
capturedAt: now(),
|
|
33
|
+
lines: text.length === 0 ? 0 : text.split('\n').length,
|
|
34
|
+
});
|
|
35
|
+
refreshed++;
|
|
36
|
+
}
|
|
37
|
+
return refreshed;
|
|
38
|
+
}
|
|
39
|
+
export function buildExitTranscriptEntry(opts) {
|
|
40
|
+
return {
|
|
41
|
+
ts: opts.ts,
|
|
42
|
+
id: opts.id,
|
|
43
|
+
kind: EXIT_TRANSCRIPT_KIND,
|
|
44
|
+
status: opts.status,
|
|
45
|
+
profile: opts.profile,
|
|
46
|
+
capturedAt: opts.tail.capturedAt,
|
|
47
|
+
lines: opts.tail.lines,
|
|
48
|
+
truncated: opts.tail.truncated,
|
|
49
|
+
tail: opts.tail.text,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function flushExitTranscripts(opts) {
|
|
53
|
+
const now = opts.now ?? Date.now;
|
|
54
|
+
const append = opts.append ?? ((entry) => appendActivityLog(entry));
|
|
55
|
+
const written = [];
|
|
56
|
+
for (const [id, tail] of [...opts.tails.entries()]) {
|
|
57
|
+
if (opts.tracked.has(id))
|
|
58
|
+
continue;
|
|
59
|
+
const status = opts.statusFor(id);
|
|
60
|
+
const entry = buildExitTranscriptEntry({ id, tail, status, profile: opts.profile, ts: now() });
|
|
61
|
+
try {
|
|
62
|
+
await append(entry);
|
|
63
|
+
written.push(id);
|
|
64
|
+
opts.log?.(`· ${id} · exit transcript saved (${entry.lines} lines${entry.truncated ? ', clamped' : ''} · ${status}) → activations.log\n`);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
opts.log?.(`⚠ ${id} · exit transcript not saved (activations.log write failed)\n`);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
opts.tails.delete(id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return written;
|
|
74
|
+
}
|
package/dist/lib/machine.js
CHANGED
|
@@ -13,6 +13,13 @@ export function machineFilePath(dir = configDir()) {
|
|
|
13
13
|
return path.join(dir, 'machine.json');
|
|
14
14
|
}
|
|
15
15
|
export function loadOrCreateMachineId(deps = {}) {
|
|
16
|
+
const env = deps.env ?? process.env;
|
|
17
|
+
const fromEnv = env.NONBOT_MACHINE_ID;
|
|
18
|
+
if (typeof fromEnv === 'string') {
|
|
19
|
+
const trimmed = fromEnv.trim();
|
|
20
|
+
if (trimmed.length > 0)
|
|
21
|
+
return trimmed.slice(0, 128);
|
|
22
|
+
}
|
|
16
23
|
const fs = deps.fs ?? nodeFs;
|
|
17
24
|
const uuid = deps.uuid ?? randomUUID;
|
|
18
25
|
const dir = deps.dir ?? configDir();
|
|
@@ -14,6 +14,8 @@ const STORY_TITLE_MAX = 200;
|
|
|
14
14
|
const AGENTS_MD_MAX = 24 * 1024;
|
|
15
15
|
const REPO_PATH_MAX = 1024;
|
|
16
16
|
const AGENTS_MD_HEREDOC_DELIMITER = 'NONBOT_AGENTS_EOF';
|
|
17
|
+
const REPO_URL_MAX = 512;
|
|
18
|
+
const REPO_REF_RE = /^[A-Za-z0-9._/-]{1,128}$/;
|
|
17
19
|
const THEME_ID_MAX = 128;
|
|
18
20
|
const THEME_NAME_MAX = 128;
|
|
19
21
|
const THEME_TMUX_CONF_MAX = 8 * 1024;
|
|
@@ -50,6 +52,55 @@ export function validateRepoPath(s, opts = {}) {
|
|
|
50
52
|
}
|
|
51
53
|
return s;
|
|
52
54
|
}
|
|
55
|
+
export function validateRepoUrl(s) {
|
|
56
|
+
if (s === undefined || s === null || s === '')
|
|
57
|
+
return undefined;
|
|
58
|
+
if (typeof s !== 'string') {
|
|
59
|
+
throw new ValidationError('repoUrl', 'must be a string');
|
|
60
|
+
}
|
|
61
|
+
if (s.length > REPO_URL_MAX) {
|
|
62
|
+
throw new ValidationError('repoUrl', `exceeds ${REPO_URL_MAX} chars`);
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = new URL(s);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new ValidationError('repoUrl', 'must be a parseable absolute URL');
|
|
70
|
+
}
|
|
71
|
+
if (parsed.protocol !== 'https:') {
|
|
72
|
+
throw new ValidationError('repoUrl', 'must use https');
|
|
73
|
+
}
|
|
74
|
+
if (parsed.username || parsed.password) {
|
|
75
|
+
throw new ValidationError('repoUrl', 'must not embed credentials');
|
|
76
|
+
}
|
|
77
|
+
if (/[\s'"`;|&<>\\\n\0$]/.test(s)) {
|
|
78
|
+
throw new ValidationError('repoUrl', 'contains shell metacharacters');
|
|
79
|
+
}
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
export function repoUrlLooksSafe(url) {
|
|
83
|
+
if (typeof url !== 'string' || url.length === 0)
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
validateRepoUrl(url);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function validateRepoRef(s) {
|
|
94
|
+
if (s === undefined || s === null || s === '')
|
|
95
|
+
return undefined;
|
|
96
|
+
if (typeof s !== 'string') {
|
|
97
|
+
throw new ValidationError('repoRef', 'must be a string');
|
|
98
|
+
}
|
|
99
|
+
if (!REPO_REF_RE.test(s)) {
|
|
100
|
+
throw new ValidationError('repoRef', `must match /^[A-Za-z0-9._/-]{1,128}$/ (got ${JSON.stringify(s.slice(0, 64))})`);
|
|
101
|
+
}
|
|
102
|
+
return s;
|
|
103
|
+
}
|
|
53
104
|
export function validateStoryTitle(s) {
|
|
54
105
|
if (s === undefined || s === null)
|
|
55
106
|
return undefined;
|
|
@@ -96,7 +147,22 @@ const TMUX_SAFE_DIRECTIVES = new Set([
|
|
|
96
147
|
]);
|
|
97
148
|
const TMUX_FORBIDDEN_OPTIONS = new Set([
|
|
98
149
|
'default-command', 'default-shell', 'command-alias',
|
|
150
|
+
'lock-command', 'copy-command', 'editor',
|
|
151
|
+
]);
|
|
152
|
+
const TMUX_SAFE_OPTION_SUFFIXES = [
|
|
153
|
+
'-style', '-format', '-bg', '-fg', '-attr', '-colour', '-color', '-length',
|
|
154
|
+
];
|
|
155
|
+
const TMUX_SAFE_OPTION_NAMES = new Set([
|
|
156
|
+
'status', 'status-position', 'status-justify', 'status-interval',
|
|
157
|
+
'window-status-separator', 'pane-border-status', 'pane-border-indicators',
|
|
99
158
|
]);
|
|
159
|
+
function isSafeTmuxOptionName(name) {
|
|
160
|
+
if (TMUX_FORBIDDEN_OPTIONS.has(name))
|
|
161
|
+
return false;
|
|
162
|
+
if (TMUX_SAFE_OPTION_NAMES.has(name))
|
|
163
|
+
return true;
|
|
164
|
+
return TMUX_SAFE_OPTION_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
165
|
+
}
|
|
100
166
|
export function isSafeTmuxConf(conf) {
|
|
101
167
|
for (const rawLine of conf.split(/\r?\n/)) {
|
|
102
168
|
const line = rawLine.trim();
|
|
@@ -119,6 +185,9 @@ export function isSafeTmuxConf(conf) {
|
|
|
119
185
|
if (TMUX_FORBIDDEN_OPTIONS.has(tok))
|
|
120
186
|
return false;
|
|
121
187
|
}
|
|
188
|
+
const optionName = tokens.slice(1).find((t) => !t.startsWith('-'));
|
|
189
|
+
if (!optionName || !isSafeTmuxOptionName(optionName))
|
|
190
|
+
return false;
|
|
122
191
|
}
|
|
123
192
|
}
|
|
124
193
|
return true;
|
package/dist/lib/run-prompt.js
CHANGED
|
@@ -112,6 +112,24 @@ export async function reportPrompt(args) {
|
|
|
112
112
|
return false;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
export function normalizeAnsweredPrompts(raw) {
|
|
116
|
+
if (!Array.isArray(raw))
|
|
117
|
+
return [];
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const r of raw.slice(0, ANSWERED_MAX)) {
|
|
120
|
+
if (!r || typeof r.promptId !== 'string' || typeof r.activationId !== 'string')
|
|
121
|
+
continue;
|
|
122
|
+
out.push({
|
|
123
|
+
promptId: r.promptId,
|
|
124
|
+
activationId: r.activationId,
|
|
125
|
+
paneId: typeof r.paneId === 'string' && r.paneId.length > 0 ? r.paneId : null,
|
|
126
|
+
answerIndex: typeof r.answerIndex === 'number' ? r.answerIndex : null,
|
|
127
|
+
answerText: typeof r.answerText === 'string' ? r.answerText.slice(0, ANSWER_TEXT_MAX) : null,
|
|
128
|
+
answeredAt: typeof r.answeredAt === 'number' ? r.answeredAt : undefined,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
115
133
|
export async function pollAnsweredPrompts(args) {
|
|
116
134
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
117
135
|
try {
|
|
@@ -126,22 +144,9 @@ export async function pollAnsweredPrompts(args) {
|
|
|
126
144
|
if (!res.ok)
|
|
127
145
|
return [];
|
|
128
146
|
const data = (await res.json());
|
|
129
|
-
if (!data
|
|
147
|
+
if (!data)
|
|
130
148
|
return [];
|
|
131
|
-
|
|
132
|
-
for (const r of data.prompts.slice(0, ANSWERED_MAX)) {
|
|
133
|
-
if (!r || typeof r.promptId !== 'string' || typeof r.activationId !== 'string')
|
|
134
|
-
continue;
|
|
135
|
-
out.push({
|
|
136
|
-
promptId: r.promptId,
|
|
137
|
-
activationId: r.activationId,
|
|
138
|
-
paneId: typeof r.paneId === 'string' && r.paneId.length > 0 ? r.paneId : null,
|
|
139
|
-
answerIndex: typeof r.answerIndex === 'number' ? r.answerIndex : null,
|
|
140
|
-
answerText: typeof r.answerText === 'string' ? r.answerText.slice(0, ANSWER_TEXT_MAX) : null,
|
|
141
|
-
answeredAt: typeof r.answeredAt === 'number' ? r.answeredAt : undefined,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
return out;
|
|
149
|
+
return normalizeAnsweredPrompts(data.prompts);
|
|
145
150
|
}
|
|
146
151
|
catch {
|
|
147
152
|
return [];
|
|
@@ -166,12 +171,28 @@ export async function confirmDelivered(args) {
|
|
|
166
171
|
return false;
|
|
167
172
|
}
|
|
168
173
|
}
|
|
174
|
+
const ANSI_CSI_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
175
|
+
const ANSI_CSI8_RE = /\x9B[0-?]*[ -/]*[@-~]/g;
|
|
176
|
+
const ANSI_OSC_RE = /\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)?/g;
|
|
177
|
+
const ANSI_ESC2_RE = /\x1B[@-~]/g;
|
|
178
|
+
const CONTROL_BYTE_RE = /[\x00-\x1F\x7F-\x9F]/g;
|
|
179
|
+
export function sanitizeAnswerText(text) {
|
|
180
|
+
if (typeof text !== 'string')
|
|
181
|
+
return '';
|
|
182
|
+
return text
|
|
183
|
+
.replace(ANSI_CSI_RE, '')
|
|
184
|
+
.replace(ANSI_CSI8_RE, '')
|
|
185
|
+
.replace(ANSI_OSC_RE, '')
|
|
186
|
+
.replace(ANSI_ESC2_RE, '')
|
|
187
|
+
.replace(CONTROL_BYTE_RE, ' ')
|
|
188
|
+
.trim();
|
|
189
|
+
}
|
|
169
190
|
export function injectAnswer(paneId, answerIndex, answerText, spawnImpl = nodeSpawnSync) {
|
|
170
191
|
if (!PANE_ID_RE.test(paneId))
|
|
171
192
|
return false;
|
|
172
193
|
const keys = answerIndex !== null && Number.isFinite(answerIndex) && answerIndex >= 0
|
|
173
194
|
? String(answerIndex)
|
|
174
|
-
: answerText ?? '';
|
|
195
|
+
: sanitizeAnswerText(answerText ?? '');
|
|
175
196
|
if (keys.length === 0)
|
|
176
197
|
return false;
|
|
177
198
|
try {
|
|
@@ -192,7 +213,7 @@ export function injectAnswer(paneId, answerIndex, answerText, spawnImpl = nodeSp
|
|
|
192
213
|
}
|
|
193
214
|
}
|
|
194
215
|
export async function deliverAnsweredPrompts(opts) {
|
|
195
|
-
const answered = await pollAnsweredPrompts({
|
|
216
|
+
const answered = opts.prompts ?? await pollAnsweredPrompts({
|
|
196
217
|
baseUrl: opts.baseUrl,
|
|
197
218
|
pat: opts.pat,
|
|
198
219
|
machineId: opts.machineId,
|
package/dist/lib/snapshot.js
CHANGED
|
@@ -27,22 +27,24 @@ export function captureSnapshotPane(paneId, maxLines = MAX_LINES_DEFAULT, spawnI
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
const PAT_TOKEN_RE = /pat_[A-Za-z0-9_-]{8,}/g;
|
|
30
|
-
const
|
|
30
|
+
const ANTHROPIC_KEY_RE = /sk-ant-[A-Za-z0-9_-]{8,}/g;
|
|
31
|
+
const GITHUB_TOKEN_RE = /(?:gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,})/g;
|
|
32
|
+
const ENV_ASSIGN_LINE_RE = /(?:^|\s|export )\w*(?:NONBOT_|TOKEN|API_KEY|SECRET)\w*=/;
|
|
31
33
|
const AUTH_BEARER_RE = /Authorization: Bearer \S+/g;
|
|
34
|
+
export function scrubLine(line) {
|
|
35
|
+
if (ENV_ASSIGN_LINE_RE.test(line) || line.includes('NONBOT_PAT')) {
|
|
36
|
+
return '[redacted env line]';
|
|
37
|
+
}
|
|
38
|
+
return line
|
|
39
|
+
.replace(GITHUB_TOKEN_RE, '[REDACTED-GIT-TOKEN]')
|
|
40
|
+
.replace(PAT_TOKEN_RE, 'pat_[REDACTED]')
|
|
41
|
+
.replace(ANTHROPIC_KEY_RE, 'sk-ant-[REDACTED]')
|
|
42
|
+
.replace(AUTH_BEARER_RE, 'Authorization: Bearer [REDACTED]');
|
|
43
|
+
}
|
|
32
44
|
export function scrubSnapshot(text) {
|
|
33
45
|
if (typeof text !== 'string' || text.length === 0)
|
|
34
46
|
return '';
|
|
35
|
-
return text
|
|
36
|
-
.split('\n')
|
|
37
|
-
.map((line) => {
|
|
38
|
-
if (ENV_EXPORT_LINE_RE.test(line) || line.includes('NONBOT_PAT')) {
|
|
39
|
-
return '[redacted env line]';
|
|
40
|
-
}
|
|
41
|
-
return line
|
|
42
|
-
.replace(PAT_TOKEN_RE, 'pat_[REDACTED]')
|
|
43
|
-
.replace(AUTH_BEARER_RE, 'Authorization: Bearer [REDACTED]');
|
|
44
|
-
})
|
|
45
|
-
.join('\n');
|
|
47
|
+
return text.split('\n').map(scrubLine).join('\n');
|
|
46
48
|
}
|
|
47
49
|
export function clampSnapshotTail(text, maxBytes = SNAPSHOT_MAX_BYTES) {
|
|
48
50
|
const buf = Buffer.from(text, 'utf-8');
|
package/dist/lib/terminal.js
CHANGED
|
@@ -23,6 +23,10 @@ function toOsascriptArgs(lines) {
|
|
|
23
23
|
export const TMUX_PRESS_KEY_TRAILER = "; printf '\\n%s\\n' 'Run finished — press enter to close'" +
|
|
24
24
|
'; read _' +
|
|
25
25
|
'; tmux kill-pane';
|
|
26
|
+
export function tmuxCloudTrailer(exitFile) {
|
|
27
|
+
const safe = exitFile.replace(/'/g, `'\\''`);
|
|
28
|
+
return `; echo $? > '${safe}'`;
|
|
29
|
+
}
|
|
26
30
|
export const TERMINAL_PROFILES = [
|
|
27
31
|
{
|
|
28
32
|
id: 'terminal',
|
|
@@ -123,8 +127,9 @@ export const TERMINAL_PROFILES = [
|
|
|
123
127
|
id: 'tmux',
|
|
124
128
|
displayName: 'tmux pane',
|
|
125
129
|
platform: 'darwin',
|
|
126
|
-
launch: (s) => {
|
|
130
|
+
launch: (s, opts) => {
|
|
127
131
|
const safe = s.replace(/'/g, `'\\''`);
|
|
132
|
+
const trailer = opts?.cloudExitFile ? tmuxCloudTrailer(opts.cloudExitFile) : TMUX_PRESS_KEY_TRAILER;
|
|
128
133
|
return {
|
|
129
134
|
cmd: 'tmux',
|
|
130
135
|
args: [
|
|
@@ -132,7 +137,7 @@ export const TERMINAL_PROFILES = [
|
|
|
132
137
|
'-P',
|
|
133
138
|
'-F',
|
|
134
139
|
'#{pane_id}',
|
|
135
|
-
`bash '${safe}' 2>&1${
|
|
140
|
+
`bash '${safe}' 2>&1${trailer}`,
|
|
136
141
|
';',
|
|
137
142
|
'select-layout',
|
|
138
143
|
'tiled',
|
|
@@ -186,8 +191,9 @@ export const TERMINAL_PROFILES = [
|
|
|
186
191
|
id: 'tmux',
|
|
187
192
|
displayName: 'tmux pane',
|
|
188
193
|
platform: 'linux',
|
|
189
|
-
launch: (s) => {
|
|
194
|
+
launch: (s, opts) => {
|
|
190
195
|
const safe = s.replace(/'/g, `'\\''`);
|
|
196
|
+
const trailer = opts?.cloudExitFile ? tmuxCloudTrailer(opts.cloudExitFile) : TMUX_PRESS_KEY_TRAILER;
|
|
191
197
|
return {
|
|
192
198
|
cmd: 'tmux',
|
|
193
199
|
args: [
|
|
@@ -195,7 +201,7 @@ export const TERMINAL_PROFILES = [
|
|
|
195
201
|
'-P',
|
|
196
202
|
'-F',
|
|
197
203
|
'#{pane_id}',
|
|
198
|
-
`bash '${safe}' 2>&1${
|
|
204
|
+
`bash '${safe}' 2>&1${trailer}`,
|
|
199
205
|
';',
|
|
200
206
|
'select-layout',
|
|
201
207
|
'tiled',
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { VERSION } from '../version.js';
|
|
2
|
+
export const NPM_LATEST_URL = 'https://registry.npmjs.org/@nonbot/cli/latest';
|
|
3
|
+
export const UPDATE_CHECK_TIMEOUT_MS = 3_000;
|
|
4
|
+
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60_000;
|
|
5
|
+
export const UPDATE_CHECK_OPT_OUT_ENV = 'NONBOT_NO_UPDATE_CHECK';
|
|
6
|
+
export function updateCheckDisabled(env = process.env) {
|
|
7
|
+
const v = env[UPDATE_CHECK_OPT_OUT_ENV];
|
|
8
|
+
if (typeof v !== 'string')
|
|
9
|
+
return false;
|
|
10
|
+
const t = v.trim().toLowerCase();
|
|
11
|
+
return t === '1' || t === 'true';
|
|
12
|
+
}
|
|
13
|
+
export function parseSemver(v) {
|
|
14
|
+
if (typeof v !== 'string')
|
|
15
|
+
return null;
|
|
16
|
+
const m = /^v?(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:[-+][0-9A-Za-z.-]{0,64})?$/.exec(v.trim());
|
|
17
|
+
if (!m)
|
|
18
|
+
return null;
|
|
19
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
20
|
+
}
|
|
21
|
+
export function compareVersions(a, b) {
|
|
22
|
+
const pa = parseSemver(a);
|
|
23
|
+
const pb = parseSemver(b);
|
|
24
|
+
if (!pa || !pb)
|
|
25
|
+
return 0;
|
|
26
|
+
for (let i = 0; i < 3; i++) {
|
|
27
|
+
if (pa[i] > pb[i])
|
|
28
|
+
return 1;
|
|
29
|
+
if (pa[i] < pb[i])
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
export function isNewerVersion(latest, current) {
|
|
35
|
+
return compareVersions(latest, current) > 0;
|
|
36
|
+
}
|
|
37
|
+
export function updateAvailableLine(current, latest) {
|
|
38
|
+
return `Update available: @nonbot/cli ${current} → ${latest} (npm i -g @nonbot/cli)`;
|
|
39
|
+
}
|
|
40
|
+
export async function fetchLatestVersion(opts = {}) {
|
|
41
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetchImpl(opts.url ?? NPM_LATEST_URL, {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
headers: { Accept: 'application/json' },
|
|
46
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS),
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
return null;
|
|
50
|
+
const body = (await res.json());
|
|
51
|
+
const version = body?.version;
|
|
52
|
+
return parseSemver(version) ? String(version).trim() : null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function createUpdateChecker(opts) {
|
|
59
|
+
const current = opts.current ?? VERSION;
|
|
60
|
+
const env = opts.env ?? process.env;
|
|
61
|
+
const now = opts.now ?? Date.now;
|
|
62
|
+
const intervalMs = opts.intervalMs ?? UPDATE_CHECK_INTERVAL_MS;
|
|
63
|
+
const fetchImpl = opts.fetchImpl === undefined ? fetch : opts.fetchImpl;
|
|
64
|
+
let lastCheckedAt = null;
|
|
65
|
+
let latestSeen = null;
|
|
66
|
+
return {
|
|
67
|
+
get lastCheckedAt() {
|
|
68
|
+
return lastCheckedAt;
|
|
69
|
+
},
|
|
70
|
+
get latestSeen() {
|
|
71
|
+
return latestSeen;
|
|
72
|
+
},
|
|
73
|
+
async maybeCheck() {
|
|
74
|
+
if (fetchImpl === null || updateCheckDisabled(env))
|
|
75
|
+
return false;
|
|
76
|
+
const t = now();
|
|
77
|
+
if (lastCheckedAt !== null && t - lastCheckedAt < intervalMs)
|
|
78
|
+
return false;
|
|
79
|
+
lastCheckedAt = t;
|
|
80
|
+
try {
|
|
81
|
+
const latest = await fetchLatestVersion({
|
|
82
|
+
fetchImpl,
|
|
83
|
+
timeoutMs: opts.timeoutMs,
|
|
84
|
+
url: opts.url,
|
|
85
|
+
});
|
|
86
|
+
if (!latest)
|
|
87
|
+
return false;
|
|
88
|
+
latestSeen = latest;
|
|
89
|
+
if (!isNewerVersion(latest, current))
|
|
90
|
+
return false;
|
|
91
|
+
opts.log(updateAvailableLine(current, latest));
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.10.1';
|
package/package.json
CHANGED