@agentage/cli 0.38.0 → 0.38.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/dist/cli.js +5 -3
- package/dist/commands/auth/setup.d.ts +6 -14
- package/dist/commands/auth/setup.js +9 -121
- package/dist/commands/status/status.d.ts +4 -3
- package/dist/commands/status/status.js +3 -2
- package/dist/commands/update/update.d.ts +3 -1
- package/dist/commands/update/update.js +25 -22
- package/dist/lib/auth/api.d.ts +3 -2
- package/dist/lib/auth/api.js +50 -56
- package/dist/lib/auth/env-match.js +1 -0
- package/dist/lib/auth/introspect.js +10 -11
- package/dist/lib/auth/oauth.d.ts +2 -0
- package/dist/lib/auth/oauth.js +30 -28
- package/dist/lib/auth/sign-in.d.ts +20 -0
- package/dist/lib/auth/sign-in.js +95 -0
- package/dist/lib/fs/config.d.ts +1 -0
- package/dist/lib/fs/config.js +14 -21
- package/dist/lib/fs/file-lock.d.ts +7 -2
- package/dist/lib/fs/file-lock.js +64 -74
- package/dist/lib/fs/json-file.d.ts +3 -0
- package/dist/lib/fs/json-file.js +43 -0
- package/dist/lib/net/fetch.d.ts +10 -0
- package/dist/lib/net/fetch.js +13 -0
- package/dist/lib/net/http.js +51 -23
- package/dist/lib/npm/npm-cli.d.ts +17 -0
- package/dist/lib/npm/npm-cli.js +77 -0
- package/dist/lib/status/status-info.js +1 -0
- package/dist/lib/update/update-cache.d.ts +1 -0
- package/dist/lib/update/update-cache.js +7 -11
- package/dist/lib/update/update-check.js +1 -4
- package/dist/program.d.ts +18 -1
- package/dist/program.js +75 -32
- package/package.json +1 -1
- package/dist/lib/update/update-lock.d.ts +0 -2
- package/dist/lib/update/update-lock.js +0 -109
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
2
|
import { ensureConfigDir, getConfigDir } from '../fs/config.js';
|
|
3
|
+
import { isRecord, readJson, writeJsonAtomic } from '../fs/json-file.js';
|
|
4
4
|
import { fetchJsonUnref } from '../net/http.js';
|
|
5
5
|
import { compareVersions, INSTALL_HINT, REGISTRY_URL } from './update-check.js';
|
|
6
6
|
import { VERSION } from '../../utils/version.js';
|
|
@@ -8,18 +8,14 @@ const CACHE_FILE = 'update-check.json';
|
|
|
8
8
|
const TTL_MS = 60 * 60 * 1000; // passive checks refresh at most once an hour
|
|
9
9
|
const BG_TIMEOUT_MS = 3000; // hard cap on the whole background attempt, connect included
|
|
10
10
|
const cachePath = () => join(getConfigDir(), CACHE_FILE);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
11
|
+
// A wrong-typed field reads as no cache: the post-action hint must never throw.
|
|
12
|
+
export const isUpdateCache = (value) => isRecord(value) &&
|
|
13
|
+
typeof value['checkedAt'] === 'number' &&
|
|
14
|
+
(value['latest'] === null || typeof value['latest'] === 'string');
|
|
15
|
+
export const readUpdateCache = () => readJson(cachePath(), isUpdateCache);
|
|
20
16
|
const writeUpdateCache = (latest, now) => {
|
|
21
17
|
ensureConfigDir();
|
|
22
|
-
|
|
18
|
+
writeJsonAtomic(cachePath(), { checkedAt: now, latest });
|
|
23
19
|
};
|
|
24
20
|
// The one dim hint line printed after a command, read purely from the cache (never the network),
|
|
25
21
|
// only when the cached latest is newer than us. null = print nothing.
|
|
@@ -7,10 +7,7 @@ export const REGISTRY_URL = 'https://registry.npmjs.org/@agentage/cli/latest';
|
|
|
7
7
|
// Compare dotted numeric versions (major.minor.patch); prerelease/build suffixes are
|
|
8
8
|
// ignored - good enough for an update hint. Returns -1 / 0 / 1.
|
|
9
9
|
export const compareVersions = (a, b) => {
|
|
10
|
-
const parts = (v) => v
|
|
11
|
-
.split('-')[0]
|
|
12
|
-
.split('.')
|
|
13
|
-
.map((n) => Number.parseInt(n, 10) || 0);
|
|
10
|
+
const parts = (v) => (v.split('-')[0] ?? '').split('.').map((n) => Number.parseInt(n, 10) || 0);
|
|
14
11
|
const pa = parts(a);
|
|
15
12
|
const pb = parts(b);
|
|
16
13
|
for (let i = 0; i < 3; i++) {
|
package/dist/program.d.ts
CHANGED
|
@@ -1 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { type SetupOptions } from './commands/auth/setup.js';
|
|
3
|
+
import { type StatusOptions } from './commands/status/status.js';
|
|
4
|
+
import { type UpdateOptions } from './commands/update/update.js';
|
|
5
|
+
export interface ProgramDeps {
|
|
6
|
+
setup: (opts: SetupOptions) => Promise<void>;
|
|
7
|
+
status: (opts: StatusOptions) => Promise<void>;
|
|
8
|
+
update: (opts: UpdateOptions) => Promise<void>;
|
|
9
|
+
updateHint: () => string | null;
|
|
10
|
+
refreshUpdateCache: () => Promise<void>;
|
|
11
|
+
writeOut: (text: string) => void;
|
|
12
|
+
writeErr: (text: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const EXIT_OK = 0;
|
|
15
|
+
export declare const EXIT_FAILED = 1;
|
|
16
|
+
export declare const EXIT_USAGE = 2;
|
|
17
|
+
export declare const createProgram: (deps?: ProgramDeps) => Command;
|
|
18
|
+
export declare const run: (argv?: string[], deps?: ProgramDeps) => Promise<number>;
|
package/dist/program.js
CHANGED
|
@@ -1,38 +1,81 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { registerSetup } from './commands/auth/setup.js';
|
|
4
|
-
import { registerStatus } from './commands/status/status.js';
|
|
5
|
-
import { registerUpdate } from './commands/update/update.js';
|
|
2
|
+
import { Command, CommanderError } from 'commander';
|
|
3
|
+
import { registerSetup, runSetup } from './commands/auth/setup.js';
|
|
4
|
+
import { registerStatus, runStatus } from './commands/status/status.js';
|
|
5
|
+
import { registerUpdate, runUpdate } from './commands/update/update.js';
|
|
6
|
+
import { ensureSignedIn, signOut } from './lib/auth/sign-in.js';
|
|
6
7
|
import { refreshUpdateCache, updateHint } from './lib/update/update-cache.js';
|
|
7
8
|
import { VERSION } from './utils/version.js';
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
const defaultDeps = {
|
|
10
|
+
setup: (opts) => runSetup(opts, {
|
|
11
|
+
ensureSignedIn: (signInOpts) => ensureSignedIn(signInOpts),
|
|
12
|
+
signOut: () => signOut(),
|
|
13
|
+
printStatus: () => runStatus({}),
|
|
14
|
+
}),
|
|
15
|
+
status: (opts) => runStatus(opts),
|
|
16
|
+
update: (opts) => runUpdate(opts),
|
|
17
|
+
updateHint,
|
|
18
|
+
refreshUpdateCache: () => refreshUpdateCache(),
|
|
19
|
+
writeOut: (text) => void process.stdout.write(text),
|
|
20
|
+
writeErr: (text) => void process.stderr.write(text),
|
|
21
|
+
};
|
|
22
|
+
export const EXIT_OK = 0;
|
|
23
|
+
export const EXIT_FAILED = 1;
|
|
24
|
+
export const EXIT_USAGE = 2;
|
|
25
|
+
const ACTION_FAILED = 'agentage.actionFailed';
|
|
26
|
+
const messageOf = (err) => (err instanceof Error ? err.message : String(err));
|
|
27
|
+
// One failure path for every command: a red stderr line, then exit 1 through run().
|
|
28
|
+
const withAction = (deps, fn, prefix = '') => async (opts) => {
|
|
29
|
+
try {
|
|
30
|
+
await fn(opts);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
deps.writeErr(`${chalk.red(`${prefix}${messageOf(err)}`)}\n`);
|
|
34
|
+
throw new CommanderError(EXIT_FAILED, ACTION_FAILED, messageOf(err));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
// status and update report update state themselves; the hint never touches --json stdout.
|
|
38
|
+
const hintAfter = (deps) => (_program, action) => {
|
|
39
|
+
if (action.name() === 'status' || action.name() === 'update')
|
|
21
40
|
return;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
41
|
+
const hint = action.opts()['json'] ? null : deps.updateHint();
|
|
42
|
+
if (hint)
|
|
43
|
+
deps.writeErr(`${chalk.dim(hint)}\n`);
|
|
44
|
+
void deps.refreshUpdateCache();
|
|
45
|
+
};
|
|
46
|
+
export const createProgram = (deps = defaultDeps) => {
|
|
47
|
+
const program = new Command()
|
|
48
|
+
.name('agentage')
|
|
49
|
+
.description('Connect this machine to agentage from the terminal')
|
|
50
|
+
.version(VERSION)
|
|
51
|
+
.exitOverride()
|
|
52
|
+
.configureOutput({ writeOut: deps.writeOut, writeErr: deps.writeErr })
|
|
53
|
+
.addHelpText('after', '\nNew here? Run: agentage setup to connect this machine.')
|
|
54
|
+
.hook('postAction', hintAfter(deps));
|
|
55
|
+
registerSetup(program, withAction(deps, deps.setup, 'Setup failed: '));
|
|
56
|
+
registerStatus(program, withAction(deps, deps.status));
|
|
57
|
+
registerUpdate(program, withAction(deps, deps.update));
|
|
58
|
+
return program;
|
|
59
|
+
};
|
|
60
|
+
// Commander's own exit 0 (--help, --version) stays 0; any other commander error is usage.
|
|
61
|
+
const exitCodeOf = (err, deps) => {
|
|
62
|
+
if (!(err instanceof CommanderError)) {
|
|
63
|
+
deps.writeErr(`${chalk.red(messageOf(err))}\n`);
|
|
64
|
+
return EXIT_FAILED;
|
|
65
|
+
}
|
|
66
|
+
if (err.code === ACTION_FAILED)
|
|
67
|
+
return EXIT_FAILED;
|
|
68
|
+
return err.exitCode === 0 ? EXIT_OK : EXIT_USAGE;
|
|
69
|
+
};
|
|
70
|
+
// Returns the exit code instead of exiting, so piped stdout is never cut short.
|
|
71
|
+
export const run = async (argv = process.argv, deps = defaultDeps) => {
|
|
72
|
+
if (process.env['NO_COLOR'])
|
|
73
|
+
chalk.level = 0; // chalk ignores NO_COLOR on its own
|
|
74
|
+
try {
|
|
75
|
+
await createProgram(deps).parseAsync(argv);
|
|
76
|
+
return EXIT_OK;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
return exitCodeOf(err, deps);
|
|
26
80
|
}
|
|
27
|
-
void refreshUpdateCache();
|
|
28
|
-
});
|
|
29
|
-
registerSetup(program);
|
|
30
|
-
registerStatus(program);
|
|
31
|
-
registerUpdate(program);
|
|
32
|
-
// Called by the bin (src/cli.ts).
|
|
33
|
-
export const run = async () => {
|
|
34
|
-
await program.parseAsync().catch((err) => {
|
|
35
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
36
|
-
process.exit(1);
|
|
37
|
-
});
|
|
38
81
|
};
|
package/package.json
CHANGED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { ensureConfigDir, getConfigDir } from '../fs/config.js';
|
|
4
|
-
const LOCK_FILE = 'update.lock';
|
|
5
|
-
const LOCK_TTL_MS = 10 * 60 * 1000; // a lock older than this whose holder is gone is a crashed run
|
|
6
|
-
// Past this age a holder is taken over even if its pid still resolves (pid reuse after a crash); far
|
|
7
|
-
// beyond any real run or scheduler pause, so a live-but-slow holder is never mistaken for crashed.
|
|
8
|
-
const STALE_HARD_MS = 30 * 60 * 1000;
|
|
9
|
-
const lockPath = () => join(getConfigDir(), LOCK_FILE);
|
|
10
|
-
// The lock file holds "<pid> <timestamp>": pid gates crash-takeover, the trailing token is the
|
|
11
|
-
// acquisition time. A legacy single-token file parses as pid 0 (unknown holder), aging out on TTL.
|
|
12
|
-
const holderOf = (path) => {
|
|
13
|
-
try {
|
|
14
|
-
const parts = readFileSync(path, 'utf-8').trim().split(/\s+/);
|
|
15
|
-
const at = Number.parseInt(parts[parts.length - 1] ?? '', 10);
|
|
16
|
-
if (Number.isNaN(at))
|
|
17
|
-
return null;
|
|
18
|
-
const pid = parts.length >= 2 ? Number.parseInt(parts[0] ?? '', 10) : 0;
|
|
19
|
-
return { pid: Number.isNaN(pid) ? 0 : pid, at };
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
// Same-host liveness probe. ESRCH = the holder is gone (crashed); EPERM = alive but owned by another
|
|
26
|
-
// user. pid 0/unknown counts as gone so a legacy or malformed lock still ages out on the TTL.
|
|
27
|
-
const holderAlive = (pid) => {
|
|
28
|
-
if (pid <= 0)
|
|
29
|
-
return false;
|
|
30
|
-
try {
|
|
31
|
-
process.kill(pid, 0);
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
return err.code === 'EPERM';
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
// A holder is safe to take over only once it is past the TTL AND provably gone: a dead pid, our own
|
|
39
|
-
// leftover pid, or so old that pid reuse is the only explanation. A live holder merely starved of
|
|
40
|
-
// CPU is never stolen - stealing it would let two writers run concurrently.
|
|
41
|
-
const isCrashed = (holder, now) => {
|
|
42
|
-
const age = now - holder.at;
|
|
43
|
-
if (age < LOCK_TTL_MS)
|
|
44
|
-
return false;
|
|
45
|
-
if (age >= STALE_HARD_MS)
|
|
46
|
-
return true;
|
|
47
|
-
return holder.pid === process.pid || !holderAlive(holder.pid);
|
|
48
|
-
};
|
|
49
|
-
const unlinkQuiet = (path) => {
|
|
50
|
-
try {
|
|
51
|
-
unlinkSync(path);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// already gone
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
// Serialize stale-lock takeover behind its own exclusive create, so no taker can ever delete a
|
|
58
|
-
// lock that turned FRESH mid-takeover (the ABA race: reader sees the stale timestamp, a winner
|
|
59
|
-
// re-creates the lock, the reader deletes the winner's file). Under the guard the staleness is
|
|
60
|
-
// re-checked and only a genuinely stale lock is cleared; losers simply refuse. A guard left by a
|
|
61
|
-
// crashed taker expires like the lock itself. Returns whether to re-race the create.
|
|
62
|
-
const takeOverStale = (now) => {
|
|
63
|
-
const guard = `${lockPath()}.takeover`;
|
|
64
|
-
const guardHeld = holderOf(guard);
|
|
65
|
-
if (guardHeld !== null && isCrashed(guardHeld, now))
|
|
66
|
-
unlinkQuiet(guard);
|
|
67
|
-
try {
|
|
68
|
-
writeFileSync(guard, `${process.pid} ${now}`, { flag: 'wx' });
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return false; // another taker is mid-takeover; let it win
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
const holder = holderOf(lockPath());
|
|
75
|
-
if (holder !== null && !isCrashed(holder, now))
|
|
76
|
-
return false; // fresh or a live-but-slow holder
|
|
77
|
-
unlinkQuiet(lockPath());
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
unlinkQuiet(guard);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
// Acquire the single-writer update lock. The O_EXCL ('wx') create IS the mutex - concurrent runs
|
|
85
|
-
// cannot both win it, unlike a check-then-write. A fresh holder (younger than the TTL) refuses;
|
|
86
|
-
// a stale one (crashed run) is cleared under the takeover guard and re-raced once. `now` is
|
|
87
|
-
// injectable.
|
|
88
|
-
export const acquireUpdateLock = (now = Date.now()) => {
|
|
89
|
-
ensureConfigDir();
|
|
90
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
91
|
-
try {
|
|
92
|
-
writeFileSync(lockPath(), `${process.pid} ${now}`, { flag: 'wx' });
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
const holder = holderOf(lockPath());
|
|
97
|
-
if (holder !== null && !isCrashed(holder, now))
|
|
98
|
-
return false;
|
|
99
|
-
if (!takeOverStale(now))
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return false;
|
|
104
|
-
};
|
|
105
|
-
export const releaseUpdateLock = () => {
|
|
106
|
-
const path = lockPath();
|
|
107
|
-
if (existsSync(path))
|
|
108
|
-
unlinkSync(path);
|
|
109
|
-
};
|