openbroker 1.9.4 → 1.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to Open Broker will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ### Added
8
+ - Added an allowlisted `openbroker install <package>` command for optional companion packages, initially supporting `monitoring` (`openbroker-monitoring`) and `extended` (`openbroker-extended`). Re-running installs the latest release; `--tag`, `--dry`, and `--list` are supported.
9
+
5
10
  ## [1.9.3] - 2026-06-23
6
11
 
7
12
  ### Changed
package/README.md CHANGED
@@ -8,6 +8,18 @@ Hyperliquid trading CLI. Execute orders, manage positions, and run trading strat
8
8
  npm install -g openbroker
9
9
  ```
10
10
 
11
+ ### Companion packages
12
+
13
+ OpenBroker manages optional, allowlisted companion packages through the same CLI:
14
+
15
+ ```bash
16
+ openbroker install --list # Show supported packages
17
+ openbroker install monitoring # Local automation dashboard
18
+ openbroker install extended # Extended Exchange CLI
19
+ ```
20
+
21
+ The command installs globally through npm so each package's executable is available on your `PATH`. Re-run the same command to upgrade to the latest release, or pin a release with `--tag <version>`. Use `--dry` to preview the npm command.
22
+
11
23
  ### Codex: one-command install
12
24
 
13
25
  Install the Codex skill, persistent CLI, and restricted API-wallet onboarding flow:
@@ -38,6 +50,9 @@ openbroker search --query GOLD # Find markets
38
50
 
39
51
  ```bash
40
52
  npx openbroker@latest install --codex # Codex skill + CLI + API-wallet onboarding
53
+ openbroker install --list # List optional companion packages
54
+ openbroker install monitoring # Install or upgrade openbroker-monitoring
55
+ openbroker install extended # Install or upgrade openbroker-extended
41
56
  openbroker setup --api-wallet # Recommended: restricted agent wallet + browser approval
42
57
  openbroker setup # Interactive; API wallet is the default
43
58
  ```
package/SKILL.md CHANGED
@@ -301,7 +301,7 @@ Use `--no-ws` only for debugging or networks that cannot maintain WebSockets. In
301
301
  `openbroker-monitoring` is optional but useful for long-running automations, live debugging, and post-run inspection.
302
302
 
303
303
  ```bash
304
- npm install openbroker-monitoring
304
+ openbroker install monitoring
305
305
  openbroker auto run ./my-automation.ts --id my-auto
306
306
  openbroker-monitoring serve --host 127.0.0.1 --port 3001
307
307
  ```
package/bin/cli.ts CHANGED
@@ -11,7 +11,7 @@ const scriptsDir = path.resolve(__dirname, '../scripts');
11
11
 
12
12
  const commands: Record<string, { script: string; description: string }> = {
13
13
  // Setup
14
- 'install': { script: 'setup/install.ts', description: 'Install OpenBroker for an agent harness' },
14
+ 'install': { script: 'setup/install.ts', description: 'Install companion packages or an agent harness' },
15
15
  'setup': { script: 'setup/onboard.ts', description: 'Interactive setup wizard' },
16
16
  'onboard': { script: 'setup/onboard.ts', description: 'Interactive setup wizard' },
17
17
  'approve-builder': { script: 'setup/approve-builder.ts', description: 'Approve builder fee' },
@@ -69,7 +69,7 @@ Open Broker - Hyperliquid Trading CLI
69
69
  Usage: openbroker <command> [options]
70
70
 
71
71
  Setup:
72
- install Install OpenBroker for Codex or another agent harness
72
+ install Install companion packages or OpenBroker for an agent harness
73
73
  setup One-command setup (wallet + config + builder approval)
74
74
 
75
75
  Info Commands:
@@ -139,6 +139,8 @@ Utility:
139
139
 
140
140
  Examples:
141
141
  npx openbroker@latest install --codex # Install skill + CLI + API-wallet setup
142
+ openbroker install monitoring # Install or upgrade the automation dashboard
143
+ openbroker install extended # Install or upgrade the Extended Exchange CLI
142
144
  openbroker setup --api-wallet # Recommended restricted API-wallet setup
143
145
  openbroker account # View account info
144
146
  openbroker buy --coin ETH --size 0.1 # Market buy 0.1 ETH
@@ -5,19 +5,54 @@ import * as path from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { spawnSync } from 'child_process';
8
+ import { INSTALLABLE_PACKAGES, packageSpec, resolveInstallablePackage, } from './package-catalog.js';
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
10
11
  const packageRoot = path.resolve(__dirname, '../..');
11
- const args = new Set(process.argv.slice(2));
12
+ const rawArgs = process.argv.slice(2);
13
+ const args = new Set(rawArgs);
14
+ function positionalArgs() {
15
+ const positionals = [];
16
+ for (let index = 0; index < rawArgs.length; index++) {
17
+ const arg = rawArgs[index];
18
+ if (arg === '--tag') {
19
+ index++;
20
+ continue;
21
+ }
22
+ if (!arg.startsWith('-'))
23
+ positionals.push(arg);
24
+ }
25
+ return positionals;
26
+ }
27
+ function optionValue(flag) {
28
+ const index = rawArgs.indexOf(flag);
29
+ if (index < 0)
30
+ return null;
31
+ const value = rawArgs[index + 1];
32
+ if (!value || value.startsWith('-'))
33
+ fail(`${flag} requires a value`);
34
+ return value;
35
+ }
12
36
  function printUsage() {
13
37
  console.log(`
14
- OpenBroker Harness Installer
15
- ============================
38
+ OpenBroker Installer
39
+ ====================
16
40
 
17
41
  Usage:
42
+ openbroker install <package> [--tag <version>] [--dry]
43
+ openbroker install --list
18
44
  openbroker install --codex [options]
19
45
  npx openbroker@latest install --codex [options]
20
46
 
47
+ Companion packages:
48
+ monitoring Install the local automation dashboard
49
+ extended Install the Extended Exchange CLI
50
+
51
+ Package options:
52
+ --tag <tag> Install a release tag or exact version (default: latest)
53
+ --dry Print the npm command without installing
54
+ --list List supported companion packages
55
+
21
56
  Harnesses:
22
57
  --codex Install the OpenBroker skill for Codex
23
58
 
@@ -74,6 +109,55 @@ function installGlobalCli() {
74
109
  fail('global CLI installation failed. Fix the npm permission error, then rerun with --skip-cli.');
75
110
  }
76
111
  }
112
+ function printInstallablePackages() {
113
+ console.log('Installable OpenBroker packages:\n');
114
+ for (const entry of INSTALLABLE_PACKAGES) {
115
+ console.log(` ${entry.key.padEnd(12)} ${entry.packageName.padEnd(26)} ${entry.description}`);
116
+ }
117
+ console.log('\nInstall or upgrade with: openbroker install <package>');
118
+ }
119
+ function installCompanionPackage(target) {
120
+ const entry = resolveInstallablePackage(target);
121
+ if (!entry) {
122
+ printInstallablePackages();
123
+ fail(`unknown installable package: ${target}`);
124
+ }
125
+ const allowedFlags = new Set(['--tag', '--dry']);
126
+ const unsupported = rawArgs.filter((arg, index) => (arg.startsWith('-')
127
+ && !allowedFlags.has(arg)
128
+ && rawArgs[index - 1] !== '--tag'));
129
+ if (unsupported.length > 0)
130
+ fail(`unsupported package option: ${unsupported[0]}`);
131
+ const tag = optionValue('--tag') ?? 'latest';
132
+ let spec;
133
+ try {
134
+ spec = packageSpec(entry, tag);
135
+ }
136
+ catch (error) {
137
+ fail(error instanceof Error ? error.message : String(error));
138
+ }
139
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
140
+ const npmArgs = ['install', '--global', spec];
141
+ console.log(`OpenBroker — Install ${entry.key}`);
142
+ console.log('================================\n');
143
+ console.log(`Package: ${spec}`);
144
+ console.log(`Command: ${npmCommand} ${npmArgs.join(' ')}`);
145
+ if (args.has('--dry')) {
146
+ console.log('\nDry run only; nothing was installed.');
147
+ return;
148
+ }
149
+ const result = spawnSync(npmCommand, npmArgs, { stdio: 'inherit' });
150
+ if (result.error)
151
+ fail(`could not start npm: ${result.error.message}`);
152
+ if (result.status !== 0) {
153
+ fail(`installation failed for ${entry.packageName}; resolve the npm error and retry`);
154
+ }
155
+ console.log(`\n✅ ${entry.packageName} installed successfully.`);
156
+ console.log(`Available command: ${entry.command}`);
157
+ console.log('\nNext steps:');
158
+ for (const step of entry.nextSteps)
159
+ console.log(` ${step}`);
160
+ }
77
161
  function runApiWalletSetup() {
78
162
  const onboardPath = path.join(packageRoot, 'scripts', 'setup', 'onboard.ts');
79
163
  console.log('\nStarting restricted API-wallet onboarding...\n');
@@ -94,6 +178,19 @@ function main() {
94
178
  printUsage();
95
179
  return;
96
180
  }
181
+ if (args.has('--list')) {
182
+ printInstallablePackages();
183
+ return;
184
+ }
185
+ const positionals = positionalArgs();
186
+ if (positionals.length > 0) {
187
+ if (positionals.length > 1)
188
+ fail(`expected one package name, received: ${positionals.join(' ')}`);
189
+ if (args.has('--codex'))
190
+ fail('choose either a companion package or the --codex harness installer');
191
+ installCompanionPackage(positionals[0]);
192
+ return;
193
+ }
97
194
  if (!args.has('--codex')) {
98
195
  printUsage();
99
196
  fail('choose a supported harness flag such as --codex');
@@ -0,0 +1,12 @@
1
+ export interface InstallablePackage {
2
+ key: string;
3
+ aliases: string[];
4
+ packageName: string;
5
+ command: string;
6
+ description: string;
7
+ nextSteps: string[];
8
+ }
9
+ export declare const INSTALLABLE_PACKAGES: InstallablePackage[];
10
+ export declare function resolveInstallablePackage(input: string): InstallablePackage | null;
11
+ export declare function packageSpec(entry: InstallablePackage, tag?: string): string;
12
+ //# sourceMappingURL=package-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-catalog.d.ts","sourceRoot":"","sources":["../../scripts/setup/package-catalog.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,eAAO,MAAM,oBAAoB,EAAE,kBAAkB,EAuBpD,CAAC;AAEF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAOlF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,SAAW,GAAG,MAAM,CAK7E"}
@@ -0,0 +1,36 @@
1
+ export const INSTALLABLE_PACKAGES = [
2
+ {
3
+ key: 'monitoring',
4
+ aliases: ['openbroker-monitoring'],
5
+ packageName: 'openbroker-monitoring',
6
+ command: 'openbroker-monitoring',
7
+ description: 'Local automation dashboard and optional audit observer',
8
+ nextSteps: [
9
+ 'openbroker-monitoring serve --host 127.0.0.1 --port 3001',
10
+ 'Open http://127.0.0.1:3001',
11
+ ],
12
+ },
13
+ {
14
+ key: 'extended',
15
+ aliases: ['openbroker-extended'],
16
+ packageName: 'openbroker-extended',
17
+ command: 'openbroker-ex',
18
+ description: 'Extended Exchange trading CLI and library',
19
+ nextSteps: [
20
+ 'openbroker-ex --help',
21
+ 'openbroker-ex setup',
22
+ ],
23
+ },
24
+ ];
25
+ export function resolveInstallablePackage(input) {
26
+ const normalized = input.trim().toLowerCase();
27
+ return INSTALLABLE_PACKAGES.find((entry) => (entry.key === normalized
28
+ || entry.packageName === normalized
29
+ || entry.aliases.includes(normalized))) ?? null;
30
+ }
31
+ export function packageSpec(entry, tag = 'latest') {
32
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(tag)) {
33
+ throw new Error(`invalid package tag or version: ${tag}`);
34
+ }
35
+ return `${entry.packageName}@${tag}`;
36
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=package-catalog.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-catalog.test.d.ts","sourceRoot":"","sources":["../../scripts/setup/package-catalog.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,31 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { INSTALLABLE_PACKAGES, packageSpec, resolveInstallablePackage, } from './package-catalog.js';
6
+ const installScript = fileURLToPath(new URL('./install.ts', import.meta.url));
7
+ test('resolves companion packages by short name and npm package name', () => {
8
+ assert.equal(resolveInstallablePackage('monitoring')?.packageName, 'openbroker-monitoring');
9
+ assert.equal(resolveInstallablePackage('openbroker-monitoring')?.key, 'monitoring');
10
+ assert.equal(resolveInstallablePackage('EXTENDED')?.command, 'openbroker-ex');
11
+ assert.equal(resolveInstallablePackage('unknown'), null);
12
+ });
13
+ test('builds pinned or latest npm package specs without accepting arbitrary specs', () => {
14
+ const monitoring = INSTALLABLE_PACKAGES.find((entry) => entry.key === 'monitoring');
15
+ assert.ok(monitoring);
16
+ assert.equal(packageSpec(monitoring), 'openbroker-monitoring@latest');
17
+ assert.equal(packageSpec(monitoring, '1.4.2'), 'openbroker-monitoring@1.4.2');
18
+ assert.throws(() => packageSpec(monitoring, 'npm:unrelated-package'));
19
+ assert.throws(() => packageSpec(monitoring, '../local-package'));
20
+ });
21
+ test('installer dry run prints the global npm operation without writing', () => {
22
+ const result = spawnSync(process.execPath, ['--import', 'tsx', installScript, 'monitoring', '--tag', '1.4.2', '--dry'], { encoding: 'utf8' });
23
+ assert.equal(result.status, 0, result.stderr);
24
+ assert.match(result.stdout, /npm install --global openbroker-monitoring@1\.4\.2/);
25
+ assert.match(result.stdout, /nothing was installed/i);
26
+ });
27
+ test('installer rejects packages outside the catalog', () => {
28
+ const result = spawnSync(process.execPath, ['--import', 'tsx', installScript, 'unrelated-package', '--dry'], { encoding: 'utf8' });
29
+ assert.equal(result.status, 1);
30
+ assert.match(result.stderr, /unknown installable package/i);
31
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,10 +51,11 @@
51
51
  "funding-scan": "tsx scripts/info/funding-scan.ts",
52
52
  "outcomes": "tsx scripts/info/outcomes.ts",
53
53
  "build": "tsc",
54
- "prepublishOnly": "npm run build && npm run test:guardrails && npm run test:realtime && npm run test:cli",
54
+ "prepublishOnly": "npm run build && npm run test:guardrails && npm run test:realtime && npm run test:install && npm run test:cli",
55
55
  "test:cli": "node --import tsx bin/cli.ts --help",
56
56
  "test:guardrails": "node --import tsx --test scripts/auto/guardrails.test.ts",
57
- "test:realtime": "node --import tsx --test scripts/auto/realtime.test.ts"
57
+ "test:realtime": "node --import tsx --test scripts/auto/realtime.test.ts",
58
+ "test:install": "node --import tsx --test scripts/setup/package-catalog.test.ts"
58
59
  },
59
60
  "dependencies": {
60
61
  "@nktkas/hyperliquid": "^0.30.3",
@@ -6,21 +6,59 @@ import * as path from 'path';
6
6
  import { homedir } from 'os';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { spawnSync } from 'child_process';
9
+ import {
10
+ INSTALLABLE_PACKAGES,
11
+ packageSpec,
12
+ resolveInstallablePackage,
13
+ } from './package-catalog.js';
9
14
 
10
15
  const __filename = fileURLToPath(import.meta.url);
11
16
  const __dirname = path.dirname(__filename);
12
17
  const packageRoot = path.resolve(__dirname, '../..');
13
- const args = new Set(process.argv.slice(2));
18
+ const rawArgs = process.argv.slice(2);
19
+ const args = new Set(rawArgs);
20
+
21
+ function positionalArgs(): string[] {
22
+ const positionals: string[] = [];
23
+ for (let index = 0; index < rawArgs.length; index++) {
24
+ const arg = rawArgs[index];
25
+ if (arg === '--tag') {
26
+ index++;
27
+ continue;
28
+ }
29
+ if (!arg.startsWith('-')) positionals.push(arg);
30
+ }
31
+ return positionals;
32
+ }
33
+
34
+ function optionValue(flag: string): string | null {
35
+ const index = rawArgs.indexOf(flag);
36
+ if (index < 0) return null;
37
+ const value = rawArgs[index + 1];
38
+ if (!value || value.startsWith('-')) fail(`${flag} requires a value`);
39
+ return value;
40
+ }
14
41
 
15
42
  function printUsage(): void {
16
43
  console.log(`
17
- OpenBroker Harness Installer
18
- ============================
44
+ OpenBroker Installer
45
+ ====================
19
46
 
20
47
  Usage:
48
+ openbroker install <package> [--tag <version>] [--dry]
49
+ openbroker install --list
21
50
  openbroker install --codex [options]
22
51
  npx openbroker@latest install --codex [options]
23
52
 
53
+ Companion packages:
54
+ monitoring Install the local automation dashboard
55
+ extended Install the Extended Exchange CLI
56
+
57
+ Package options:
58
+ --tag <tag> Install a release tag or exact version (default: latest)
59
+ --dry Print the npm command without installing
60
+ --list List supported companion packages
61
+
24
62
  Harnesses:
25
63
  --codex Install the OpenBroker skill for Codex
26
64
 
@@ -93,6 +131,62 @@ function installGlobalCli(): void {
93
131
  }
94
132
  }
95
133
 
134
+ function printInstallablePackages(): void {
135
+ console.log('Installable OpenBroker packages:\n');
136
+ for (const entry of INSTALLABLE_PACKAGES) {
137
+ console.log(` ${entry.key.padEnd(12)} ${entry.packageName.padEnd(26)} ${entry.description}`);
138
+ }
139
+ console.log('\nInstall or upgrade with: openbroker install <package>');
140
+ }
141
+
142
+ function installCompanionPackage(target: string): void {
143
+ const entry = resolveInstallablePackage(target);
144
+ if (!entry) {
145
+ printInstallablePackages();
146
+ fail(`unknown installable package: ${target}`);
147
+ }
148
+
149
+ const allowedFlags = new Set(['--tag', '--dry']);
150
+ const unsupported = rawArgs.filter((arg, index) => (
151
+ arg.startsWith('-')
152
+ && !allowedFlags.has(arg)
153
+ && rawArgs[index - 1] !== '--tag'
154
+ ));
155
+ if (unsupported.length > 0) fail(`unsupported package option: ${unsupported[0]}`);
156
+
157
+ const tag = optionValue('--tag') ?? 'latest';
158
+ let spec: string;
159
+ try {
160
+ spec = packageSpec(entry, tag);
161
+ } catch (error) {
162
+ fail(error instanceof Error ? error.message : String(error));
163
+ }
164
+
165
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
166
+ const npmArgs = ['install', '--global', spec];
167
+
168
+ console.log(`OpenBroker — Install ${entry.key}`);
169
+ console.log('================================\n');
170
+ console.log(`Package: ${spec}`);
171
+ console.log(`Command: ${npmCommand} ${npmArgs.join(' ')}`);
172
+
173
+ if (args.has('--dry')) {
174
+ console.log('\nDry run only; nothing was installed.');
175
+ return;
176
+ }
177
+
178
+ const result = spawnSync(npmCommand, npmArgs, { stdio: 'inherit' });
179
+ if (result.error) fail(`could not start npm: ${result.error.message}`);
180
+ if (result.status !== 0) {
181
+ fail(`installation failed for ${entry.packageName}; resolve the npm error and retry`);
182
+ }
183
+
184
+ console.log(`\n✅ ${entry.packageName} installed successfully.`);
185
+ console.log(`Available command: ${entry.command}`);
186
+ console.log('\nNext steps:');
187
+ for (const step of entry.nextSteps) console.log(` ${step}`);
188
+ }
189
+
96
190
  function runApiWalletSetup(): void {
97
191
  const onboardPath = path.join(packageRoot, 'scripts', 'setup', 'onboard.ts');
98
192
 
@@ -121,6 +215,19 @@ function main(): void {
121
215
  return;
122
216
  }
123
217
 
218
+ if (args.has('--list')) {
219
+ printInstallablePackages();
220
+ return;
221
+ }
222
+
223
+ const positionals = positionalArgs();
224
+ if (positionals.length > 0) {
225
+ if (positionals.length > 1) fail(`expected one package name, received: ${positionals.join(' ')}`);
226
+ if (args.has('--codex')) fail('choose either a companion package or the --codex harness installer');
227
+ installCompanionPackage(positionals[0]);
228
+ return;
229
+ }
230
+
124
231
  if (!args.has('--codex')) {
125
232
  printUsage();
126
233
  fail('choose a supported harness flag such as --codex');
@@ -0,0 +1,50 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import {
6
+ INSTALLABLE_PACKAGES,
7
+ packageSpec,
8
+ resolveInstallablePackage,
9
+ } from './package-catalog.js';
10
+
11
+ const installScript = fileURLToPath(new URL('./install.ts', import.meta.url));
12
+
13
+ test('resolves companion packages by short name and npm package name', () => {
14
+ assert.equal(resolveInstallablePackage('monitoring')?.packageName, 'openbroker-monitoring');
15
+ assert.equal(resolveInstallablePackage('openbroker-monitoring')?.key, 'monitoring');
16
+ assert.equal(resolveInstallablePackage('EXTENDED')?.command, 'openbroker-ex');
17
+ assert.equal(resolveInstallablePackage('unknown'), null);
18
+ });
19
+
20
+ test('builds pinned or latest npm package specs without accepting arbitrary specs', () => {
21
+ const monitoring = INSTALLABLE_PACKAGES.find((entry) => entry.key === 'monitoring');
22
+ assert.ok(monitoring);
23
+ assert.equal(packageSpec(monitoring), 'openbroker-monitoring@latest');
24
+ assert.equal(packageSpec(monitoring, '1.4.2'), 'openbroker-monitoring@1.4.2');
25
+ assert.throws(() => packageSpec(monitoring, 'npm:unrelated-package'));
26
+ assert.throws(() => packageSpec(monitoring, '../local-package'));
27
+ });
28
+
29
+ test('installer dry run prints the global npm operation without writing', () => {
30
+ const result = spawnSync(
31
+ process.execPath,
32
+ ['--import', 'tsx', installScript, 'monitoring', '--tag', '1.4.2', '--dry'],
33
+ { encoding: 'utf8' },
34
+ );
35
+
36
+ assert.equal(result.status, 0, result.stderr);
37
+ assert.match(result.stdout, /npm install --global openbroker-monitoring@1\.4\.2/);
38
+ assert.match(result.stdout, /nothing was installed/i);
39
+ });
40
+
41
+ test('installer rejects packages outside the catalog', () => {
42
+ const result = spawnSync(
43
+ process.execPath,
44
+ ['--import', 'tsx', installScript, 'unrelated-package', '--dry'],
45
+ { encoding: 'utf8' },
46
+ );
47
+
48
+ assert.equal(result.status, 1);
49
+ assert.match(result.stderr, /unknown installable package/i);
50
+ });
@@ -0,0 +1,49 @@
1
+ export interface InstallablePackage {
2
+ key: string;
3
+ aliases: string[];
4
+ packageName: string;
5
+ command: string;
6
+ description: string;
7
+ nextSteps: string[];
8
+ }
9
+
10
+ export const INSTALLABLE_PACKAGES: InstallablePackage[] = [
11
+ {
12
+ key: 'monitoring',
13
+ aliases: ['openbroker-monitoring'],
14
+ packageName: 'openbroker-monitoring',
15
+ command: 'openbroker-monitoring',
16
+ description: 'Local automation dashboard and optional audit observer',
17
+ nextSteps: [
18
+ 'openbroker-monitoring serve --host 127.0.0.1 --port 3001',
19
+ 'Open http://127.0.0.1:3001',
20
+ ],
21
+ },
22
+ {
23
+ key: 'extended',
24
+ aliases: ['openbroker-extended'],
25
+ packageName: 'openbroker-extended',
26
+ command: 'openbroker-ex',
27
+ description: 'Extended Exchange trading CLI and library',
28
+ nextSteps: [
29
+ 'openbroker-ex --help',
30
+ 'openbroker-ex setup',
31
+ ],
32
+ },
33
+ ];
34
+
35
+ export function resolveInstallablePackage(input: string): InstallablePackage | null {
36
+ const normalized = input.trim().toLowerCase();
37
+ return INSTALLABLE_PACKAGES.find((entry) => (
38
+ entry.key === normalized
39
+ || entry.packageName === normalized
40
+ || entry.aliases.includes(normalized)
41
+ )) ?? null;
42
+ }
43
+
44
+ export function packageSpec(entry: InstallablePackage, tag = 'latest'): string {
45
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(tag)) {
46
+ throw new Error(`invalid package tag or version: ${tag}`);
47
+ }
48
+ return `${entry.packageName}@${tag}`;
49
+ }