@claude-flow/cli 3.32.2 → 3.32.4

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.
Files changed (38) hide show
  1. package/.claude/helpers/helpers.manifest.json +3 -3
  2. package/.claude/helpers/statusline.cjs +38 -38
  3. package/catalog-manifest.json +2 -2
  4. package/dist/src/auth/client.d.ts +89 -0
  5. package/dist/src/auth/client.js +242 -0
  6. package/dist/src/auth/constants.d.ts +7 -0
  7. package/dist/src/auth/constants.js +7 -0
  8. package/dist/src/auth/scopes.d.ts +14 -0
  9. package/dist/src/auth/scopes.js +21 -0
  10. package/dist/src/auth/security-bridge.d.ts +36 -0
  11. package/dist/src/auth/security-bridge.js +42 -0
  12. package/dist/src/auth/session.d.ts +20 -0
  13. package/dist/src/auth/session.js +32 -0
  14. package/dist/src/auth/state.d.ts +19 -0
  15. package/dist/src/auth/state.js +53 -0
  16. package/dist/src/auth/types.d.ts +27 -0
  17. package/dist/src/auth/types.js +11 -0
  18. package/dist/src/commands/auth.d.ts +15 -0
  19. package/dist/src/commands/auth.js +244 -0
  20. package/dist/src/commands/doctor.js +211 -4
  21. package/dist/src/commands/index.js +2 -0
  22. package/dist/src/commands/proxy-lifecycle.d.ts +12 -0
  23. package/dist/src/commands/proxy-lifecycle.js +232 -0
  24. package/dist/src/commands/proxy.js +92 -4
  25. package/dist/src/proxy/install.d.ts +29 -0
  26. package/dist/src/proxy/install.js +135 -0
  27. package/dist/src/proxy/lifecycle.d.ts +61 -0
  28. package/dist/src/proxy/lifecycle.js +249 -0
  29. package/dist/src/proxy/paths.d.ts +34 -0
  30. package/dist/src/proxy/paths.js +70 -0
  31. package/dist/src/proxy/release.d.ts +47 -0
  32. package/dist/src/proxy/release.js +138 -0
  33. package/dist/src/proxy/token-bridge.d.ts +5 -0
  34. package/dist/src/proxy/token-bridge.js +61 -0
  35. package/dist/src/proxy/verify.d.ts +44 -0
  36. package/dist/src/proxy/verify.js +68 -0
  37. package/package.json +2 -2
  38. package/plugins/ruflo-metaharness/scripts/smoke.sh +11 -11
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `ruflo proxy install|start|stop|status|logs|update|uninstall` (ADR-307) —
3
+ * the full lifecycle command set, kept in its own file so
4
+ * src/commands/proxy.ts (the ADR-313/314/315 consent subcommands) stays
5
+ * under the repo's 500-line-per-file convention. Merged into one
6
+ * `proxyCommand` in proxy.ts.
7
+ */
8
+ import type { Command } from '../types.js';
9
+ /** Pinned and reviewed; later upgrades remain explicit commands. */
10
+ export declare const DEFAULT_PROXY_RELEASE = "0.4.0";
11
+ export declare const proxyLifecycleSubcommands: Command[];
12
+ //# sourceMappingURL=proxy-lifecycle.d.ts.map
@@ -0,0 +1,232 @@
1
+ /**
2
+ * `ruflo proxy install|start|stop|status|logs|update|uninstall` (ADR-307) —
3
+ * the full lifecycle command set, kept in its own file so
4
+ * src/commands/proxy.ts (the ADR-313/314/315 consent subcommands) stays
5
+ * under the repo's 500-line-per-file convention. Merged into one
6
+ * `proxyCommand` in proxy.ts.
7
+ */
8
+ import { output } from '../output.js';
9
+ import { hasConsent, recordConsent, revokeConsent } from '../funnel/index.js';
10
+ import { installProxy, uninstallProxy } from '../proxy/install.js';
11
+ import { startForeground, startBackground, stopProxy, getProxyStatus, readProxyLogTail, watchProxyLog, ProxyNotInstalledError, ProxyAlreadyRunningError, } from '../proxy/lifecycle.js';
12
+ import { proxyTokenPath } from '../proxy/paths.js';
13
+ import { removeInjectedToken, startTokenRefreshPump } from '../proxy/token-bridge.js';
14
+ /** Pinned and reviewed; later upgrades remain explicit commands. */
15
+ export const DEFAULT_PROXY_RELEASE = '0.4.0';
16
+ const INSTALL_DISCLOSURE = [
17
+ 'Installing the Meta LLM Proxy (ADR-304/307).',
18
+ '',
19
+ 'This downloads a separately-released Rust binary (Ed25519-signature and',
20
+ 'checksum verified before anything is written to disk) and runs it as a',
21
+ 'local process bound to 127.0.0.1 only. The proxy routes to LOCAL',
22
+ 'backends by default — no prompt leaves this machine. Cloud routing is a',
23
+ 'separate, explicit opt-in (`ruflo proxy config --cloud`), never enabled',
24
+ 'by install alone.',
25
+ '',
26
+ 'Uninstall anytime: ruflo proxy uninstall',
27
+ ].join('\n');
28
+ const installSub = {
29
+ name: 'install',
30
+ description: 'Download, verify, and install the meta-proxy binary (ADR-307)',
31
+ options: [
32
+ // NOT named 'version' — index.ts:107 globally intercepts any --version/-v
33
+ // anywhere in argv (`if (flags.version || flags.V) { showVersion(); return; }`)
34
+ // BEFORE subcommand dispatch, regardless of which command defines it. A
35
+ // subcommand-local --version is silently swallowed by the CLI's own
36
+ // `ruflo --version` handling — confirmed the hard way in E2E testing.
37
+ { name: 'release', description: `Release version to install (default: ${DEFAULT_PROXY_RELEASE})`, type: 'string' },
38
+ { name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
39
+ ],
40
+ action: async (ctx) => {
41
+ // Resolve the reviewed default before the consent gate. An explicit empty
42
+ // override still fails below without recording a consent receipt.
43
+ const version = typeof ctx.flags.release === 'string' ? ctx.flags.release : DEFAULT_PROXY_RELEASE;
44
+ if (!version) {
45
+ output.printError('ruflo proxy install requires --release <x.y.z> — there is no version-discovery ' +
46
+ 'endpoint yet (see the plan doc for the tracked follow-up). Find the latest at ' +
47
+ 'the release channel and pass it explicitly.');
48
+ return { success: false, exitCode: 1 };
49
+ }
50
+ if (!hasConsent('proxy-install')) {
51
+ output.writeln(INSTALL_DISCLOSURE);
52
+ output.writeln('');
53
+ const confirmed = Boolean(ctx.flags.yes);
54
+ if (!confirmed) {
55
+ output.writeln(`Re-run with --yes to confirm: ruflo proxy install --yes (installs ${version})`);
56
+ return { success: true, data: { confirmed: false } };
57
+ }
58
+ recordConsent('proxy-install', true, 'proxy-install');
59
+ }
60
+ try {
61
+ const spinner = output.createSpinner({ text: `Installing meta-proxy ${version}...`, spinner: 'dots' });
62
+ spinner.start();
63
+ const result = await installProxy({ version, log: (line) => spinner.setText(line) });
64
+ spinner.succeed(`meta-proxy ${version} installed`);
65
+ output.writeln(` binary: ${result.binaryPath}`);
66
+ output.writeln(` sha256: ${result.sha256}`);
67
+ return { success: true, data: result };
68
+ }
69
+ catch (e) {
70
+ const message = e instanceof Error ? e.message : String(e);
71
+ output.printError('Install failed', message);
72
+ return { success: false, message, exitCode: 1 };
73
+ }
74
+ },
75
+ };
76
+ const updateSub = {
77
+ name: 'update',
78
+ description: 'Re-verify and replace the installed binary with a specific version (never automatic)',
79
+ options: [{ name: 'release', description: 'Release version to install', type: 'string', required: true }],
80
+ action: async (ctx) => {
81
+ const version = typeof ctx.flags.release === 'string' ? ctx.flags.release : undefined;
82
+ if (!version) {
83
+ output.printError('ruflo proxy update requires --release <x.y.z>');
84
+ return { success: false, exitCode: 1 };
85
+ }
86
+ try {
87
+ const spinner = output.createSpinner({ text: `Updating meta-proxy to ${version}...`, spinner: 'dots' });
88
+ spinner.start();
89
+ const result = await installProxy({ version, log: (line) => spinner.setText(line) });
90
+ spinner.succeed(`meta-proxy updated to ${version}`);
91
+ output.writeln(` binary: ${result.binaryPath}`);
92
+ return { success: true, data: result };
93
+ }
94
+ catch (e) {
95
+ const message = e instanceof Error ? e.message : String(e);
96
+ output.printError('Update failed', message);
97
+ return { success: false, message, exitCode: 1 };
98
+ }
99
+ },
100
+ };
101
+ const startSub = {
102
+ name: 'start',
103
+ description: 'Start meta-proxy (foreground by default; --service to detach)',
104
+ options: [
105
+ { name: 'service', description: 'Run detached (background), survives terminal close but not a reboot — full OS-service registration is not yet implemented', type: 'boolean', default: false },
106
+ ],
107
+ action: async (ctx) => {
108
+ const service = Boolean(ctx.flags.service);
109
+ try {
110
+ if (service) {
111
+ const { pid } = await startBackground();
112
+ output.printSuccess(`meta-proxy started in the background (pid ${pid})`);
113
+ output.writeln(' Note: survives this terminal closing, but not a reboot — OS-service registration is not yet implemented.');
114
+ output.writeln(' Logs: ruflo proxy logs');
115
+ return { success: true, data: { pid } };
116
+ }
117
+ output.writeln('Starting meta-proxy in the foreground — press Ctrl+C to stop.');
118
+ await startTokenRefreshPump();
119
+ await startForeground(); // never returns normally
120
+ return { success: true };
121
+ }
122
+ catch (e) {
123
+ if (e instanceof ProxyNotInstalledError || e instanceof ProxyAlreadyRunningError) {
124
+ output.printError(e.message);
125
+ return { success: false, message: e.message, exitCode: 1 };
126
+ }
127
+ const message = e instanceof Error ? e.message : String(e);
128
+ output.printError('Failed to start meta-proxy', message);
129
+ return { success: false, message, exitCode: 1 };
130
+ }
131
+ },
132
+ };
133
+ const superviseSub = {
134
+ name: 'supervise',
135
+ description: 'Internal detached supervisor for token refresh and meta-proxy lifecycle',
136
+ action: async () => {
137
+ await startTokenRefreshPump();
138
+ await startForeground(true);
139
+ return { success: true };
140
+ },
141
+ };
142
+ const stopSub = {
143
+ name: 'stop',
144
+ description: 'Stop a running meta-proxy process',
145
+ action: async () => {
146
+ const result = await stopProxy();
147
+ removeInjectedToken();
148
+ if (!result.wasRunning) {
149
+ output.writeln('meta-proxy was not running.');
150
+ return { success: true, data: result };
151
+ }
152
+ output.printSuccess(`meta-proxy stopped (was pid ${result.pid})`);
153
+ return { success: true, data: result };
154
+ },
155
+ };
156
+ const statusSub = {
157
+ name: 'status',
158
+ description: 'Show meta-proxy install + process status',
159
+ options: [{ name: 'json', description: 'Machine-readable output', type: 'boolean', default: false }],
160
+ action: async (ctx) => {
161
+ const status = getProxyStatus();
162
+ if (ctx.flags.json) {
163
+ output.printJson(status);
164
+ return { success: true, data: status };
165
+ }
166
+ output.writeln(`Installed: ${status.installed ? 'yes' : 'no'}`);
167
+ output.writeln(`Running: ${status.running ? `yes (pid ${status.pid})` : 'no'}`);
168
+ if (status.stalePidFile)
169
+ output.writeln(' (a stale PID file was found and will be cleared on next start)');
170
+ if (!status.installed)
171
+ output.writeln(`Run: ruflo proxy install --yes (installs ${DEFAULT_PROXY_RELEASE})`);
172
+ else if (!status.running)
173
+ output.writeln('Run: ruflo proxy start');
174
+ return { success: true, data: status };
175
+ },
176
+ };
177
+ const logsSub = {
178
+ name: 'logs',
179
+ description: 'Show meta-proxy --service logs',
180
+ options: [
181
+ { name: 'follow', short: 'f', description: 'Stream new log lines as they arrive', type: 'boolean', default: false },
182
+ ],
183
+ action: async (ctx) => {
184
+ if (ctx.flags.follow) {
185
+ output.writeln('Following meta-proxy logs — press Ctrl+C to stop.');
186
+ try {
187
+ const watcher = watchProxyLog((line) => output.writeln(line));
188
+ await new Promise((resolve) => {
189
+ process.on('SIGINT', () => {
190
+ watcher.close();
191
+ resolve();
192
+ });
193
+ });
194
+ return { success: true };
195
+ }
196
+ catch (e) {
197
+ const message = e instanceof Error ? e.message : String(e);
198
+ output.printError(message);
199
+ return { success: false, message, exitCode: 1 };
200
+ }
201
+ }
202
+ const tail = readProxyLogTail();
203
+ if (!tail) {
204
+ output.writeln('No log content yet — meta-proxy has never been started in --service mode.');
205
+ return { success: true, data: { empty: true } };
206
+ }
207
+ output.writeln(tail);
208
+ return { success: true };
209
+ },
210
+ };
211
+ const uninstallSub = {
212
+ name: 'uninstall',
213
+ description: 'Stop the proxy (if running), remove the binary, token, and consent receipt',
214
+ action: async () => {
215
+ const status = getProxyStatus();
216
+ if (status.running) {
217
+ await stopProxy();
218
+ output.writeln('Stopped the running meta-proxy process.');
219
+ }
220
+ const removed = await uninstallProxy();
221
+ const { existsSync, unlinkSync } = await import('node:fs');
222
+ const tokenPath = proxyTokenPath();
223
+ if (existsSync(tokenPath))
224
+ unlinkSync(tokenPath);
225
+ removeInjectedToken();
226
+ revokeConsent('proxy-install', 'proxy-uninstall');
227
+ output.printSuccess(removed ? 'meta-proxy uninstalled.' : 'Nothing was installed — cleaned up any leftover state.');
228
+ return { success: true, data: { removed } };
229
+ },
230
+ };
231
+ export const proxyLifecycleSubcommands = [installSub, updateSub, startSub, superviseSub, stopSub, statusSub, logsSub, uninstallSub];
232
+ //# sourceMappingURL=proxy-lifecycle.js.map
@@ -23,6 +23,7 @@ import { clearRateLimitStatus, readRateLimitStatus } from '../funnel/rate-limit-
23
23
  import { clearQuotaLowStatus, readQuotaLowStatus } from '../funnel/power-saver-notifier.js';
24
24
  import { getInstalledCliVersion } from '../init/helper-refresh.js';
25
25
  import * as path from 'path';
26
+ import { proxyLifecycleSubcommands } from './proxy-lifecycle.js';
26
27
  const PROXY_CONFIG_FILE = 'proxy-config.toml';
27
28
  /**
28
29
  * Minimal hand-rolled TOML writer — this config has exactly one boolean
@@ -40,12 +41,12 @@ function readProxyConfigRaw() {
40
41
  return '';
41
42
  }
42
43
  }
43
- function writeConsentMirrorLine(field, value) {
44
+ function writeConfigLine(field, rawValue) {
44
45
  const dir = funnelStateDir();
45
46
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
46
47
  const target = path.join(dir, PROXY_CONFIG_FILE);
47
48
  const raw = readProxyConfigRaw();
48
- const line = `${field} = ${value}`;
49
+ const line = `${field} = ${rawValue}`;
49
50
  const pattern = new RegExp(`^${field}\\s*=.*$`, 'm');
50
51
  let next;
51
52
  if (pattern.test(raw)) {
@@ -58,6 +59,9 @@ function writeConsentMirrorLine(field, value) {
58
59
  fs.writeFileSync(tmp, next, { encoding: 'utf-8', mode: 0o600 });
59
60
  fs.renameSync(tmp, target);
60
61
  }
62
+ function writeConsentMirrorLine(field, value) {
63
+ writeConfigLine(field, String(value));
64
+ }
61
65
  function writeSponsoredConsentMirror(granted) {
62
66
  writeConsentMirrorLine('sponsored_consent_granted', granted);
63
67
  }
@@ -67,6 +71,78 @@ function writePowerSaverConsentMirror(granted) {
67
71
  function writeTrainingShareConsentMirror(granted) {
68
72
  writeConsentMirrorLine('training_share_consent_granted', granted);
69
73
  }
74
+ /**
75
+ * `default_data_plane` — the ADR-304 cloud-routing toggle. Values confirmed
76
+ * against meta-proxy's actual `DataPlane` enum (`src/config.rs`,
77
+ * `#[serde(rename_all = "snake_case")]`): "local" | "cloud" | "sponsored" |
78
+ * "passthrough" (the last two are not written by this command — sponsored
79
+ * is ADR-313's own consent flag, passthrough is the proxy's own default).
80
+ */
81
+ function readDataPlane() {
82
+ const raw = readProxyConfigRaw();
83
+ const match = raw.match(/^default_data_plane\s*=\s*"([^"]*)"/m);
84
+ return match ? match[1] : 'passthrough'; // matches the Rust struct's own default
85
+ }
86
+ function writeDataPlane(plane) {
87
+ writeConfigLine('default_data_plane', `"${plane}"`);
88
+ }
89
+ const CLOUD_ROUTING_DISCLOSURE = [
90
+ 'Enabling cloud routing.',
91
+ '',
92
+ 'With cloud routing ON, prompts for cloud-tier requests are sent to',
93
+ 'api.cognitum.one and forwarded to the selected provider',
94
+ '(Claude / GPT / Gemini / DeepSeek / OpenRouter).',
95
+ '',
96
+ 'Requests routed to local backends never leave this machine.',
97
+ '',
98
+ 'Disable anytime: ruflo proxy config --local-only',
99
+ ].join('\n');
100
+ const configSub = {
101
+ name: 'config',
102
+ description: 'Toggle cloud routing (ADR-304) — local backends only by default',
103
+ options: [
104
+ { name: 'cloud', description: 'Enable cloud routing (requires cloud-routing consent)', type: 'boolean', default: false },
105
+ { name: 'local-only', description: 'Disable cloud routing, revert to local-only routing', type: 'boolean', default: false },
106
+ { name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
107
+ ],
108
+ action: async (ctx) => {
109
+ const wantCloud = Boolean(ctx.flags.cloud);
110
+ const wantLocalOnly = Boolean(ctx.flags.localOnly ?? ctx.flags['local-only']);
111
+ if (wantCloud && wantLocalOnly) {
112
+ output.printError('Pass either --cloud or --local-only, not both.');
113
+ return { success: false, exitCode: 1 };
114
+ }
115
+ if (!wantCloud && !wantLocalOnly) {
116
+ const plane = readDataPlane();
117
+ output.writeln(`Current data plane: ${plane}`);
118
+ output.writeln(plane === 'cloud'
119
+ ? 'Cloud routing is ON — cloud-tier requests go to api.cognitum.one.'
120
+ : 'Cloud routing is OFF — requests never leave this machine (or use your own Claude subscription on Passthrough).');
121
+ return { success: true, data: { plane } };
122
+ }
123
+ if (wantLocalOnly) {
124
+ writeDataPlane('local');
125
+ revokeConsent('cloud-routing', 'proxy-config-local-only');
126
+ output.printSuccess('Cloud routing disabled — reverted to local-only routing.');
127
+ return { success: true, data: { plane: 'local' } };
128
+ }
129
+ // wantCloud
130
+ if (!hasConsent('cloud-routing')) {
131
+ output.writeln(CLOUD_ROUTING_DISCLOSURE);
132
+ output.writeln('');
133
+ if (!ctx.flags.yes) {
134
+ output.writeln('Re-run with --yes to confirm: ruflo proxy config --cloud --yes');
135
+ return { success: true, data: { confirmed: false } };
136
+ }
137
+ recordConsent('cloud-routing', true, 'proxy-config-cloud');
138
+ }
139
+ writeDataPlane('cloud');
140
+ output.printSuccess('Cloud routing enabled.');
141
+ output.writeln(' Requests routed to local backends still never leave this machine.');
142
+ output.writeln(' Disable anytime: ruflo proxy config --local-only');
143
+ return { success: true, data: { plane: 'cloud' } };
144
+ },
145
+ };
70
146
  const SPONSOR_DISCLOSURE = [
71
147
  'Enabling sponsored downtime mode.',
72
148
  '',
@@ -292,19 +368,31 @@ const trainingShareStatusSub = {
292
368
  };
293
369
  export const proxyCommand = {
294
370
  name: 'proxy',
295
- description: 'Meta LLM Proxy — sponsored downtime + power saver + training-data sharing (ADR-304/307/313/314/315)',
371
+ description: 'Meta LLM Proxy — install/lifecycle + sponsored downtime + power saver + training-data sharing (ADR-304/307/313/314/315)',
296
372
  subcommands: [
373
+ ...proxyLifecycleSubcommands,
374
+ configSub,
297
375
  sponsorEnableSub, sponsorDisableSub, sponsorStatusSub, sponsorClearSub,
298
376
  powerSaverEnableSub, powerSaverDisableSub, powerSaverStatusSub, powerSaverClearSub,
299
377
  trainingShareEnableSub, trainingShareDisableSub, trainingShareStatusSub,
300
378
  ],
301
379
  examples: [
380
+ { command: 'ruflo proxy install --yes', description: 'Install the signed Meta-Proxy v0.4.0 binary' },
381
+ { command: 'ruflo proxy start', description: 'Start meta-proxy in the foreground' },
382
+ { command: 'ruflo proxy status', description: 'Show install + process status' },
383
+ { command: 'ruflo proxy config --cloud --yes', description: 'Enable cloud routing (ADR-304)' },
384
+ { command: 'ruflo proxy config --local-only', description: 'Revert to local-only routing' },
302
385
  { command: 'ruflo proxy sponsor-status', description: 'Show current sponsored-mode state' },
303
386
  { command: 'ruflo proxy sponsor-enable --yes', description: 'Opt into sponsored downtime capacity' },
304
387
  { command: 'ruflo proxy power-saver-enable --yes', description: 'Opt into power saver mode' },
305
388
  { command: 'ruflo proxy training-share-enable --yes', description: 'Opt into training-data sharing (ADR-315)' },
306
389
  ],
307
- action: sponsorStatusSub.action,
390
+ action: async (ctx) => {
391
+ const { getProxyStatus } = await import('../proxy/lifecycle.js');
392
+ const status = getProxyStatus();
393
+ output.writeln(`Installed: ${status.installed ? 'yes' : 'no'}; Running: ${status.running ? `yes (pid ${status.pid})` : 'no'}`);
394
+ return sponsorStatusSub.action(ctx);
395
+ },
308
396
  };
309
397
  export default proxyCommand;
310
398
  //# sourceMappingURL=proxy.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `ruflo proxy install`/`update` orchestration (ADR-307): download -> verify
3
+ * -> extract -> place -> record. The per-user bearer token
4
+ * (`~/.ruflo/proxy-token`) is NOT generated here — confirmed empirically
5
+ * (2026-07-16) that the meta-proxy binary itself creates it on first launch
6
+ * (`load_or_create_token()`), so this module's job ends at a verified binary
7
+ * on disk plus an install manifest doctor can check against.
8
+ *
9
+ * @module proxy/install
10
+ */
11
+ export declare class ExtractionError extends Error {
12
+ constructor(message: string);
13
+ }
14
+ export interface InstallOptions {
15
+ version: string;
16
+ log?: (line: string) => void;
17
+ }
18
+ export interface InstallResult {
19
+ version: string;
20
+ binaryPath: string;
21
+ sha256: string;
22
+ }
23
+ /**
24
+ * Full install pipeline. Refuses (throws) on any verification failure —
25
+ * never writes a partially-verified binary into the live install path.
26
+ */
27
+ export declare function installProxy(opts: InstallOptions): Promise<InstallResult>;
28
+ export declare function uninstallProxy(): Promise<boolean>;
29
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `ruflo proxy install`/`update` orchestration (ADR-307): download -> verify
3
+ * -> extract -> place -> record. The per-user bearer token
4
+ * (`~/.ruflo/proxy-token`) is NOT generated here — confirmed empirically
5
+ * (2026-07-16) that the meta-proxy binary itself creates it on first launch
6
+ * (`load_or_create_token()`), so this module's job ends at a verified binary
7
+ * on disk plus an install manifest doctor can check against.
8
+ *
9
+ * @module proxy/install
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as os from 'node:os';
13
+ import * as path from 'node:path';
14
+ import { fetchReleaseAssets, detectTargetTriple, releaseArchiveExtension, releaseAssetFilename } from './release.js';
15
+ import { verifyRelease, sha256Hex, PROXY_RELEASE_PUBKEY_PEM } from './verify.js';
16
+ import { proxyBinaryPath, proxyInstallManifestPath } from './paths.js';
17
+ export class ExtractionError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = 'ExtractionError';
21
+ }
22
+ }
23
+ function binaryNameInArchive() {
24
+ return process.platform === 'win32' ? 'meta-proxy.exe' : 'meta-proxy';
25
+ }
26
+ /**
27
+ * Extracts the archive via the OS's own tools — `tar` for `.tar.gz`
28
+ * (present on macOS/Linux/Windows 10+), PowerShell `Expand-Archive`
29
+ * specifically for `.zip` on Windows (not tar's bsdtar zip support — not
30
+ * reliable enough to lean on). Zero new archive-parsing dependency, matching
31
+ * this repo's existing taste for shelling out over adding a parser dep.
32
+ */
33
+ async function extractArchive(archivePath, extractDir, ext) {
34
+ const { SafeExecutor } = await import('@claude-flow/security');
35
+ fs.mkdirSync(extractDir, { recursive: true });
36
+ if (ext === 'tar.gz') {
37
+ const exec = new SafeExecutor({ allowedCommands: ['tar'], timeout: 60_000 });
38
+ const result = await exec.execute('tar', ['xzf', archivePath, '-C', extractDir]);
39
+ if (result.exitCode !== 0) {
40
+ throw new ExtractionError(`tar extraction failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
41
+ }
42
+ return;
43
+ }
44
+ // .zip — PowerShell Expand-Archive, single-quoted literal paths (doubling
45
+ // any embedded single quote per PowerShell string-literal escaping) passed
46
+ // as ONE argv element to -Command. shell:false means no OS shell ever
47
+ // tokenizes this string — only powershell.exe's own parser does.
48
+ const escape = (p) => p.replace(/'/g, "''");
49
+ const command = `Expand-Archive -LiteralPath '${escape(archivePath)}' -DestinationPath '${escape(extractDir)}' -Force`;
50
+ const exec = new SafeExecutor({ allowedCommands: ['powershell', 'powershell.exe'], timeout: 60_000 });
51
+ const result = await exec.execute('powershell', ['-NoProfile', '-NonInteractive', '-Command', command]);
52
+ if (result.exitCode !== 0) {
53
+ throw new ExtractionError(`Expand-Archive failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
54
+ }
55
+ }
56
+ /**
57
+ * Full install pipeline. Refuses (throws) on any verification failure —
58
+ * never writes a partially-verified binary into the live install path.
59
+ */
60
+ export async function installProxy(opts) {
61
+ const log = opts.log ?? (() => { });
62
+ const triple = detectTargetTriple();
63
+ const archiveFilename = releaseAssetFilename(opts.version, triple);
64
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruflo-proxy-install-'));
65
+ try {
66
+ log(`Fetching meta-proxy ${opts.version} (${triple})...`);
67
+ const assets = await fetchReleaseAssets(opts.version, triple, workDir, log);
68
+ log('Verifying release signature and checksum...');
69
+ const { sha256 } = verifyRelease({
70
+ sumsBytes: assets.sumsBytes,
71
+ sigBase64: assets.sigBase64,
72
+ assetBytes: assets.archiveBytes,
73
+ assetFilename: assets.archiveFilename,
74
+ });
75
+ log(`Verified — sha256 ${sha256.slice(0, 16)}…`);
76
+ // fetchReleaseAssets's dev (gh) path already wrote the archive to workDir
77
+ // under archiveFilename; ensure it's there regardless of source so
78
+ // extraction always has a real file to operate on.
79
+ const archivePath = path.join(workDir, archiveFilename);
80
+ if (!fs.existsSync(archivePath)) {
81
+ fs.writeFileSync(archivePath, assets.archiveBytes);
82
+ }
83
+ const extractDir = path.join(workDir, 'extracted');
84
+ await extractArchive(archivePath, extractDir, releaseArchiveExtension(triple));
85
+ const extractedBinaryPath = path.join(extractDir, binaryNameInArchive());
86
+ if (!fs.existsSync(extractedBinaryPath)) {
87
+ throw new ExtractionError(`archive did not contain the expected binary at its root: ${binaryNameInArchive()}`);
88
+ }
89
+ // Defense in depth: confirm the extracted binary genuinely resolves
90
+ // inside extractDir (catches a symlink swap or similar), even though
91
+ // we only ever read one specific expected relative path, never an
92
+ // archive-listed one (so "zip slip" via arbitrary archive paths isn't
93
+ // reachable here in the first place).
94
+ const { PathValidator } = await import('@claude-flow/security');
95
+ const validator = new PathValidator({ allowedPrefixes: [extractDir] });
96
+ const validation = await validator.validate(extractedBinaryPath);
97
+ if (!validation.isValid) {
98
+ throw new ExtractionError(`extracted binary path failed validation: ${validation.errors.join('; ') || 'unknown'}`);
99
+ }
100
+ const finalPath = proxyBinaryPath();
101
+ fs.mkdirSync(path.dirname(finalPath), { recursive: true, mode: 0o700 });
102
+ const tmp = `${finalPath}.tmp`;
103
+ fs.copyFileSync(extractedBinaryPath, tmp);
104
+ fs.chmodSync(tmp, 0o755);
105
+ fs.renameSync(tmp, finalPath);
106
+ const liveSha = sha256Hex(fs.readFileSync(finalPath));
107
+ const manifest = {
108
+ version: opts.version,
109
+ sha256: liveSha,
110
+ verifiedAt: new Date().toISOString(),
111
+ pubkeyFingerprint: sha256Hex(Buffer.from(PROXY_RELEASE_PUBKEY_PEM)).slice(0, 16),
112
+ };
113
+ fs.mkdirSync(path.dirname(proxyInstallManifestPath()), { recursive: true });
114
+ fs.writeFileSync(proxyInstallManifestPath(), JSON.stringify(manifest, null, 2), { mode: 0o600 });
115
+ log(`meta-proxy ${opts.version} installed at ${finalPath}`);
116
+ return { version: opts.version, binaryPath: finalPath, sha256: liveSha };
117
+ }
118
+ finally {
119
+ fs.rmSync(workDir, { recursive: true, force: true });
120
+ }
121
+ }
122
+ export async function uninstallProxy() {
123
+ const binPath = proxyBinaryPath();
124
+ const existed = fs.existsSync(binPath);
125
+ if (existed)
126
+ fs.unlinkSync(binPath);
127
+ try {
128
+ fs.unlinkSync(proxyInstallManifestPath());
129
+ }
130
+ catch {
131
+ /* absent — fine */
132
+ }
133
+ return existed;
134
+ }
135
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * meta-proxy process lifecycle (ADR-307) — start/stop/status/logs.
3
+ *
4
+ * Adapts daemon.ts's proven pattern (PID file, O_EXCL lockfile for atomic
5
+ * check-then-start, signal-0 liveness, SIGTERM->1000ms->SIGKILL) to a
6
+ * native binary instead of a forked Node process. The binary itself takes
7
+ * no CLI flags — confirmed empirically (2026-07-16): `meta-proxy.exe` has
8
+ * no `--version`/`--help`, and any invocation just starts the server reading
9
+ * its own config file — so `spawn()` here passes zero arguments, always.
10
+ *
11
+ * Foreground `start` (the ADR-307 default) uses `stdio: 'inherit'` and
12
+ * blocks directly — simplest and safest, no log-file redirection needed.
13
+ * `start --service` needs REAL file-descriptor redirection
14
+ * (`stdio: ['ignore', fd, fd]`) + `detached: true` + `unref()` via
15
+ * `child_process.spawn()` directly — `SafeExecutor.executeStreaming()`
16
+ * buffers output in-process, which is wrong for a process meant to outlive
17
+ * the `ruflo` invocation that started it.
18
+ *
19
+ * @module proxy/lifecycle
20
+ */
21
+ import * as fs from 'node:fs';
22
+ export declare class ProxyNotInstalledError extends Error {
23
+ constructor();
24
+ }
25
+ export declare class ProxyAlreadyRunningError extends Error {
26
+ readonly pid: number;
27
+ constructor(pid: number);
28
+ }
29
+ export interface ProxyStatus {
30
+ installed: boolean;
31
+ running: boolean;
32
+ pid: number | null;
33
+ stalePidFile: boolean;
34
+ }
35
+ export declare function getProxyStatus(): ProxyStatus;
36
+ /**
37
+ * Foreground start (ADR-307 default) — blocks the caller until the process
38
+ * exits or is interrupted. `stdio: 'inherit'` passes the proxy's own output
39
+ * straight through to the terminal; signals (Ctrl+C) propagate naturally to
40
+ * the child, no manual forwarding needed.
41
+ */
42
+ export declare function startForeground(supervised?: boolean): Promise<never>;
43
+ /**
44
+ * Background/`--service` start — detaches so the process outlives this
45
+ * `ruflo` invocation, redirecting stdout/stderr to a real log file (not
46
+ * buffered in-process). Returns once the child's PID is confirmed written,
47
+ * without waiting for the process to exit.
48
+ */
49
+ export declare function startBackground(): Promise<{
50
+ pid: number;
51
+ }>;
52
+ export interface StopResult {
53
+ wasRunning: boolean;
54
+ pid: number | null;
55
+ }
56
+ /** SIGTERM -> 1000ms -> SIGKILL if still alive, mirroring daemon.ts's killBackgroundDaemon. */
57
+ export declare function stopProxy(): Promise<StopResult>;
58
+ export declare function readProxyLogTail(maxBytes?: number): string;
59
+ /** Streams new log lines as they're appended, starting from the current end of file. */
60
+ export declare function watchProxyLog(onLine: (line: string) => void): fs.FSWatcher;
61
+ //# sourceMappingURL=lifecycle.d.ts.map