moneyos 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -26,6 +26,10 @@ moneyos auth unlock
26
26
  moneyos auth lock
27
27
  moneyos auth status
28
28
  moneyos auth change-password
29
+ moneyos add <tool>
30
+ moneyos remove <tool>
31
+ moneyos tools
32
+ moneyos swap <amount> <tokenIn> <tokenOut> [--chain <id>] [--provider odos]
29
33
  moneyos backup export [--out ./wallet-backup.json] [--force]
30
34
  moneyos backup restore <path> [--force]
31
35
  moneyos backup status
@@ -41,8 +45,9 @@ Example:
41
45
  ```bash
42
46
  moneyos init
43
47
  moneyos auth unlock
44
- moneyos balance USDC
45
- moneyos backup status
48
+ moneyos add swap
49
+ moneyos tools
50
+ moneyos swap 0.1 RYZE ETH
46
51
  ```
47
52
 
48
53
  ## SDK
@@ -62,14 +67,48 @@ const tx = await moneyos.send("USDC", "0x...", "10");
62
67
  console.log(tx.hash);
63
68
  ```
64
69
 
70
+ If you want a local script or workflow to reuse an already-unlocked MoneyOS
71
+ session, unlock first in the terminal:
72
+
73
+ ```bash
74
+ moneyos auth unlock
75
+ ```
76
+
77
+ Then attach that session explicitly in code:
78
+
79
+ ```ts
80
+ import { createMoneyOS, connectLocalSession } from "moneyos";
81
+
82
+ const execute = await connectLocalSession();
83
+ const moneyos = createMoneyOS({
84
+ chainId: 42161,
85
+ execute,
86
+ });
87
+ ```
88
+
89
+ Tool packages should still execute against `moneyos.runtime`; they should not
90
+ import `connectLocalSession()` themselves.
91
+
65
92
  The runtime seam is intentionally small. `createMoneyOS` can also take injected
66
93
  `execute`, `read`, and `assets` implementations, which is how external packages
67
94
  plug in.
68
95
 
69
- Swap is no longer built into the root SDK or CLI. The canonical implementation
70
- lives in `@moneyos/swap`, which executes against `moneyos.runtime`.
96
+ Swap still lives in the separate `@moneyos/swap` package, but the root CLI now
97
+ owns first-class tool install/use UX for CLI-integrated packages.
98
+
99
+ For CLI usage, install the root package and then add tools into the user-level
100
+ MoneyOS tool home:
101
+
102
+ ```bash
103
+ npm install moneyos
104
+ moneyos init
105
+ moneyos auth unlock
106
+ moneyos add swap
107
+ moneyos tools
108
+ moneyos swap 0.1 RYZE ETH
109
+ ```
71
110
 
72
- Install it alongside the root package:
111
+ For direct package usage, install the tool package alongside `moneyos`:
73
112
 
74
113
  ```bash
75
114
  npm install moneyos @moneyos/swap
@@ -108,8 +147,9 @@ Published packages:
108
147
  - `@moneyos/core`
109
148
  - `@moneyos/swap`
110
149
 
111
- `moneyos@0.4.0` no longer bundles swap into the root SDK or CLI. If you want
112
- swap, install `@moneyos/swap` alongside `moneyos`.
150
+ Current `moneyos` releases no longer bundle swap into the root SDK or CLI. If
151
+ you want swap from the root CLI, install `moneyos` and then run
152
+ `moneyos add swap`.
113
153
 
114
154
  ## Current wallet model
115
155
 
@@ -119,6 +159,7 @@ What is landed in code today:
119
159
  - `~/.moneyos/config.json` now stores only non-secret settings such as chain and RPC configuration
120
160
  - `MONEYOS_PRIVATE_KEY` remains an explicit override for ephemeral CI or agent runs
121
161
  - `moneyos auth unlock` opens a short-lived local session for write commands
162
+ - workflow scripts can attach to that unlocked session with `connectLocalSession()`
122
163
  - `moneyos auth change-password` rotates the local wallet password and locks the current session
123
164
  - `moneyos backup export|restore|status` manages encrypted wallet backups
124
165
  - Normal wallet commands resolve their write path through one shared session-aware flow
package/dist/cli/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { Command as Command7 } from "commander";
4
+ import { Command as Command10 } from "commander";
5
+ import { realpathSync } from "fs";
5
6
  import { resolve as resolve3 } from "path";
6
7
  import { fileURLToPath as fileURLToPath2 } from "url";
7
8
 
@@ -510,6 +511,15 @@ function saveConfig(config, path = CONFIG_FILE) {
510
511
  function getConfigPath() {
511
512
  return CONFIG_FILE;
512
513
  }
514
+ function getToolHomeDir() {
515
+ return join3(CONFIG_DIR, "tools");
516
+ }
517
+ function getToolHomePackageJsonPath() {
518
+ return join3(getToolHomeDir(), "package.json");
519
+ }
520
+ function getToolRegistryPath() {
521
+ return join3(getToolHomeDir(), "registry.json");
522
+ }
513
523
  function getWalletPath(config) {
514
524
  return config?.walletPath ?? join3(CONFIG_DIR, "wallet.json");
515
525
  }
@@ -1441,6 +1451,43 @@ import {
1441
1451
  parseUnits,
1442
1452
  encodeFunctionData
1443
1453
  } from "viem";
1454
+
1455
+ // src/core/assets.ts
1456
+ var DefaultAssetRegistry = class {
1457
+ nativeTokenAddress = NATIVE_TOKEN_ADDRESS;
1458
+ getToken = getToken;
1459
+ getTokenAddress = getTokenAddress;
1460
+ getChain = getChain;
1461
+ };
1462
+
1463
+ // src/core/no-executor.ts
1464
+ var NO_SIGNING_ACCOUNT_ERROR = "No signing account configured. Set `signer`, `privateKey`, or `execute` in MoneyOS config.";
1465
+ function throwNoSigningAccount() {
1466
+ throw new Error(NO_SIGNING_ACCOUNT_ERROR);
1467
+ }
1468
+ function createNoExecutorClient() {
1469
+ return {
1470
+ mode: "eoa",
1471
+ getAddress() {
1472
+ return throwNoSigningAccount();
1473
+ },
1474
+ async send(_call) {
1475
+ return throwNoSigningAccount();
1476
+ },
1477
+ async sendBatch(_calls) {
1478
+ return throwNoSigningAccount();
1479
+ },
1480
+ capabilities() {
1481
+ return {
1482
+ sponsoredGas: false,
1483
+ batching: false,
1484
+ simulation: false
1485
+ };
1486
+ }
1487
+ };
1488
+ }
1489
+
1490
+ // src/core/client.ts
1444
1491
  var ERC20_ABI = [
1445
1492
  {
1446
1493
  name: "balanceOf",
@@ -1474,12 +1521,6 @@ var ERC20_ABI = [
1474
1521
  outputs: [{ name: "", type: "string" }]
1475
1522
  }
1476
1523
  ];
1477
- var DefaultAssetRegistry = class {
1478
- nativeTokenAddress = NATIVE_TOKEN_ADDRESS;
1479
- getToken = getToken;
1480
- getTokenAddress = getTokenAddress;
1481
- getChain = getChain;
1482
- };
1483
1524
  var MoneyOS = class {
1484
1525
  read;
1485
1526
  executor;
@@ -1525,9 +1566,7 @@ var MoneyOS = class {
1525
1566
  }
1526
1567
  requireExecutor() {
1527
1568
  if (!this.executor) {
1528
- throw new Error(
1529
- "No signing account configured. Set `signer`, `privateKey`, or `execute` in MoneyOS config."
1530
- );
1569
+ throw new Error(NO_SIGNING_ACCOUNT_ERROR);
1531
1570
  }
1532
1571
  return this.executor;
1533
1572
  }
@@ -1604,6 +1643,23 @@ var MoneyOS = class {
1604
1643
  }
1605
1644
  };
1606
1645
 
1646
+ // src/local-session.ts
1647
+ async function connectLocalSession(options = {}) {
1648
+ const socketPath = options.socketPath ?? getSessionSocketPath();
1649
+ const tokenPath = options.tokenPath ?? getSessionTokenPath();
1650
+ const session = await getSessionStatus(socketPath, tokenPath);
1651
+ if (!session) {
1652
+ throw new Error(
1653
+ "No active local MoneyOS session found. Run `moneyos auth unlock` locally first."
1654
+ );
1655
+ }
1656
+ return new SessionExecutionClient({
1657
+ socketPath,
1658
+ tokenPath,
1659
+ address: session.address
1660
+ });
1661
+ }
1662
+
1607
1663
  // src/cli/wallet.ts
1608
1664
  function resolveEnvPrivateKey(explicit) {
1609
1665
  return explicit ?? process.env.MONEYOS_PRIVATE_KEY;
@@ -1664,16 +1720,15 @@ async function buildCliMoneyOSConfig(config, options = {}) {
1664
1720
  }
1665
1721
  const socketPath = getSessionPath(options);
1666
1722
  const tokenPath = getTokenPath(options);
1667
- const session = await getSessionStatus(socketPath, tokenPath);
1668
- if (session) {
1723
+ try {
1669
1724
  return {
1670
1725
  ...moneyosConfig,
1671
- execute: new SessionExecutionClient({
1726
+ execute: await connectLocalSession({
1672
1727
  socketPath,
1673
- tokenPath,
1674
- address: session.address
1728
+ tokenPath
1675
1729
  })
1676
1730
  };
1731
+ } catch {
1677
1732
  }
1678
1733
  const wallet = getWalletStore(config, options);
1679
1734
  if (wallet.exists()) {
@@ -2088,21 +2143,341 @@ backupCommand.command("status").description("Show the local wallet backup status
2088
2143
  console.log(formatBackupStatus(status));
2089
2144
  });
2090
2145
 
2146
+ // src/cli/commands/tools.ts
2147
+ import { Command as Command9 } from "commander";
2148
+
2149
+ // src/cli/tools/manager.ts
2150
+ import { spawn as spawn2 } from "child_process";
2151
+ import { createRequire } from "module";
2152
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2153
+ import { dirname as dirname4, join as join4 } from "path";
2154
+ import { pathToFileURL } from "url";
2155
+ import { Command as Command8, CommanderError } from "commander";
2156
+
2157
+ // src/cli/tools/runtime.ts
2158
+ import { Command as Command7 } from "commander";
2159
+ var defaultCreateMoneyOSCliContextDependencies = {
2160
+ loadConfig,
2161
+ connectLocalSession,
2162
+ createReadClient: ({ chainId, rpcUrl }) => new ViemReadClient({
2163
+ defaultChainId: chainId,
2164
+ rpcUrl
2165
+ }),
2166
+ createAssets: () => new DefaultAssetRegistry()
2167
+ };
2168
+ function createMoneyOSCliContext(deps = defaultCreateMoneyOSCliContextDependencies) {
2169
+ return {
2170
+ Command: Command7,
2171
+ async getRuntime(options = {}) {
2172
+ const config = deps.loadConfig();
2173
+ const chainId = options.chainId ?? config.chainId ?? 42161;
2174
+ const rpcUrl = config.rpcUrl;
2175
+ return {
2176
+ read: deps.createReadClient({ chainId, rpcUrl }),
2177
+ execute: options.requireSession ? await deps.connectLocalSession() : createNoExecutorClient(),
2178
+ assets: deps.createAssets(),
2179
+ config: {
2180
+ defaultChainId: chainId,
2181
+ rpcUrl
2182
+ }
2183
+ };
2184
+ }
2185
+ };
2186
+ }
2187
+
2188
+ // src/cli/tools/manager.ts
2189
+ var RESERVED_ROOT_COMMANDS = /* @__PURE__ */ new Set(["init", "auth", "backup", "balance", "send", "keystore", "add", "remove", "tools", "help", "__session-daemon"]);
2190
+ var FIRST_PARTY_TOOL_ALIASES = { swap: "@moneyos/swap" };
2191
+ var SHARED_TOOL_HOME_DEPENDENCIES = { "@moneyos/core": "^0.1.0", viem: "^2.45.1" };
2192
+ var VALID_COMMAND_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
2193
+ var getPaths = () => ({
2194
+ rootDir: getToolHomeDir(),
2195
+ packageJsonPath: getToolHomePackageJsonPath(),
2196
+ registryPath: getToolRegistryPath()
2197
+ });
2198
+ function ensureToolHome(paths) {
2199
+ if (!existsSync5(paths.rootDir)) mkdirSync4(paths.rootDir, { recursive: true, mode: 448 });
2200
+ const exists = existsSync5(paths.packageJsonPath);
2201
+ const parsed = exists ? JSON.parse(readFileSync4(paths.packageJsonPath, "utf8")) : void 0;
2202
+ const packageJson = {
2203
+ name: parsed?.name ?? "moneyos-tools",
2204
+ private: parsed?.private ?? true,
2205
+ description: parsed?.description ?? "User-level installed MoneyOS CLI tools",
2206
+ dependencies: { ...parsed?.dependencies ?? {} }
2207
+ };
2208
+ let changed = !exists;
2209
+ for (const [name, version2] of Object.entries(SHARED_TOOL_HOME_DEPENDENCIES)) {
2210
+ if (packageJson.dependencies[name] !== version2) {
2211
+ packageJson.dependencies[name] = version2;
2212
+ changed = true;
2213
+ }
2214
+ }
2215
+ if (changed) writeFileSync3(paths.packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
2216
+ `, { mode: 384 });
2217
+ if (!existsSync5(paths.registryPath)) writeFileSync3(paths.registryPath, "[]\n", { mode: 384 });
2218
+ }
2219
+ function parseRegistryEntry(value) {
2220
+ if (typeof value !== "object" || value === null) throw new Error("Invalid tool registry entry.");
2221
+ const entry = value;
2222
+ if (typeof entry.packageName !== "string" || typeof entry.packageVersion !== "string" || entry.toolVersion !== 1 || typeof entry.name !== "string" || typeof entry.description !== "string" || !Array.isArray(entry.commandPath) || entry.commandPath.length === 0 || entry.commandPath.some((segment) => typeof segment !== "string" || !VALID_COMMAND_SEGMENT.test(segment))) throw new Error("Invalid tool registry entry.");
2223
+ return { packageName: entry.packageName, packageVersion: entry.packageVersion, toolVersion: 1, name: entry.name, commandPath: [...entry.commandPath], description: entry.description };
2224
+ }
2225
+ function parseLoadedTool(packageName, packageVersion, value) {
2226
+ if (typeof value !== "object" || value === null) throw new Error(`Package ${packageName} does not export \`moneyosCliTool\`.`);
2227
+ const tool = value;
2228
+ if (tool.version !== 1 || typeof tool.name !== "string" || typeof tool.description !== "string" || !Array.isArray(tool.commandPath) || tool.commandPath.length === 0 || tool.commandPath.some((segment) => typeof segment !== "string" || !VALID_COMMAND_SEGMENT.test(segment)) || typeof tool.createCommand !== "function") throw new Error(`Package ${packageName} exports an invalid \`moneyosCliTool\`.`);
2229
+ return { packageName, packageVersion, toolVersion: 1, name: tool.name, commandPath: [...tool.commandPath], description: tool.description, createCommand: tool.createCommand };
2230
+ }
2231
+ function readRegistry(paths) {
2232
+ ensureToolHome(paths);
2233
+ const parsed = JSON.parse(readFileSync4(paths.registryPath, "utf8"));
2234
+ if (!Array.isArray(parsed)) throw new Error(`Tool registry at ${paths.registryPath} is invalid.`);
2235
+ return parsed.map(parseRegistryEntry);
2236
+ }
2237
+ function writeRegistry(paths, entries) {
2238
+ ensureToolHome(paths);
2239
+ writeFileSync3(paths.registryPath, `${JSON.stringify(entries, null, 2)}
2240
+ `, { mode: 384 });
2241
+ }
2242
+ function getConflict(entry, entries) {
2243
+ const path = entry.commandPath.join(" ");
2244
+ if (RESERVED_ROOT_COMMANDS.has(entry.name) || RESERVED_ROOT_COMMANDS.has(entry.commandPath[0])) {
2245
+ return `reserved root command collision for ${path}`;
2246
+ }
2247
+ for (const other of entries) {
2248
+ if (other.packageName === entry.packageName) continue;
2249
+ const otherPath = other.commandPath.join(" ");
2250
+ if (path === otherPath || path.startsWith(`${otherPath} `) || otherPath.startsWith(`${path} `)) {
2251
+ return `command path ${path} collides with installed tool path ${otherPath}`;
2252
+ }
2253
+ }
2254
+ }
2255
+ async function runNpm(paths, args) {
2256
+ await new Promise((resolve4, reject) => {
2257
+ const child = spawn2("npm", args, { cwd: paths.rootDir, stdio: "pipe" });
2258
+ let stderr = "";
2259
+ child.stderr.on("data", (chunk) => {
2260
+ stderr += chunk.toString();
2261
+ });
2262
+ child.on("error", reject);
2263
+ child.on("close", (code) => code === 0 ? resolve4() : reject(new Error(stderr.trim() || `npm ${args[0]} failed with exit code ${String(code)}.`)));
2264
+ });
2265
+ }
2266
+ async function loadInstalledToolFromFs(paths, packageName) {
2267
+ const toolRequire = createRequire(paths.packageJsonPath);
2268
+ let entryPath;
2269
+ try {
2270
+ entryPath = toolRequire.resolve(packageName);
2271
+ } catch {
2272
+ throw new Error(`Package ${packageName} is missing from ${join4(paths.rootDir, "node_modules")}.`);
2273
+ }
2274
+ for (let root = dirname4(entryPath); root !== dirname4(root); root = dirname4(root)) {
2275
+ const packageJsonPath2 = join4(root, "package.json");
2276
+ if (!existsSync5(packageJsonPath2)) continue;
2277
+ const packageJson = JSON.parse(readFileSync4(packageJsonPath2, "utf8"));
2278
+ if (packageJson.name === packageName && typeof packageJson.version === "string") {
2279
+ const moduleNamespace = await import(pathToFileURL(entryPath).href);
2280
+ return parseLoadedTool(
2281
+ packageJson.name,
2282
+ packageJson.version,
2283
+ moduleNamespace.moneyosCliTool ?? moduleNamespace.default?.moneyosCliTool
2284
+ );
2285
+ }
2286
+ }
2287
+ throw new Error(`Could not find package.json for installed package ${packageName}.`);
2288
+ }
2289
+ function createCliToolManager(params = {}) {
2290
+ const paths = params.paths ?? getPaths();
2291
+ const cliContext = params.cliContext ?? createMoneyOSCliContext();
2292
+ const install = params.packageManager?.install ?? ((toolPaths, spec) => runNpm(toolPaths, ["install", "--save-exact", "--no-fund", "--no-audit", spec]));
2293
+ const uninstall = params.packageManager?.uninstall ?? ((toolPaths, packageName) => runNpm(toolPaths, ["uninstall", "--no-fund", "--no-audit", packageName]));
2294
+ const loadInstalledTool = params.moduleLoader ? async (toolPaths, packageName) => {
2295
+ const loaded = await params.moduleLoader(toolPaths, packageName);
2296
+ return parseLoadedTool(loaded.packageName, loaded.packageVersion, loaded.cliTool);
2297
+ } : loadInstalledToolFromFs;
2298
+ const sameMetadata = (left, right) => left.packageName === right.packageName && left.packageVersion === right.packageVersion && left.toolVersion === right.toolVersion && left.name === right.name && left.description === right.description && left.commandPath.join("\0") === right.commandPath.join("\0");
2299
+ const resolveInstalledTool = (input, entries) => entries.find((entry) => entry.packageName === input) ?? entries.find((entry) => entry.packageName === FIRST_PARTY_TOOL_ALIASES[input]) ?? (() => {
2300
+ const matches = entries.filter((entry) => entry.name === input || entry.commandPath.join(" ") === input);
2301
+ return matches.length === 1 ? matches[0] : void 0;
2302
+ })();
2303
+ async function invoke(entry, args) {
2304
+ let command;
2305
+ try {
2306
+ const loaded = await loadInstalledTool(paths, entry.packageName);
2307
+ if (!sameMetadata(entry, loaded)) throw new Error("installed metadata does not match registry");
2308
+ command = loaded.createCommand(cliContext);
2309
+ if (!(command instanceof Command8) || command.name() !== entry.commandPath[entry.commandPath.length - 1]) {
2310
+ throw new Error("createCommand() did not return the expected command");
2311
+ }
2312
+ command.exitOverride();
2313
+ } catch (error) {
2314
+ const message = error instanceof Error ? error.message : String(error);
2315
+ throw new Error(`Installed tool ${entry.commandPath.join(" ")} is broken: ${message}. Run \`moneyos add ${entry.packageName}\` to repair it or \`moneyos remove ${entry.packageName}\` to uninstall it.`);
2316
+ }
2317
+ try {
2318
+ await command.parseAsync([process.execPath, entry.commandPath[entry.commandPath.length - 1], ...args]);
2319
+ } catch (error) {
2320
+ if (error instanceof CommanderError) {
2321
+ if (error.code !== "commander.helpDisplayed") process.exitCode = error.exitCode || 1;
2322
+ return;
2323
+ }
2324
+ throw error;
2325
+ }
2326
+ }
2327
+ return {
2328
+ getRegistryEntries() {
2329
+ return readRegistry(paths);
2330
+ },
2331
+ mountInstalledToolCommands(program) {
2332
+ let entries;
2333
+ try {
2334
+ entries = readRegistry(paths);
2335
+ } catch {
2336
+ return;
2337
+ }
2338
+ const groups = /* @__PURE__ */ new Map();
2339
+ for (const entry of entries) {
2340
+ if (getConflict(entry, entries)) continue;
2341
+ let parent = program;
2342
+ const segments = [];
2343
+ for (const segment of entry.commandPath.slice(0, -1)) {
2344
+ segments.push(segment);
2345
+ const key = segments.join(" ");
2346
+ const group = groups.get(key) ?? new Command8(segment).description("Installed MoneyOS tool commands");
2347
+ if (!groups.has(key)) {
2348
+ groups.set(key, group);
2349
+ parent.addCommand(group);
2350
+ }
2351
+ parent = group;
2352
+ }
2353
+ parent.addCommand(
2354
+ new Command8(entry.commandPath[entry.commandPath.length - 1]).description(entry.description).allowUnknownOption(true).allowExcessArguments(true).helpOption(false).argument("[args...]").action(async (args) => {
2355
+ await invoke(entry, args);
2356
+ })
2357
+ );
2358
+ }
2359
+ },
2360
+ async addTool(input) {
2361
+ const spec = FIRST_PARTY_TOOL_ALIASES[input] ?? input;
2362
+ const entries = readRegistry(paths);
2363
+ const previous = resolveInstalledTool(input, entries);
2364
+ ensureToolHome(paths);
2365
+ const before = JSON.parse(readFileSync4(paths.packageJsonPath, "utf8")).dependencies ?? {};
2366
+ await install(paths, spec);
2367
+ const after = JSON.parse(readFileSync4(paths.packageJsonPath, "utf8")).dependencies ?? {};
2368
+ const changed = previous?.packageName ?? Object.keys(after).find((name) => before[name] !== after[name] && !(name in SHARED_TOOL_HOME_DEPENDENCIES));
2369
+ try {
2370
+ const loaded = await loadInstalledTool(paths, changed ?? spec);
2371
+ const next = previous ? entries.filter((entry2) => entry2.packageName !== previous.packageName) : entries;
2372
+ const conflict = getConflict(loaded, next);
2373
+ if (conflict) throw new Error(conflict);
2374
+ const entry = { packageName: loaded.packageName, packageVersion: loaded.packageVersion, toolVersion: loaded.toolVersion, name: loaded.name, commandPath: loaded.commandPath, description: loaded.description };
2375
+ writeRegistry(paths, [...next, entry]);
2376
+ return entry;
2377
+ } catch (error) {
2378
+ try {
2379
+ if (previous) await install(paths, `${previous.packageName}@${previous.packageVersion}`);
2380
+ else if (changed) await uninstall(paths, changed);
2381
+ } catch {
2382
+ }
2383
+ throw error;
2384
+ }
2385
+ },
2386
+ async removeTool(input) {
2387
+ const entries = readRegistry(paths);
2388
+ const entry = resolveInstalledTool(input, entries);
2389
+ if (!entry) throw new Error(`Tool ${input} is not installed.`);
2390
+ await uninstall(paths, entry.packageName);
2391
+ writeRegistry(paths, entries.filter((installed) => installed.packageName !== entry.packageName));
2392
+ return entry;
2393
+ },
2394
+ async listTools() {
2395
+ const entries = readRegistry(paths);
2396
+ const tools = await Promise.all(entries.map(async (entry) => {
2397
+ const conflict = getConflict(entry, entries);
2398
+ if (conflict) return { ...entry, state: "conflict", problems: [conflict] };
2399
+ try {
2400
+ const loaded = await loadInstalledTool(paths, entry.packageName);
2401
+ if (!sameMetadata(entry, loaded)) throw new Error("installed metadata does not match registry");
2402
+ return { ...entry, state: "ok", problems: [] };
2403
+ } catch (error) {
2404
+ return { ...entry, state: "broken", problems: [error instanceof Error ? error.message : String(error)] };
2405
+ }
2406
+ }));
2407
+ return tools.sort((left, right) => left.commandPath.join(" ").localeCompare(right.commandPath.join(" ")));
2408
+ }
2409
+ };
2410
+ }
2411
+ function formatToolStatusTable(tools) {
2412
+ if (tools.length === 0) return "No tools installed.";
2413
+ const commandWidth = Math.max("COMMAND".length, ...tools.map((tool) => tool.commandPath.join(" ").length));
2414
+ const packageWidth = Math.max("PACKAGE".length, ...tools.map((tool) => tool.packageName.length));
2415
+ const versionWidth = Math.max("VERSION".length, ...tools.map((tool) => tool.packageVersion.length));
2416
+ const lines = [`${"COMMAND".padEnd(commandWidth)} ${"PACKAGE".padEnd(packageWidth)} ${"VERSION".padEnd(versionWidth)} STATE`];
2417
+ for (const tool of tools) {
2418
+ lines.push(`${tool.commandPath.join(" ").padEnd(commandWidth)} ${tool.packageName.padEnd(packageWidth)} ${tool.packageVersion.padEnd(versionWidth)} ${tool.state}`);
2419
+ for (const problem of tool.problems) lines.push(` ${problem}`);
2420
+ }
2421
+ return lines.join("\n");
2422
+ }
2423
+
2424
+ // src/cli/commands/tools.ts
2425
+ function formatError(error) {
2426
+ return error instanceof Error ? error.message : String(error);
2427
+ }
2428
+ function createAddToolCommand(toolManager) {
2429
+ return new Command9("add").description("Install or update a MoneyOS CLI tool into the user tool home").argument("<tool>", "Tool alias or npm package spec").action(async (tool) => {
2430
+ try {
2431
+ const entry = await toolManager.addTool(tool);
2432
+ console.log(
2433
+ `Installed ${entry.packageName}@${entry.packageVersion} as \`moneyos ${entry.commandPath.join(" ")}\`.`
2434
+ );
2435
+ } catch (error) {
2436
+ console.error(formatError(error));
2437
+ process.exitCode = 1;
2438
+ }
2439
+ });
2440
+ }
2441
+ function createRemoveToolCommand(toolManager) {
2442
+ return new Command9("remove").description("Remove an installed MoneyOS CLI tool from the user tool home").argument("<tool>", "Installed tool name, command path, or npm package").action(async (tool) => {
2443
+ try {
2444
+ const entry = await toolManager.removeTool(tool);
2445
+ console.log(
2446
+ `Removed ${entry.packageName} from \`moneyos ${entry.commandPath.join(" ")}\`.`
2447
+ );
2448
+ } catch (error) {
2449
+ console.error(formatError(error));
2450
+ process.exitCode = 1;
2451
+ }
2452
+ });
2453
+ }
2454
+ function createToolsCommand(toolManager) {
2455
+ return new Command9("tools").description("List installed MoneyOS CLI tools from the registry").action(async () => {
2456
+ try {
2457
+ console.log(formatToolStatusTable(await toolManager.listTools()));
2458
+ } catch (error) {
2459
+ console.error(formatError(error));
2460
+ process.exitCode = 1;
2461
+ }
2462
+ });
2463
+ }
2464
+
2091
2465
  // src/cli/version.ts
2092
- import { readFileSync as readFileSync4 } from "fs";
2466
+ import { readFileSync as readFileSync5 } from "fs";
2093
2467
  import { fileURLToPath } from "url";
2094
- import { dirname as dirname4, resolve as resolve2 } from "path";
2468
+ import { dirname as dirname5, resolve as resolve2 } from "path";
2095
2469
  var packageJsonPath = resolve2(
2096
- dirname4(fileURLToPath(import.meta.url)),
2470
+ dirname5(fileURLToPath(import.meta.url)),
2097
2471
  "../../package.json"
2098
2472
  );
2099
2473
  var version = JSON.parse(
2100
- readFileSync4(packageJsonPath, "utf8")
2474
+ readFileSync5(packageJsonPath, "utf8")
2101
2475
  ).version;
2102
2476
 
2103
2477
  // src/cli/index.ts
2104
- function createProgram() {
2105
- const program = new Command7();
2478
+ function createProgram(options = {}) {
2479
+ const program = new Command10();
2480
+ const toolManager = options.toolManager ?? createCliToolManager();
2106
2481
  program.name("moneyos").description("The operating system for money").version(version);
2107
2482
  program.addCommand(initCommand);
2108
2483
  program.addCommand(balanceCommand);
@@ -2110,20 +2485,33 @@ function createProgram() {
2110
2485
  program.addCommand(keystoreCommand);
2111
2486
  program.addCommand(authCommand);
2112
2487
  program.addCommand(backupCommand);
2488
+ program.addCommand(createAddToolCommand(toolManager));
2489
+ program.addCommand(createRemoveToolCommand(toolManager));
2490
+ program.addCommand(createToolsCommand(toolManager));
2491
+ toolManager.mountInstalledToolCommands(program);
2113
2492
  program.command("__session-daemon", { hidden: true }).action(async () => {
2114
2493
  await runSessionDaemonProcess();
2115
2494
  });
2116
2495
  return program;
2117
2496
  }
2118
- var cliEntry = process.argv[1];
2119
- var isEntrypoint = typeof cliEntry === "string" && resolve3(cliEntry) === fileURLToPath2(import.meta.url);
2120
- if (isEntrypoint) {
2497
+ function resolveEntrypointPath(path) {
2498
+ try {
2499
+ return realpathSync(path);
2500
+ } catch {
2501
+ return resolve3(path);
2502
+ }
2503
+ }
2504
+ function isEntrypointPath(cliEntry, moduleUrl) {
2505
+ return typeof cliEntry === "string" && resolveEntrypointPath(cliEntry) === fileURLToPath2(moduleUrl);
2506
+ }
2507
+ if (isEntrypointPath(process.argv[1], import.meta.url)) {
2121
2508
  void createProgram().parseAsync(process.argv).catch((error) => {
2122
2509
  console.error(error instanceof Error ? error.message : String(error));
2123
2510
  process.exitCode = 1;
2124
2511
  });
2125
2512
  }
2126
2513
  export {
2127
- createProgram
2514
+ createProgram,
2515
+ isEntrypointPath
2128
2516
  };
2129
2517
  //# sourceMappingURL=index.js.map