agent-dealer 0.1.12 → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-dealer/server",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -21,7 +21,7 @@
21
21
  "typecheck": "tsc --noEmit"
22
22
  },
23
23
  "dependencies": {
24
- "@agent-dealer/shared": "*",
24
+ "@agent-dealer/shared": "0.2.0",
25
25
  "@fastify/cors": "^11.0.1",
26
26
  "@fastify/static": "^8.2.0",
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-dealer/shared",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/dist/doctor.js CHANGED
@@ -4,6 +4,8 @@ import { claudeAvailable } from "./cli-check.js";
4
4
  import { loadProdEnvFile, prodEnvFilePath, prodHomeDir, resolveBundledListenPort, shortenHome, } from "./env.js";
5
5
  import { probeAgentDealer } from "./ports.js";
6
6
  import { resolveServerEntry, resolveUiDist } from "./paths.js";
7
+ import { detectInstallKind, localBinLauncherPath, readCurrentManagedVersion, readUpdateState, resolveCurrentVersionDir, } from "./managed/index.js";
8
+ import { getVersion } from "./version.js";
7
9
  export async function runDoctor() {
8
10
  let failed = false;
9
11
  const major = Number(process.versions.node.split(".")[0]);
@@ -53,6 +55,22 @@ export async function runDoctor() {
53
55
  if (fs.existsSync(home)) {
54
56
  console.log(`✓ data ${shortenHome(home)}`);
55
57
  }
58
+ console.log(`Package version ${getVersion()}`);
59
+ const kind = detectInstallKind();
60
+ console.log(`Install kind: ${kind}`);
61
+ if (kind === "managed") {
62
+ const current = readCurrentManagedVersion();
63
+ const currentDir = resolveCurrentVersionDir();
64
+ console.log(`Managed current: ${current ?? "(unknown)"} (${currentDir ?? "missing"})`);
65
+ console.log(`Launcher: ${localBinLauncherPath()}`);
66
+ const pending = readUpdateState()?.pendingVersion;
67
+ if (pending) {
68
+ console.log(`Pending managed version: ${pending} (activates on next start/doctor/upgrade)`);
69
+ }
70
+ }
71
+ else {
72
+ console.log("Tip: agent-dealer install # managed CLI + auto-updates (data unchanged)");
73
+ }
56
74
  const port = resolveBundledListenPort();
57
75
  const probe = await probeAgentDealer("127.0.0.1", port);
58
76
  if (probe.up) {
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { runStop } from "./stop.js";
6
6
  import { maybeCheckForUpdateOnRun } from "./update-check.js";
7
7
  import { getVersion } from "./version.js";
8
8
  import { runUpgrade, printUpgradeHelp } from "./upgrade.js";
9
+ import { runInstall, printInstallHelp } from "./install.js";
9
10
  function printUsage() {
10
11
  console.log(`Usage:
11
12
  agent-dealer setup [--home DIR] [--force]
@@ -13,7 +14,8 @@ function printUsage() {
13
14
  agent-dealer stop
14
15
  agent-dealer status
15
16
  agent-dealer doctor
16
- agent-dealer upgrade [--to VERSION] [--yes]
17
+ agent-dealer install [--to VERSION] [--migrate-cli] [--purge-global]
18
+ agent-dealer upgrade [--to VERSION] [--yes] [--check]
17
19
  agent-dealer --version
18
20
 
19
21
  Human control plane for agent execution.`);
@@ -92,6 +94,13 @@ export async function runCli(argv) {
92
94
  return 0;
93
95
  return runDoctor();
94
96
  }
97
+ if (cmd === "install") {
98
+ if (args.includes("--help")) {
99
+ printInstallHelp();
100
+ return 0;
101
+ }
102
+ return runInstall(args.slice(1));
103
+ }
95
104
  if (cmd === "upgrade") {
96
105
  if (args.includes("--help")) {
97
106
  printUpgradeHelp();
@@ -99,17 +108,20 @@ export async function runCli(argv) {
99
108
  }
100
109
  let toVersion;
101
110
  let yes = false;
111
+ let check = false;
102
112
  for (let i = 1; i < args.length; i += 1) {
103
113
  if (args[i] === "--to")
104
114
  toVersion = args[++i];
105
115
  else if (args[i] === "--yes")
106
116
  yes = true;
117
+ else if (args[i] === "--check")
118
+ check = true;
107
119
  else {
108
120
  console.error(`Unknown upgrade option: ${args[i]}`);
109
121
  return 1;
110
122
  }
111
123
  }
112
- return runUpgrade({ toVersion, yes });
124
+ return runUpgrade({ toVersion, yes, check });
113
125
  }
114
126
  console.error(`Unknown command: ${cmd}`);
115
127
  printUsage();
@@ -0,0 +1,8 @@
1
+ import { installCliVersionToPrefix } from "./managed/index.js";
2
+ export type InstallDeps = {
3
+ installVersion?: typeof installCliVersionToPrefix;
4
+ fetchLatest?: () => Promise<string | null>;
5
+ purgeGlobal?: () => Promise<number>;
6
+ };
7
+ export declare function printInstallHelp(): void;
8
+ export declare function runInstall(args: string[], deps?: InstallDeps): Promise<number>;
@@ -0,0 +1,72 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { activateVersion, cliEntryInVersionDir, fetchLatestVersion, installCliVersionToPrefix, localBinLauncherPath, PACKAGE_NAME, versionDir, } from "./managed/index.js";
6
+ function parseArgs(args) {
7
+ let to;
8
+ let migrateCli = false;
9
+ let purgeGlobal = false;
10
+ for (let i = 0; i < args.length; i += 1) {
11
+ const a = args[i];
12
+ if (a === "--to")
13
+ to = args[++i];
14
+ else if (a === "--migrate-cli")
15
+ migrateCli = true;
16
+ else if (a === "--purge-global")
17
+ purgeGlobal = true;
18
+ }
19
+ return { to, migrateCli, purgeGlobal };
20
+ }
21
+ export function printInstallHelp() {
22
+ console.log(`Usage:
23
+ agent-dealer install [--to VERSION] [--migrate-cli] [--purge-global]
24
+
25
+ Installs agent-dealer into ~/.agent-dealer/versions and writes ~/.local/bin/agent-dealer.
26
+ Existing config, queue data, and logs are left unchanged.`);
27
+ }
28
+ export async function runInstall(args, deps = {}) {
29
+ if (args.includes("--help") || args.includes("-h")) {
30
+ printInstallHelp();
31
+ return 0;
32
+ }
33
+ const { to, migrateCli, purgeGlobal } = parseArgs(args);
34
+ const fetchLatest = deps.fetchLatest ?? fetchLatestVersion;
35
+ const installVersion = deps.installVersion ?? installCliVersionToPrefix;
36
+ const version = to ?? (await fetchLatest());
37
+ if (!version) {
38
+ console.error("Could not resolve latest version from npm.");
39
+ return 1;
40
+ }
41
+ if (!fs.existsSync(cliEntryInVersionDir(versionDir(version)))) {
42
+ const result = await installVersion(version);
43
+ if (!result.ok) {
44
+ console.error(`Managed install failed: ${result.error}`);
45
+ return 1;
46
+ }
47
+ }
48
+ activateVersion(version);
49
+ const home = process.env.AGENT_DEALER_HOME?.trim() || path.join(os.homedir(), ".agent-dealer");
50
+ console.log(`Installed ${PACKAGE_NAME}@${version} to ${home} (data home unchanged).`);
51
+ console.log(`Launcher: ${localBinLauncherPath()}`);
52
+ console.log('If command not found: export PATH="$HOME/.local/bin:$PATH"');
53
+ if (migrateCli) {
54
+ console.log("Prefer ~/.local/bin ahead of any npm global agent-dealer on PATH.");
55
+ }
56
+ console.log("Next: agent-dealer doctor && agent-dealer start --daemon --open");
57
+ if (purgeGlobal) {
58
+ const code = deps.purgeGlobal
59
+ ? await deps.purgeGlobal()
60
+ : await new Promise((resolve) => {
61
+ const child = spawn("npm", ["uninstall", "-g", PACKAGE_NAME], {
62
+ stdio: "inherit",
63
+ shell: process.platform === "win32",
64
+ });
65
+ child.on("exit", (c) => resolve(c ?? 1));
66
+ });
67
+ if (code !== 0) {
68
+ console.warn("Warning: could not uninstall npm global package (managed install is still active).");
69
+ }
70
+ }
71
+ return 0;
72
+ }
@@ -0,0 +1,2 @@
1
+ export declare function activateVersion(version: string): void;
2
+ export declare function pruneOldVersions(keep: number): void;
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { writeLocalBinLauncher } from "./launcher.js";
4
+ import { cliEntryInVersionDir, currentLinkPath, resolveCurrentVersionDir, versionDir, versionsDir, } from "./paths.js";
5
+ import { compareSemver } from "./semver.js";
6
+ export function activateVersion(version) {
7
+ const target = versionDir(version);
8
+ const entry = cliEntryInVersionDir(target);
9
+ if (!fs.existsSync(entry)) {
10
+ throw new Error(`Cannot activate ${version}: missing ${entry}`);
11
+ }
12
+ const link = currentLinkPath();
13
+ fs.mkdirSync(path.dirname(link), { recursive: true });
14
+ try {
15
+ const st = fs.lstatSync(link);
16
+ if (st.isSymbolicLink() || st.isFile()) {
17
+ fs.unlinkSync(link);
18
+ }
19
+ else {
20
+ fs.rmSync(link, { recursive: true, force: true });
21
+ }
22
+ }
23
+ catch {
24
+ // missing
25
+ }
26
+ fs.symlinkSync(target, link, "dir");
27
+ writeLocalBinLauncher();
28
+ pruneOldVersions(3);
29
+ }
30
+ export function pruneOldVersions(keep) {
31
+ const root = versionsDir();
32
+ if (!fs.existsSync(root))
33
+ return;
34
+ const currentReal = resolveCurrentVersionDir();
35
+ const names = fs
36
+ .readdirSync(root, { withFileTypes: true })
37
+ .filter((d) => d.isDirectory() && !d.name.startsWith("."))
38
+ .map((d) => d.name)
39
+ .sort((a, b) => compareSemver(b, a));
40
+ const retained = names.slice(0, Math.max(keep, 0));
41
+ for (const name of names) {
42
+ const full = path.join(root, name);
43
+ if (retained.includes(name))
44
+ continue;
45
+ if (currentReal && fs.existsSync(full) && fs.realpathSync(full) === currentReal)
46
+ continue;
47
+ fs.rmSync(full, { recursive: true, force: true });
48
+ }
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { activateVersion } from "./activate.js";
7
+ import { detectInstallKind } from "./install-kind.js";
8
+ import { currentLinkPath, localBinLauncherPath, versionDir } from "./paths.js";
9
+ function seedVersion(ver) {
10
+ const dir = versionDir(ver);
11
+ const bin = path.join(dir, "node_modules", "agent-dealer", "dist", "bin.js");
12
+ fs.mkdirSync(path.dirname(bin), { recursive: true });
13
+ fs.writeFileSync(bin, "ok\n");
14
+ }
15
+ test("managed activate preserves sibling data and writes launcher", () => {
16
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "adlr-act-"));
17
+ const localBin = path.join(tmp, "local-bin");
18
+ process.env.AGENT_DEALER_HOME = tmp;
19
+ process.env.AGENT_DEALER_LOCAL_BIN = localBin;
20
+ try {
21
+ fs.writeFileSync(path.join(tmp, ".env"), "KEEP=1\n");
22
+ seedVersion("0.1.0");
23
+ activateVersion("0.1.0");
24
+ assert.equal(fs.realpathSync(currentLinkPath()), fs.realpathSync(versionDir("0.1.0")));
25
+ assert.equal(fs.readFileSync(path.join(tmp, ".env"), "utf8"), "KEEP=1\n");
26
+ assert.ok(fs.existsSync(localBinLauncherPath()));
27
+ assert.equal(detectInstallKind(), "managed");
28
+ }
29
+ finally {
30
+ delete process.env.AGENT_DEALER_HOME;
31
+ delete process.env.AGENT_DEALER_LOCAL_BIN;
32
+ fs.rmSync(tmp, { recursive: true, force: true });
33
+ }
34
+ });
@@ -0,0 +1,8 @@
1
+ export { agentDealerHome, cliEntryInVersionDir, currentLinkPath, localBinDir, localBinLauncherPath, partialVersionDir, resolveCurrentVersionDir, updateStatePath, versionDir, versionsDir, } from "./paths.js";
2
+ export { detectInstallKind, type InstallKind } from "./install-kind.js";
3
+ export { activateVersion, pruneOldVersions } from "./activate.js";
4
+ export { writeLocalBinLauncher } from "./launcher.js";
5
+ export { installCliVersionToPrefix, PACKAGE_NAME } from "./npm-prefix-install.js";
6
+ export { compareSemver } from "./semver.js";
7
+ export { readUpdateState, writeUpdateState, type UpdateState } from "./update-state.js";
8
+ export { ensurePendingDownload, fetchLatestVersion, isAutoupdaterDisabled, maybeActivatePendingVersion, readCurrentManagedVersion, runManagedCliEntryHooks, scheduleBackgroundUpdateCheck, } from "./updater.js";
@@ -0,0 +1,8 @@
1
+ export { agentDealerHome, cliEntryInVersionDir, currentLinkPath, localBinDir, localBinLauncherPath, partialVersionDir, resolveCurrentVersionDir, updateStatePath, versionDir, versionsDir, } from "./paths.js";
2
+ export { detectInstallKind } from "./install-kind.js";
3
+ export { activateVersion, pruneOldVersions } from "./activate.js";
4
+ export { writeLocalBinLauncher } from "./launcher.js";
5
+ export { installCliVersionToPrefix, PACKAGE_NAME } from "./npm-prefix-install.js";
6
+ export { compareSemver } from "./semver.js";
7
+ export { readUpdateState, writeUpdateState } from "./update-state.js";
8
+ export { ensurePendingDownload, fetchLatestVersion, isAutoupdaterDisabled, maybeActivatePendingVersion, readCurrentManagedVersion, runManagedCliEntryHooks, scheduleBackgroundUpdateCheck, } from "./updater.js";
@@ -0,0 +1,5 @@
1
+ export type InstallKind = "managed" | "npm-global" | "unknown";
2
+ export declare function detectInstallKind(options?: {
3
+ whichPath?: string;
4
+ npmGlobalPrefix?: string;
5
+ }): InstallKind;
@@ -0,0 +1,16 @@
1
+ import { resolveCurrentVersionDir } from "./paths.js";
2
+ export function detectInstallKind(options) {
3
+ if (resolveCurrentVersionDir())
4
+ return "managed";
5
+ const whichPath = options?.whichPath;
6
+ const prefix = options?.npmGlobalPrefix;
7
+ if (whichPath && prefix) {
8
+ const normalizedWhich = whichPath.replace(/\\/g, "/");
9
+ const normalizedPrefix = prefix.replace(/\\/g, "/").replace(/\/$/, "");
10
+ if (normalizedWhich === normalizedPrefix ||
11
+ normalizedWhich.startsWith(`${normalizedPrefix}/`)) {
12
+ return "npm-global";
13
+ }
14
+ }
15
+ return "unknown";
16
+ }
@@ -0,0 +1 @@
1
+ export declare function writeLocalBinLauncher(): void;
@@ -0,0 +1,19 @@
1
+ import fs from "node:fs";
2
+ import { localBinDir, localBinLauncherPath } from "./paths.js";
3
+ const LAUNCHER_BODY = `#!/usr/bin/env bash
4
+ set -euo pipefail
5
+ HOME_DIR="\${AGENT_DEALER_HOME:-$HOME/.agent-dealer}"
6
+ CURRENT="$HOME_DIR/current"
7
+ BIN="$CURRENT/node_modules/agent-dealer/dist/bin.js"
8
+ if [ ! -f "$BIN" ]; then
9
+ echo "agent-dealer: managed install broken (missing $BIN). Re-run: agent-dealer install" >&2
10
+ exit 1
11
+ fi
12
+ exec node "$BIN" "$@"
13
+ `;
14
+ export function writeLocalBinLauncher() {
15
+ fs.mkdirSync(localBinDir(), { recursive: true });
16
+ const target = localBinLauncherPath();
17
+ fs.writeFileSync(target, LAUNCHER_BODY, { mode: 0o755 });
18
+ fs.chmodSync(target, 0o755);
19
+ }
@@ -0,0 +1,16 @@
1
+ export declare const PACKAGE_NAME = "agent-dealer";
2
+ export type NpmSpawn = (args: string[], options: {
3
+ cwd?: string;
4
+ }) => Promise<{
5
+ code: number;
6
+ stderr: string;
7
+ }>;
8
+ export declare function installCliVersionToPrefix(version: string, options?: {
9
+ npmSpawn?: NpmSpawn;
10
+ }): Promise<{
11
+ ok: true;
12
+ dir: string;
13
+ } | {
14
+ ok: false;
15
+ error: string;
16
+ }>;
@@ -0,0 +1,38 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import { cliEntryInVersionDir, partialVersionDir, versionDir, versionsDir } from "./paths.js";
4
+ export const PACKAGE_NAME = "agent-dealer";
5
+ const defaultNpmSpawn = (args, options) => new Promise((resolve) => {
6
+ const child = spawn("npm", args, {
7
+ cwd: options.cwd,
8
+ stdio: ["ignore", "pipe", "pipe"],
9
+ shell: process.platform === "win32",
10
+ });
11
+ let stderr = "";
12
+ child.stderr?.on("data", (chunk) => {
13
+ stderr += chunk.toString();
14
+ });
15
+ child.on("error", (err) => resolve({ code: 1, stderr: err.message }));
16
+ child.on("exit", (code) => resolve({ code: code ?? 1, stderr }));
17
+ });
18
+ export async function installCliVersionToPrefix(version, options = {}) {
19
+ const npmSpawn = options.npmSpawn ?? defaultNpmSpawn;
20
+ const partial = partialVersionDir(version);
21
+ const finalDir = versionDir(version);
22
+ fs.mkdirSync(versionsDir(), { recursive: true });
23
+ fs.rmSync(partial, { recursive: true, force: true });
24
+ fs.mkdirSync(partial, { recursive: true });
25
+ const result = await npmSpawn(["install", "--prefix", partial, `${PACKAGE_NAME}@${version}`], {});
26
+ if (result.code !== 0) {
27
+ fs.rmSync(partial, { recursive: true, force: true });
28
+ return { ok: false, error: result.stderr.trim() || `npm install failed (exit ${result.code})` };
29
+ }
30
+ const entry = cliEntryInVersionDir(partial);
31
+ if (!fs.existsSync(entry)) {
32
+ fs.rmSync(partial, { recursive: true, force: true });
33
+ return { ok: false, error: `Install succeeded but missing CLI entry: ${entry}` };
34
+ }
35
+ fs.rmSync(finalDir, { recursive: true, force: true });
36
+ fs.renameSync(partial, finalDir);
37
+ return { ok: true, dir: finalDir };
38
+ }
@@ -0,0 +1,11 @@
1
+ export declare function agentDealerHome(): string;
2
+ export declare function versionsDir(): string;
3
+ export declare function versionDir(version: string): string;
4
+ export declare function partialVersionDir(version: string): string;
5
+ export declare function currentLinkPath(): string;
6
+ export declare function updateStatePath(): string;
7
+ /** Override with AGENT_DEALER_LOCAL_BIN for tests. */
8
+ export declare function localBinDir(): string;
9
+ export declare function localBinLauncherPath(): string;
10
+ export declare function cliEntryInVersionDir(dir: string): string;
11
+ export declare function resolveCurrentVersionDir(): string | null;
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export function agentDealerHome() {
5
+ return process.env.AGENT_DEALER_HOME?.trim() || path.join(os.homedir(), ".agent-dealer");
6
+ }
7
+ export function versionsDir() {
8
+ return path.join(agentDealerHome(), "versions");
9
+ }
10
+ export function versionDir(version) {
11
+ return path.join(versionsDir(), version);
12
+ }
13
+ export function partialVersionDir(version) {
14
+ return path.join(versionsDir(), `.partial-${version}`);
15
+ }
16
+ export function currentLinkPath() {
17
+ return path.join(agentDealerHome(), "current");
18
+ }
19
+ export function updateStatePath() {
20
+ return path.join(agentDealerHome(), "update-state.json");
21
+ }
22
+ /** Override with AGENT_DEALER_LOCAL_BIN for tests. */
23
+ export function localBinDir() {
24
+ const override = process.env.AGENT_DEALER_LOCAL_BIN?.trim();
25
+ if (override)
26
+ return override;
27
+ return path.join(os.homedir(), ".local", "bin");
28
+ }
29
+ export function localBinLauncherPath() {
30
+ return path.join(localBinDir(), "agent-dealer");
31
+ }
32
+ export function cliEntryInVersionDir(dir) {
33
+ return path.join(dir, "node_modules", "agent-dealer", "dist", "bin.js");
34
+ }
35
+ export function resolveCurrentVersionDir() {
36
+ const link = currentLinkPath();
37
+ try {
38
+ if (!fs.existsSync(link))
39
+ return null;
40
+ return fs.realpathSync(link);
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
@@ -0,0 +1,2 @@
1
+ /** Semver compare: positive if a > b. */
2
+ export declare function compareSemver(a: string, b: string): number;
@@ -0,0 +1,12 @@
1
+ /** Semver compare: positive if a > b. */
2
+ export function compareSemver(a, b) {
3
+ const parse = (value) => value.replace(/^v/, "").split(".").map((part) => Number.parseInt(part, 10) || 0);
4
+ const left = parse(a);
5
+ const right = parse(b);
6
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
7
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
8
+ if (diff !== 0)
9
+ return diff > 0 ? 1 : -1;
10
+ }
11
+ return 0;
12
+ }
@@ -0,0 +1,7 @@
1
+ export interface UpdateState {
2
+ checkedAt: string;
3
+ latest: string | null;
4
+ pendingVersion: string | null;
5
+ }
6
+ export declare function readUpdateState(): UpdateState | null;
7
+ export declare function writeUpdateState(state: UpdateState): void;
@@ -0,0 +1,22 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { updateStatePath } from "./paths.js";
4
+ export function readUpdateState() {
5
+ try {
6
+ const raw = fs.readFileSync(updateStatePath(), "utf8");
7
+ const parsed = JSON.parse(raw);
8
+ return {
9
+ checkedAt: parsed.checkedAt,
10
+ latest: parsed.latest ?? null,
11
+ pendingVersion: parsed.pendingVersion ?? null,
12
+ };
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export function writeUpdateState(state) {
19
+ const file = updateStatePath();
20
+ fs.mkdirSync(path.dirname(file), { recursive: true });
21
+ fs.writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`, "utf8");
22
+ }
@@ -0,0 +1,21 @@
1
+ import { installCliVersionToPrefix } from "./npm-prefix-install.js";
2
+ export declare function isAutoupdaterDisabled(): boolean;
3
+ export declare function fetchLatestVersion(): Promise<string | null>;
4
+ export declare function readCurrentManagedVersion(): string | null;
5
+ export declare function maybeActivatePendingVersion(): {
6
+ activated: string | null;
7
+ };
8
+ export declare function ensurePendingDownload(latest: string, options?: {
9
+ installVersion?: typeof installCliVersionToPrefix;
10
+ }): Promise<void>;
11
+ export declare function scheduleBackgroundUpdateCheck(options?: {
12
+ fetchLatest?: () => Promise<string | null>;
13
+ installVersion?: typeof installCliVersionToPrefix;
14
+ }): void;
15
+ export declare function runManagedCliEntryHooks(options: {
16
+ allowActivate: boolean;
17
+ fetchLatest?: () => Promise<string | null>;
18
+ installVersion?: typeof installCliVersionToPrefix;
19
+ }): {
20
+ activated: string | null;
21
+ };
@@ -0,0 +1,107 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { activateVersion } from "./activate.js";
4
+ import { detectInstallKind } from "./install-kind.js";
5
+ import { installCliVersionToPrefix } from "./npm-prefix-install.js";
6
+ import { cliEntryInVersionDir, resolveCurrentVersionDir, versionDir } from "./paths.js";
7
+ import { compareSemver } from "./semver.js";
8
+ import { readUpdateState, writeUpdateState } from "./update-state.js";
9
+ import { fetchLatestPublishedVersion } from "../npm-registry.js";
10
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
11
+ const PACKAGE_NAME = "agent-dealer";
12
+ export function isAutoupdaterDisabled() {
13
+ const v = process.env.AGENT_DEALER_DISABLE_AUTOUPDATER?.trim().toLowerCase();
14
+ return v === "1" || v === "true";
15
+ }
16
+ export async function fetchLatestVersion() {
17
+ const v = await fetchLatestPublishedVersion(PACKAGE_NAME);
18
+ return v ?? null;
19
+ }
20
+ export function readCurrentManagedVersion() {
21
+ const real = resolveCurrentVersionDir();
22
+ if (!real)
23
+ return null;
24
+ return path.basename(real);
25
+ }
26
+ export function maybeActivatePendingVersion() {
27
+ if (isAutoupdaterDisabled())
28
+ return { activated: null };
29
+ if (detectInstallKind() !== "managed")
30
+ return { activated: null };
31
+ const state = readUpdateState();
32
+ const pending = state?.pendingVersion;
33
+ if (!pending)
34
+ return { activated: null };
35
+ const dir = versionDir(pending);
36
+ if (!fs.existsSync(cliEntryInVersionDir(dir)))
37
+ return { activated: null };
38
+ activateVersion(pending);
39
+ writeUpdateState({
40
+ checkedAt: state?.checkedAt ?? new Date().toISOString(),
41
+ latest: state?.latest ?? pending,
42
+ pendingVersion: null,
43
+ });
44
+ return { activated: pending };
45
+ }
46
+ export async function ensurePendingDownload(latest, options = {}) {
47
+ if (isAutoupdaterDisabled())
48
+ return;
49
+ const dir = versionDir(latest);
50
+ if (fs.existsSync(cliEntryInVersionDir(dir))) {
51
+ const prev = readUpdateState();
52
+ writeUpdateState({
53
+ checkedAt: prev?.checkedAt ?? new Date().toISOString(),
54
+ latest,
55
+ pendingVersion: latest,
56
+ });
57
+ return;
58
+ }
59
+ const install = options.installVersion ?? installCliVersionToPrefix;
60
+ const result = await install(latest);
61
+ if (!result.ok)
62
+ return;
63
+ const prev = readUpdateState();
64
+ writeUpdateState({
65
+ checkedAt: prev?.checkedAt ?? new Date().toISOString(),
66
+ latest,
67
+ pendingVersion: latest,
68
+ });
69
+ }
70
+ export function scheduleBackgroundUpdateCheck(options = {}) {
71
+ if (isAutoupdaterDisabled() || detectInstallKind() !== "managed")
72
+ return;
73
+ const state = readUpdateState();
74
+ if (state?.checkedAt) {
75
+ const age = Date.now() - Date.parse(state.checkedAt);
76
+ if (Number.isFinite(age) && age >= 0 && age < CACHE_TTL_MS)
77
+ return;
78
+ }
79
+ const fetchLatest = options.fetchLatest ?? fetchLatestVersion;
80
+ void (async () => {
81
+ try {
82
+ const latest = await fetchLatest();
83
+ writeUpdateState({
84
+ checkedAt: new Date().toISOString(),
85
+ latest,
86
+ pendingVersion: readUpdateState()?.pendingVersion ?? null,
87
+ });
88
+ if (!latest)
89
+ return;
90
+ const current = readCurrentManagedVersion();
91
+ if (current && compareSemver(latest, current) <= 0)
92
+ return;
93
+ await ensurePendingDownload(latest, { installVersion: options.installVersion });
94
+ }
95
+ catch {
96
+ // background only
97
+ }
98
+ })();
99
+ }
100
+ export function runManagedCliEntryHooks(options) {
101
+ const activated = options.allowActivate ? maybeActivatePendingVersion().activated : null;
102
+ scheduleBackgroundUpdateCheck({
103
+ fetchLatest: options.fetchLatest,
104
+ installVersion: options.installVersion,
105
+ });
106
+ return { activated };
107
+ }
@@ -1,5 +1,6 @@
1
1
  type UpdateCheckResult = {
2
2
  upgraded: boolean;
3
3
  };
4
+ /** npm-global / unknown path — keep TTY prompt behavior. Managed uses hooks instead. */
4
5
  export declare function maybeCheckForUpdateOnRun(): Promise<UpdateCheckResult>;
5
6
  export {};
@@ -1,32 +1,15 @@
1
+ import { detectInstallKind, runManagedCliEntryHooks } from "./managed/index.js";
2
+ import { installAgentDealerVersion } from "./upgrade.js";
3
+ import { fetchLatestPublishedVersion } from "./npm-registry.js";
4
+ import { getVersion } from "./version.js";
5
+ import { compareSemver } from "./managed/semver.js";
1
6
  import fs from "node:fs";
2
7
  import path from "node:path";
3
8
  import readline from "node:readline";
4
9
  import { prodHomeDir } from "./env.js";
5
- import { getVersion } from "./version.js";
6
- import { fetchLatestPublishedVersion } from "./npm-registry.js";
7
- import { installAgentDealerVersion } from "./upgrade.js";
8
10
  const PKG_NAME = "agent-dealer";
9
11
  const CACHE_FILE = ".agent-dealer-update-check.json";
10
12
  const CHECK_THROTTLE_MS = 24 * 60 * 60 * 1000;
11
- function parseXyzVersion(v) {
12
- const m = v.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
13
- if (!m)
14
- return undefined;
15
- return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
16
- }
17
- function isNewer(latest, current) {
18
- const a = parseXyzVersion(latest);
19
- const b = parseXyzVersion(current);
20
- if (!a || !b)
21
- return false;
22
- if (a.major !== b.major)
23
- return a.major > b.major;
24
- if (a.minor !== b.minor)
25
- return a.minor > b.minor;
26
- if (a.patch !== b.patch)
27
- return a.patch > b.patch;
28
- return false;
29
- }
30
13
  async function promptYesNo(message) {
31
14
  if (!process.stdin.isTTY || !process.stdout.isTTY)
32
15
  return false;
@@ -36,16 +19,14 @@ async function promptYesNo(message) {
36
19
  return answer.trim().toLowerCase().startsWith("y");
37
20
  }
38
21
  function cachePath() {
39
- const home = prodHomeDir();
40
- return path.join(home, CACHE_FILE);
22
+ return path.join(prodHomeDir(), CACHE_FILE);
41
23
  }
42
24
  function readCache() {
43
25
  try {
44
26
  const p = cachePath();
45
27
  if (!fs.existsSync(p))
46
28
  return undefined;
47
- const raw = fs.readFileSync(p, "utf8");
48
- return JSON.parse(raw);
29
+ return JSON.parse(fs.readFileSync(p, "utf8"));
49
30
  }
50
31
  catch {
51
32
  return undefined;
@@ -53,23 +34,34 @@ function readCache() {
53
34
  }
54
35
  function writeCache(next) {
55
36
  try {
56
- const home = prodHomeDir();
57
- fs.mkdirSync(home, { recursive: true });
37
+ fs.mkdirSync(prodHomeDir(), { recursive: true });
58
38
  fs.writeFileSync(cachePath(), JSON.stringify(next), "utf8");
59
39
  }
60
40
  catch {
61
- // Ignore cache write failures (permissions, read-only FS, etc).
41
+ // ignore
62
42
  }
63
43
  }
44
+ /** npm-global / unknown path — keep TTY prompt behavior. Managed uses hooks instead. */
64
45
  export async function maybeCheckForUpdateOnRun() {
46
+ if (detectInstallKind() === "managed") {
47
+ const { activated } = runManagedCliEntryHooks({ allowActivate: true });
48
+ if (activated) {
49
+ console.log(`[agent-dealer] Activated managed version ${activated}`);
50
+ console.log("Re-run your command to use the new version.");
51
+ return { upgraded: true };
52
+ }
53
+ return { upgraded: false };
54
+ }
65
55
  if (process.env.AGENT_DEALER_DISABLE_UPGRADE_CHECK === "1") {
66
56
  return { upgraded: false };
67
57
  }
58
+ if (process.env.AGENT_DEALER_DISABLE_AUTOUPDATER === "1") {
59
+ return { upgraded: false };
60
+ }
68
61
  const current = getVersion();
69
62
  const cache = readCache();
70
63
  if (cache?.checkedAt && Date.now() - cache.checkedAt < CHECK_THROTTLE_MS) {
71
- // Throttle network checks, but still report based on cached latestVersion.
72
- if (cache.latestVersion && isNewer(cache.latestVersion, current)) {
64
+ if (cache.latestVersion && compareSemver(cache.latestVersion, current) > 0) {
73
65
  console.log(`Update available: ${current} -> ${cache.latestVersion}`);
74
66
  const autoUpgrade = ["1", "true", "yes"].includes(process.env.AGENT_DEALER_AUTO_UPGRADE?.trim().toLowerCase() ?? "");
75
67
  if (autoUpgrade) {
@@ -79,9 +71,9 @@ export async function maybeCheckForUpdateOnRun() {
79
71
  const canPrompt = process.stdin.isTTY && process.stdout.isTTY;
80
72
  if (!canPrompt) {
81
73
  console.log(`Run: agent-dealer upgrade`);
74
+ console.log("Tip: agent-dealer install # managed auto-updates");
82
75
  return { upgraded: false };
83
76
  }
84
- // Prompt at most once per throttle window to avoid nagging.
85
77
  const promptAgeMs = cache.promptedAt ? Date.now() - cache.promptedAt : Infinity;
86
78
  if (promptAgeMs >= CHECK_THROTTLE_MS) {
87
79
  writeCache({ ...cache, promptedAt: Date.now() });
@@ -89,20 +81,16 @@ export async function maybeCheckForUpdateOnRun() {
89
81
  if (!ok)
90
82
  return { upgraded: false };
91
83
  }
92
- else {
93
- // Recently prompted; do not prompt again.
94
- }
95
84
  const code = await installAgentDealerVersion(cache.latestVersion);
96
85
  return { upgraded: code === 0 };
97
86
  }
98
87
  return { upgraded: false };
99
88
  }
100
89
  const latest = await fetchLatestPublishedVersion(PKG_NAME);
101
- // Always write checkedAt so we don't hammer the registry on repeated failures.
102
90
  writeCache({ checkedAt: Date.now(), latestVersion: latest, promptedAt: cache?.promptedAt });
103
91
  if (!latest || latest === current)
104
92
  return { upgraded: false };
105
- if (!isNewer(latest, current))
93
+ if (compareSemver(latest, current) <= 0)
106
94
  return { upgraded: false };
107
95
  const autoUpgrade = ["1", "true", "yes"].includes(process.env.AGENT_DEALER_AUTO_UPGRADE?.trim().toLowerCase() ?? "");
108
96
  console.log(`Update available: ${current} -> ${latest}`);
package/dist/upgrade.d.ts CHANGED
@@ -2,5 +2,6 @@ export declare function printUpgradeHelp(): void;
2
2
  export declare function runUpgrade(options?: {
3
3
  toVersion?: string;
4
4
  yes?: boolean;
5
+ check?: boolean;
5
6
  }): Promise<number>;
6
7
  export declare function installAgentDealerVersion(version: string): Promise<number>;
package/dist/upgrade.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import readline from "node:readline";
3
+ import { activateVersion, compareSemver, detectInstallKind, fetchLatestVersion, installCliVersionToPrefix, readCurrentManagedVersion, } from "./managed/index.js";
3
4
  import { fetchLatestPublishedVersion } from "./npm-registry.js";
5
+ import { getVersion } from "./version.js";
4
6
  const PKG_NAME = "agent-dealer";
5
7
  export function printUpgradeHelp() {
6
8
  console.log(`Usage:
7
- agent-dealer upgrade [--to VERSION] [--yes]
9
+ agent-dealer upgrade [--to VERSION] [--yes] [--check]
8
10
 
9
- Installs the latest global version of agent-dealer (or --to VERSION).`);
11
+ Managed install: download into ~/.agent-dealer/versions and activate.
12
+ npm-global: npm install -g agent-dealer@VERSION.`);
10
13
  }
11
14
  function parseBooleanLike(s) {
12
15
  const v = s?.trim().toLowerCase();
@@ -32,14 +35,53 @@ async function runNpmGlobalInstall(version) {
32
35
  }
33
36
  export async function runUpgrade(options = {}) {
34
37
  const toVersion = options.toVersion?.trim();
38
+ if (detectInstallKind() === "managed") {
39
+ const current = readCurrentManagedVersion() ?? getVersion();
40
+ const latest = toVersion ?? (await fetchLatestVersion());
41
+ if (!latest) {
42
+ console.error(`Could not resolve latest version for ${PKG_NAME}.`);
43
+ return 1;
44
+ }
45
+ console.log(`Current: ${current}`);
46
+ console.log(`Latest: ${latest}`);
47
+ console.log("Install: managed");
48
+ if (!toVersion && compareSemver(latest, current) <= 0) {
49
+ console.log("Already on the latest version.");
50
+ return 0;
51
+ }
52
+ if (options.check) {
53
+ console.log("Update available. Run: agent-dealer upgrade");
54
+ return 0;
55
+ }
56
+ console.log(`Upgrading managed install ${PKG_NAME} → ${latest} ...`);
57
+ const result = await installCliVersionToPrefix(latest);
58
+ if (!result.ok) {
59
+ console.error(`Upgrade failed: ${result.error}`);
60
+ return 1;
61
+ }
62
+ activateVersion(latest);
63
+ console.log("Upgrade complete. Restart any running agent-dealer process.");
64
+ return 0;
65
+ }
35
66
  let version = toVersion ?? (await fetchLatestPublishedVersion(PKG_NAME));
36
67
  if (!version) {
37
68
  console.error(`Could not resolve latest version for ${PKG_NAME}.`);
38
69
  return 1;
39
70
  }
40
- const autoYes = options.yes ??
41
- parseBooleanLike(process.env.AGENT_DEALER_UPGRADE_YES) ??
42
- false;
71
+ console.log(`Current: ${getVersion()}`);
72
+ console.log(`Latest: ${version}`);
73
+ console.log("Install: npm-global (or unknown)");
74
+ if (options.check) {
75
+ if (compareSemver(version, getVersion()) > 0) {
76
+ console.log("Update available. Run: agent-dealer upgrade");
77
+ console.log("Tip: agent-dealer install # managed auto-updates (data unchanged)");
78
+ }
79
+ else {
80
+ console.log("Already on the latest version.");
81
+ }
82
+ return 0;
83
+ }
84
+ const autoYes = options.yes ?? parseBooleanLike(process.env.AGENT_DEALER_UPGRADE_YES) ?? false;
43
85
  if (!autoYes) {
44
86
  const ok = await confirmPrompt(`Upgrade ${PKG_NAME} to ${version} via "npm install -g ${PKG_NAME}@${version}"? [y/N] `);
45
87
  if (!ok) {
@@ -50,10 +92,17 @@ export async function runUpgrade(options = {}) {
50
92
  const code = await runNpmGlobalInstall(version);
51
93
  if (code === 0) {
52
94
  console.log(`Upgraded to ${PKG_NAME}@${version}. Re-run your command to use the new version.`);
95
+ console.log("Tip: agent-dealer install # switch CLI to managed auto-updates (data unchanged)");
53
96
  }
54
97
  return code;
55
98
  }
56
- // Used by update-check: prompts happen elsewhere, so we only do the install.
57
99
  export async function installAgentDealerVersion(version) {
100
+ if (detectInstallKind() === "managed") {
101
+ const result = await installCliVersionToPrefix(version);
102
+ if (!result.ok)
103
+ return 1;
104
+ activateVersion(version);
105
+ return 0;
106
+ }
58
107
  return runNpmGlobalInstall(version);
59
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dealer",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
4
  "description": "Human control plane for agent execution — queue, plan approval, audit",
5
5
  "license": "MIT",
6
6
  "type": "module",