autokap 1.7.1 → 1.7.3
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/dist/cli-runner.js +30 -0
- package/dist/cli-updater.d.ts +1 -0
- package/dist/cli-updater.js +273 -0
- package/dist/cli.js +7 -0
- package/package.json +1 -1
package/dist/cli-runner.js
CHANGED
|
@@ -223,6 +223,13 @@ export async function runCapture(options) {
|
|
|
223
223
|
// AUT-149: collect structured logs + progress events for export on failure.
|
|
224
224
|
const logCollector = new LogCollector();
|
|
225
225
|
logCollector.start();
|
|
226
|
+
// Register this local run server-side so the dashboard shows an "En cours"
|
|
227
|
+
// badge on the preset while it captures. Best-effort + local-only: skip dry
|
|
228
|
+
// runs and cloud runs (AUTOKAP_RUN_ID is set on cloud runners, which own
|
|
229
|
+
// their own capture_runs row). A failure here must never block the capture.
|
|
230
|
+
if (!options.dryRun && !process.env.AUTOKAP_RUN_ID) {
|
|
231
|
+
await postRunStart(config, runId, program.presetId, program.variants.length, options.env);
|
|
232
|
+
}
|
|
226
233
|
const runOptions = {
|
|
227
234
|
recoveryChain,
|
|
228
235
|
abortSignal: options.abortSignal,
|
|
@@ -664,6 +671,29 @@ function getProgramFetchRetryDelayMs(attempt) {
|
|
|
664
671
|
function sleep(ms) {
|
|
665
672
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
666
673
|
}
|
|
674
|
+
// Best-effort registration of a local run at its start, so the dashboard can
|
|
675
|
+
// surface an "En cours" badge on the preset while the capture is in flight.
|
|
676
|
+
// Never throws — a registration failure must not abort the capture.
|
|
677
|
+
async function postRunStart(config, runId, presetId, variantCount, env) {
|
|
678
|
+
try {
|
|
679
|
+
const response = await fetch(`${config.apiBaseUrl}/api/cli/runs`, {
|
|
680
|
+
method: 'POST',
|
|
681
|
+
headers: {
|
|
682
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
683
|
+
'Content-Type': 'application/json',
|
|
684
|
+
},
|
|
685
|
+
body: JSON.stringify({ runId, presetId, variantCount, env }),
|
|
686
|
+
signal: AbortSignal.timeout(10_000),
|
|
687
|
+
});
|
|
688
|
+
if (!response.ok) {
|
|
689
|
+
logger.warn(`[capture] Run registration failed (HTTP ${response.status}); the dashboard "in progress" badge may not appear`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
catch (err) {
|
|
693
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
694
|
+
logger.warn(`[capture] Run registration error: ${message}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
667
697
|
async function uploadResults(config, program, result, runId = randomUUID()) {
|
|
668
698
|
const artifactJobs = result.variantResults.flatMap((variant) => {
|
|
669
699
|
const variantSpec = program.variants.find((entry) => entry.id === variant.variantId);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function maybePromptUpdate(currentVersion: string): Promise<void>;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Self-update prompt for the bundled `autokap` CLI.
|
|
2
|
+
//
|
|
3
|
+
// Runs once per command, before Commander parses argv. Best-effort and
|
|
4
|
+
// fail-open: any error, network timeout, or unexpected state silently skips
|
|
5
|
+
// the prompt — the user's command must never be blocked by this check.
|
|
6
|
+
//
|
|
7
|
+
// Behavior summary:
|
|
8
|
+
// - Skipped entirely when stdin/stdout is not a TTY, CI=1 is set, the current
|
|
9
|
+
// version is a pre-release, the CLI was invoked via `npx` cache, or argv
|
|
10
|
+
// contains only --version/--help (read-only commands).
|
|
11
|
+
// - Fetches the latest version from the npm registry with a 2s hard timeout.
|
|
12
|
+
// - Caches the result for 24h in ~/.autokap/last-update-check.json to avoid
|
|
13
|
+
// spamming the registry. If the cache is fresh and reports a newer version,
|
|
14
|
+
// re-prompts without re-fetching.
|
|
15
|
+
// - On user acceptance, runs `npm install -g autokap@<version>` then respawns
|
|
16
|
+
// `autokap` with the original argv so the user's intended command runs
|
|
17
|
+
// against the new binary.
|
|
18
|
+
import { spawn } from 'node:child_process';
|
|
19
|
+
import fs from 'node:fs/promises';
|
|
20
|
+
import https from 'node:https';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import readline from 'node:readline';
|
|
23
|
+
import { URL } from 'node:url';
|
|
24
|
+
import chalk from 'chalk';
|
|
25
|
+
import { logger } from './logger.js';
|
|
26
|
+
import { getConfigDir } from './cli-config.js';
|
|
27
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/autokap/latest';
|
|
28
|
+
const CACHE_FILE_NAME = 'last-update-check.json';
|
|
29
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
30
|
+
const FETCH_TIMEOUT_MS = 2000;
|
|
31
|
+
const DISABLE_ENV_VAR = 'AUTOKAP_DISABLE_UPDATE_CHECK';
|
|
32
|
+
function isInteractive() {
|
|
33
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
|
|
34
|
+
}
|
|
35
|
+
function isDisabledByEnv() {
|
|
36
|
+
const raw = process.env[DISABLE_ENV_VAR]?.trim().toLowerCase();
|
|
37
|
+
return raw === '1' || raw === 'true';
|
|
38
|
+
}
|
|
39
|
+
function isPrerelease(version) {
|
|
40
|
+
return version.includes('-');
|
|
41
|
+
}
|
|
42
|
+
function isNpxExecution() {
|
|
43
|
+
const here = import.meta.url;
|
|
44
|
+
return here.includes('/_npx/') || here.includes('\\_npx\\');
|
|
45
|
+
}
|
|
46
|
+
function isReadOnlyOrEmptyCommand() {
|
|
47
|
+
const argv = process.argv.slice(2);
|
|
48
|
+
if (argv.length === 0)
|
|
49
|
+
return true;
|
|
50
|
+
return argv.some(a => a === '--version' || a === '-V' || a === '--help' || a === '-h');
|
|
51
|
+
}
|
|
52
|
+
function cachePath() {
|
|
53
|
+
return path.join(getConfigDir(), CACHE_FILE_NAME);
|
|
54
|
+
}
|
|
55
|
+
async function readUpdateCheckCache() {
|
|
56
|
+
try {
|
|
57
|
+
const raw = await fs.readFile(cachePath(), 'utf-8');
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (typeof parsed === 'object' &&
|
|
60
|
+
parsed !== null &&
|
|
61
|
+
typeof parsed.checkedAt === 'string' &&
|
|
62
|
+
typeof parsed.latestVersion === 'string') {
|
|
63
|
+
const obj = parsed;
|
|
64
|
+
return { checkedAt: obj.checkedAt, latestVersion: obj.latestVersion };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function writeUpdateCheckCache(latestVersion) {
|
|
73
|
+
try {
|
|
74
|
+
await fs.mkdir(getConfigDir(), { recursive: true });
|
|
75
|
+
const data = {
|
|
76
|
+
checkedAt: new Date().toISOString(),
|
|
77
|
+
latestVersion,
|
|
78
|
+
};
|
|
79
|
+
await fs.writeFile(cachePath(), JSON.stringify(data, null, 2), 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// best-effort
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isCacheFresh(cache) {
|
|
86
|
+
const checkedAt = Date.parse(cache.checkedAt);
|
|
87
|
+
if (Number.isNaN(checkedAt))
|
|
88
|
+
return false;
|
|
89
|
+
return Date.now() - checkedAt < CACHE_TTL_MS;
|
|
90
|
+
}
|
|
91
|
+
function fetchLatestVersion(timeoutMs, currentVersion) {
|
|
92
|
+
return new Promise(resolve => {
|
|
93
|
+
let settled = false;
|
|
94
|
+
const settle = (v) => {
|
|
95
|
+
if (settled)
|
|
96
|
+
return;
|
|
97
|
+
settled = true;
|
|
98
|
+
resolve(v);
|
|
99
|
+
};
|
|
100
|
+
let req;
|
|
101
|
+
try {
|
|
102
|
+
const url = new URL(REGISTRY_URL);
|
|
103
|
+
req = https.get({
|
|
104
|
+
hostname: url.hostname,
|
|
105
|
+
path: url.pathname + url.search,
|
|
106
|
+
port: url.port ? Number(url.port) : 443,
|
|
107
|
+
headers: {
|
|
108
|
+
Accept: 'application/json',
|
|
109
|
+
'User-Agent': `autokap-cli/${currentVersion}`,
|
|
110
|
+
},
|
|
111
|
+
}, res => {
|
|
112
|
+
if (res.statusCode !== 200) {
|
|
113
|
+
res.resume();
|
|
114
|
+
settle(null);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const chunks = [];
|
|
118
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
119
|
+
res.on('end', () => {
|
|
120
|
+
try {
|
|
121
|
+
const json = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
|
|
122
|
+
const version = json && typeof json === 'object' && typeof json.version === 'string'
|
|
123
|
+
? json.version
|
|
124
|
+
: null;
|
|
125
|
+
settle(version);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
settle(null);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
res.on('error', () => settle(null));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
settle(null);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
req.on('error', () => settle(null));
|
|
139
|
+
req.setTimeout(timeoutMs, () => {
|
|
140
|
+
req.destroy();
|
|
141
|
+
settle(null);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function parseSemverCore(version) {
|
|
146
|
+
const stripped = version.replace(/^v/, '').split(/[-+]/)[0];
|
|
147
|
+
const parts = stripped.split('.');
|
|
148
|
+
if (parts.length !== 3)
|
|
149
|
+
return null;
|
|
150
|
+
const nums = parts.map(p => parseInt(p, 10));
|
|
151
|
+
if (nums.some(n => Number.isNaN(n) || n < 0))
|
|
152
|
+
return null;
|
|
153
|
+
return [nums[0], nums[1], nums[2]];
|
|
154
|
+
}
|
|
155
|
+
function compareSemver(a, b) {
|
|
156
|
+
const pa = parseSemverCore(a);
|
|
157
|
+
const pb = parseSemverCore(b);
|
|
158
|
+
if (!pa || !pb)
|
|
159
|
+
return 0;
|
|
160
|
+
for (let i = 0; i < 3; i++) {
|
|
161
|
+
if (pa[i] > pb[i])
|
|
162
|
+
return 1;
|
|
163
|
+
if (pa[i] < pb[i])
|
|
164
|
+
return -1;
|
|
165
|
+
}
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
function promptYesNo(question, defaultValue) {
|
|
169
|
+
return new Promise(resolve => {
|
|
170
|
+
let resolved = false;
|
|
171
|
+
const safeResolve = (v) => {
|
|
172
|
+
if (resolved)
|
|
173
|
+
return;
|
|
174
|
+
resolved = true;
|
|
175
|
+
resolve(v);
|
|
176
|
+
};
|
|
177
|
+
const rl = readline.createInterface({
|
|
178
|
+
input: process.stdin,
|
|
179
|
+
output: process.stdout,
|
|
180
|
+
});
|
|
181
|
+
const suffix = defaultValue ? ' (Y/n) ' : ' (y/N) ';
|
|
182
|
+
rl.question(question + suffix, answer => {
|
|
183
|
+
rl.close();
|
|
184
|
+
const normalized = answer.trim().toLowerCase();
|
|
185
|
+
if (normalized === '') {
|
|
186
|
+
safeResolve(defaultValue);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (['y', 'yes', 'o', 'oui'].includes(normalized)) {
|
|
190
|
+
safeResolve(true);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (['n', 'no', 'non'].includes(normalized)) {
|
|
194
|
+
safeResolve(false);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
safeResolve(defaultValue);
|
|
198
|
+
});
|
|
199
|
+
rl.on('close', () => safeResolve(defaultValue));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function runNpmInstall(version) {
|
|
203
|
+
return new Promise(resolve => {
|
|
204
|
+
const child = spawn('npm', ['install', '-g', `autokap@${version}`], {
|
|
205
|
+
stdio: 'inherit',
|
|
206
|
+
shell: process.platform === 'win32',
|
|
207
|
+
});
|
|
208
|
+
child.on('error', () => resolve(false));
|
|
209
|
+
child.on('exit', code => resolve(code === 0));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function respawnCli() {
|
|
213
|
+
return new Promise(() => {
|
|
214
|
+
const args = process.argv.slice(2);
|
|
215
|
+
const child = spawn('autokap', args, {
|
|
216
|
+
stdio: 'inherit',
|
|
217
|
+
shell: process.platform === 'win32',
|
|
218
|
+
});
|
|
219
|
+
child.on('exit', code => {
|
|
220
|
+
process.exit(code ?? 0);
|
|
221
|
+
});
|
|
222
|
+
child.on('error', () => {
|
|
223
|
+
process.exit(0);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
export async function maybePromptUpdate(currentVersion) {
|
|
228
|
+
if (isDisabledByEnv())
|
|
229
|
+
return;
|
|
230
|
+
if (!isInteractive())
|
|
231
|
+
return;
|
|
232
|
+
if (isPrerelease(currentVersion))
|
|
233
|
+
return;
|
|
234
|
+
if (isNpxExecution())
|
|
235
|
+
return;
|
|
236
|
+
if (isReadOnlyOrEmptyCommand())
|
|
237
|
+
return;
|
|
238
|
+
let latestVersion = null;
|
|
239
|
+
const cache = await readUpdateCheckCache();
|
|
240
|
+
if (cache && isCacheFresh(cache)) {
|
|
241
|
+
if (compareSemver(cache.latestVersion, currentVersion) <= 0) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
latestVersion = cache.latestVersion;
|
|
245
|
+
}
|
|
246
|
+
if (!latestVersion) {
|
|
247
|
+
latestVersion = await fetchLatestVersion(FETCH_TIMEOUT_MS, currentVersion);
|
|
248
|
+
if (!latestVersion)
|
|
249
|
+
return;
|
|
250
|
+
await writeUpdateCheckCache(latestVersion);
|
|
251
|
+
}
|
|
252
|
+
if (compareSemver(latestVersion, currentVersion) <= 0)
|
|
253
|
+
return;
|
|
254
|
+
console.log('');
|
|
255
|
+
logger.info(`A new version of autokap is available: ${chalk.gray(currentVersion)} → ${chalk.cyan(latestVersion)}`);
|
|
256
|
+
const accepted = await promptYesNo(`Update to ${latestVersion}?`, true);
|
|
257
|
+
if (!accepted) {
|
|
258
|
+
console.log('');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
console.log('');
|
|
262
|
+
logger.info(`Running npm install -g autokap@${latestVersion}…`);
|
|
263
|
+
const installed = await runNpmInstall(latestVersion);
|
|
264
|
+
if (!installed) {
|
|
265
|
+
logger.error(`Update failed. Run manually: npm install -g autokap@${latestVersion}`);
|
|
266
|
+
console.log('');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
logger.success(`Updated to ${latestVersion}. Re-running your command…`);
|
|
270
|
+
console.log('');
|
|
271
|
+
await respawnCli();
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=cli-updater.js.map
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ const require = createRequire(import.meta.url);
|
|
|
7
7
|
const { version } = require('../package.json');
|
|
8
8
|
import { logger } from './logger.js';
|
|
9
9
|
import { writeConfig, requireConfig, getConfigPath, getDefaultApiBaseUrl, getDefaultWsUrl, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, } from './cli-config.js';
|
|
10
|
+
import { maybePromptUpdate } from './cli-updater.js';
|
|
10
11
|
// ── Program definition ──────────────────────────────────────────────
|
|
11
12
|
export const program = new Command();
|
|
12
13
|
program
|
|
@@ -523,6 +524,12 @@ const isDirectExecution = resolvedArgv && await resolvedArgv.then(p => {
|
|
|
523
524
|
return base === 'cli.js' || base === 'cli.ts';
|
|
524
525
|
});
|
|
525
526
|
if (isDirectExecution) {
|
|
527
|
+
try {
|
|
528
|
+
await maybePromptUpdate(version);
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// fail-open: a crash in the updater must never block the user's command.
|
|
532
|
+
}
|
|
526
533
|
program.parseAsync().catch(async (err) => {
|
|
527
534
|
logger.error(err.message);
|
|
528
535
|
process.exit(1);
|