@monoes/monobrowse 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +71 -0
- package/dist/src/browser/action-builder/analyzer.d.ts +0 -1
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
- package/dist/src/browser/action-builder/analyzer.js +43 -21
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/adapters/index.d.ts.map +1 -1
- package/dist/src/browser/adapters/index.js +4 -0
- package/dist/src/browser/adapters/index.js.map +1 -1
- package/dist/src/browser/browser.js +2 -2
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts +6 -0
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +56 -1
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/dashboard/api-types.d.ts +1 -1
- package/dist/src/browser/dashboard/server.d.ts +12 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +23 -28
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +1811 -0
- package/dist/src/browser/playbook/browser-handlers.d.ts +3 -0
- package/dist/src/browser/playbook/browser-handlers.d.ts.map +1 -0
- package/dist/src/browser/playbook/browser-handlers.js +377 -0
- package/dist/src/browser/playbook/browser-handlers.js.map +1 -0
- package/dist/src/browser/playbook/builtin-handlers.d.ts +3 -0
- package/dist/src/browser/playbook/builtin-handlers.d.ts.map +1 -0
- package/dist/src/browser/playbook/builtin-handlers.js +364 -0
- package/dist/src/browser/playbook/builtin-handlers.js.map +1 -0
- package/dist/src/browser/playbook/index.d.ts +3 -0
- package/dist/src/browser/playbook/index.d.ts.map +1 -0
- package/dist/src/browser/playbook/index.js +3 -0
- package/dist/src/browser/playbook/index.js.map +1 -0
- package/dist/src/browser/playbook/store.d.ts +8 -0
- package/dist/src/browser/playbook/store.d.ts.map +1 -0
- package/dist/src/browser/playbook/store.js +87 -0
- package/dist/src/browser/playbook/store.js.map +1 -0
- package/dist/src/browser/screenshot.d.ts +2 -0
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +69 -2
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +11 -0
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/cli/action.d.ts +3 -0
- package/dist/src/cli/action.d.ts.map +1 -0
- package/dist/src/cli/action.js +142 -0
- package/dist/src/cli/action.js.map +1 -0
- package/dist/src/cli/commands.d.ts +8 -0
- package/dist/src/cli/commands.d.ts.map +1 -0
- package/dist/src/cli/commands.js +3018 -0
- package/dist/src/cli/commands.js.map +1 -0
- package/dist/src/cli/output.d.ts +11 -0
- package/dist/src/cli/output.d.ts.map +1 -0
- package/dist/src/cli/output.js +33 -0
- package/dist/src/cli/output.js.map +1 -0
- package/dist/src/cli/platform.d.ts +7 -0
- package/dist/src/cli/platform.d.ts.map +1 -0
- package/dist/src/cli/platform.js +193 -0
- package/dist/src/cli/platform.js.map +1 -0
- package/dist/src/cli/playbook.d.ts +3 -0
- package/dist/src/cli/playbook.d.ts.map +1 -0
- package/dist/src/cli/playbook.js +265 -0
- package/dist/src/cli/playbook.js.map +1 -0
- package/dist/src/cli/types.d.ts +42 -0
- package/dist/src/cli/types.d.ts.map +1 -0
- package/dist/src/cli/types.js +2 -0
- package/dist/src/cli/types.js.map +1 -0
- package/dist/src/cli/workflow.d.ts +2 -0
- package/dist/src/cli/workflow.d.ts.map +1 -0
- package/dist/src/cli/workflow.js +3 -0
- package/dist/src/cli/workflow.js.map +1 -0
- package/dist/src/cli.d.ts +7 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +187 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +0 -1
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +21 -11
- package/src/browser/action-builder/analyzer.ts +41 -23
- package/src/browser/adapters/index.ts +4 -0
- package/src/browser/browser.ts +2 -2
- package/src/browser/cdp.ts +68 -1
- package/src/browser/dashboard/api-types.ts +1 -1
- package/src/browser/dashboard/server.ts +41 -30
- package/src/browser/dashboard/ui.html +1412 -45
- package/src/browser/screenshot.ts +78 -3
- package/src/browser/types.ts +11 -0
- package/src/cli/action.ts +149 -0
- package/src/cli/commands.ts +2892 -0
- package/src/cli/output.ts +37 -0
- package/src/cli/platform.ts +211 -0
- package/src/cli/types.ts +47 -0
- package/src/cli.ts +208 -0
- package/src/index.ts +0 -1
- package/src/browser/workflow/builtin-handlers.ts +0 -379
- package/src/browser/workflow/engine.ts +0 -202
- package/src/browser/workflow/expression.ts +0 -107
- package/src/browser/workflow/index.ts +0 -5
- package/src/browser/workflow/store.ts +0 -87
- package/src/browser/workflow/types.ts +0 -62
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const COLORS = {
|
|
2
|
+
reset: '\x1b[0m',
|
|
3
|
+
bold: '\x1b[1m',
|
|
4
|
+
green: '\x1b[32m',
|
|
5
|
+
yellow: '\x1b[33m',
|
|
6
|
+
blue: '\x1b[34m',
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
cyan: '\x1b[36m',
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
class OutputFormatter {
|
|
12
|
+
private colorEnabled = process.stdout.isTTY ?? false;
|
|
13
|
+
|
|
14
|
+
private c(color: keyof typeof COLORS, text: string): string {
|
|
15
|
+
if (!this.colorEnabled) return text;
|
|
16
|
+
return `${COLORS[color]}${text}${COLORS.reset}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
printSuccess(message: string): void {
|
|
20
|
+
console.log(this.c('green', '✓') + ' ' + message);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
printError(message: string, details?: string): void {
|
|
24
|
+
console.error(this.c('red', '✗') + ' ' + message);
|
|
25
|
+
if (details) console.error(this.c('red', ' ' + details));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
printWarning(message: string): void {
|
|
29
|
+
console.warn(this.c('yellow', '⚠') + ' ' + message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
printInfo(message: string): void {
|
|
33
|
+
console.log(this.c('cyan', 'ℹ') + ' ' + message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const output = new OutputFormatter();
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browse Platform Command — Manage platform connections (LinkedIn, Instagram, X, Gemini)
|
|
3
|
+
* Sessions stored as JSON at ~/.monomind/sessions.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { getAdapter } from '../index.js';
|
|
12
|
+
|
|
13
|
+
const CDP_PORT = Number(process.env['MONOMIND_CDP_PORT'] ?? 9222);
|
|
14
|
+
|
|
15
|
+
const SESSIONS_FILE = join(homedir(), '.monomind', 'sessions.json');
|
|
16
|
+
|
|
17
|
+
interface Session {
|
|
18
|
+
id: string; // "platform:username"
|
|
19
|
+
platform: string;
|
|
20
|
+
username: string;
|
|
21
|
+
cookies: string; // JSON-serialized
|
|
22
|
+
userAgent: string;
|
|
23
|
+
createdAt: number;
|
|
24
|
+
lastUsedAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function loadSessions(): Promise<Session[]> {
|
|
28
|
+
if (!existsSync(SESSIONS_FILE)) return [];
|
|
29
|
+
try {
|
|
30
|
+
const raw = await readFile(SESSIONS_FILE, 'utf-8');
|
|
31
|
+
return JSON.parse(raw) as Session[];
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function saveSessions(sessions: Session[]): Promise<void> {
|
|
38
|
+
await mkdir(join(homedir(), '.monomind'), { recursive: true });
|
|
39
|
+
await writeFile(SESSIONS_FILE, JSON.stringify(sessions, null, 2));
|
|
40
|
+
// Restrict to owner-only (rw-------) since the file contains platform session cookies.
|
|
41
|
+
await chmod(SESSIONS_FILE, 0o600);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createPlatformCommand(): Command {
|
|
45
|
+
const cmd = new Command('platform').description('Manage platform connections');
|
|
46
|
+
|
|
47
|
+
cmd
|
|
48
|
+
.command('connect <platform>')
|
|
49
|
+
.description('Connect a platform account via browser — opens Chrome, waits for login, saves session')
|
|
50
|
+
.option('--port <number>', 'CDP port of running Chrome instance', String(CDP_PORT))
|
|
51
|
+
.option('--timeout <seconds>', 'Login timeout in seconds', '300')
|
|
52
|
+
.action(async (platform: string, opts: { port: string; timeout: string }) => {
|
|
53
|
+
const SUPPORTED = ['linkedin', 'instagram', 'x', 'gemini'];
|
|
54
|
+
if (!SUPPORTED.includes(platform)) {
|
|
55
|
+
console.error(`Unknown platform: ${platform}. Supported: ${SUPPORTED.join(', ')}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let adapter: ReturnType<typeof getAdapter>;
|
|
60
|
+
try { adapter = getAdapter(platform); } catch (err) {
|
|
61
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const cdpPort = Number(opts.port);
|
|
66
|
+
const timeoutMs = Number(opts.timeout) * 1000;
|
|
67
|
+
|
|
68
|
+
// Lazy-load browser module
|
|
69
|
+
let browser: typeof import('@monoes/monobrowse');
|
|
70
|
+
try { browser = await import('@monoes/monobrowse'); } catch (err) {
|
|
71
|
+
console.error('Browser module unavailable:', err);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Connect to Chrome (or launch if needed)
|
|
76
|
+
let conn: Awaited<ReturnType<typeof browser.connectToTarget>>;
|
|
77
|
+
try {
|
|
78
|
+
conn = await browser.connectToTarget(cdpPort);
|
|
79
|
+
console.log(`Connected to Chrome on port ${cdpPort}`);
|
|
80
|
+
} catch {
|
|
81
|
+
console.log(`Chrome not found on port ${cdpPort} — launching managed browser...`);
|
|
82
|
+
try {
|
|
83
|
+
const port = await browser.launchBrowser({ port: cdpPort, headless: false });
|
|
84
|
+
conn = await browser.connectToTarget(port);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('Failed to connect or launch Chrome:', err);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { client, sessionId: sid } = conn;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const refs = new Map();
|
|
95
|
+
|
|
96
|
+
// Restore any existing cookies first so the user might already be logged in
|
|
97
|
+
const sessions = await loadSessions();
|
|
98
|
+
const existingSession = sessions.find(s => s.platform === platform);
|
|
99
|
+
if (existingSession?.cookies) {
|
|
100
|
+
try {
|
|
101
|
+
const cookies = JSON.parse(existingSession.cookies);
|
|
102
|
+
if (cookies.length > 0) await browser.setCookies(client, sid, cookies);
|
|
103
|
+
} catch { /* ignore */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Navigate to login URL
|
|
107
|
+
console.log(`\nOpening ${platform} login page...`);
|
|
108
|
+
await browser.openUrl(client, sid, adapter.loginURL());
|
|
109
|
+
|
|
110
|
+
// Check if already logged in after cookie restore
|
|
111
|
+
const pageIface = {
|
|
112
|
+
evaluate: async <T>(expr: string) => browser.evaluateJs(client, sid, expr) as Promise<T>,
|
|
113
|
+
url: async () => browser.getCurrentUrl(client, sid).catch(() => ''),
|
|
114
|
+
};
|
|
115
|
+
let isLoggedIn = await adapter.isLoggedIn(pageIface).catch(() => false);
|
|
116
|
+
|
|
117
|
+
if (!isLoggedIn) {
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log('┌──────────────────────────────────────────────────────┐');
|
|
120
|
+
console.log(`│ Please sign in to ${platform.padEnd(34)}│`);
|
|
121
|
+
console.log('│ │');
|
|
122
|
+
console.log('│ Complete the login in the browser window. │');
|
|
123
|
+
console.log(`│ Waiting up to ${Math.round(timeoutMs / 60000)} minutes for login… │`);
|
|
124
|
+
console.log('└──────────────────────────────────────────────────────┘');
|
|
125
|
+
console.log('');
|
|
126
|
+
|
|
127
|
+
const deadline = Date.now() + timeoutMs;
|
|
128
|
+
while (Date.now() < deadline) {
|
|
129
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
130
|
+
isLoggedIn = await adapter.isLoggedIn(pageIface).catch(() => false);
|
|
131
|
+
if (isLoggedIn) break;
|
|
132
|
+
// Navigate to app URL if user manually browsed away from login
|
|
133
|
+
const currentUrl = await browser.getCurrentUrl(client, sid).catch(() => '');
|
|
134
|
+
if (!currentUrl.includes(new URL(adapter.loginURL()).hostname) &&
|
|
135
|
+
!currentUrl.includes(new URL(adapter.baseURL).hostname)) {
|
|
136
|
+
await browser.openUrl(client, sid, adapter.loginURL());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!isLoggedIn) {
|
|
141
|
+
console.error(`\nLogin timeout after ${Math.round(timeoutMs / 1000)}s. Session not saved.`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('Login detected! Capturing session...');
|
|
147
|
+
|
|
148
|
+
// Capture cookies and user agent
|
|
149
|
+
const liveCookies = await browser.getCookies(client, sid).catch(() => []);
|
|
150
|
+
const userAgent = await browser.evaluateJs(client, sid, 'navigator.userAgent').catch(() => 'unknown') as string;
|
|
151
|
+
const username = await adapter.extractUsername(pageIface).catch(() => `${platform}-user`);
|
|
152
|
+
|
|
153
|
+
// Save to sessions file
|
|
154
|
+
const sessionEntry: Session = {
|
|
155
|
+
id: `${platform}:${username}`,
|
|
156
|
+
platform,
|
|
157
|
+
username: username || `${platform}-user`,
|
|
158
|
+
cookies: JSON.stringify(liveCookies),
|
|
159
|
+
userAgent: String(userAgent),
|
|
160
|
+
createdAt: existingSession?.createdAt ?? Date.now(),
|
|
161
|
+
lastUsedAt: Date.now(),
|
|
162
|
+
};
|
|
163
|
+
const idx = sessions.findIndex(s => s.platform === platform);
|
|
164
|
+
if (idx >= 0) sessions[idx] = sessionEntry; else sessions.push(sessionEntry);
|
|
165
|
+
await saveSessions(sessions);
|
|
166
|
+
|
|
167
|
+
console.log(`\n✓ Session saved: ${sessionEntry.id}`);
|
|
168
|
+
console.log(` Cookies: ${liveCookies.length} captured`);
|
|
169
|
+
console.log(` File: ${SESSIONS_FILE}`);
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log(`Session will be automatically used by: monomind browse playbook run`);
|
|
172
|
+
} finally {
|
|
173
|
+
client.close();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
cmd
|
|
178
|
+
.command('list')
|
|
179
|
+
.description('List connected platform accounts')
|
|
180
|
+
.action(async () => {
|
|
181
|
+
const sessions = await loadSessions();
|
|
182
|
+
if (sessions.length === 0) {
|
|
183
|
+
console.log('No connected accounts. Run: browse platform connect <platform>');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
console.log(`\n${'ID'.padEnd(40)} ${'PLATFORM'.padEnd(12)} ${'USERNAME'.padEnd(30)} LAST USED`);
|
|
187
|
+
console.log('─'.repeat(95));
|
|
188
|
+
for (const s of sessions) {
|
|
189
|
+
const lastUsed = new Date(s.lastUsedAt).toLocaleString();
|
|
190
|
+
console.log(`${s.id.padEnd(40)} ${s.platform.padEnd(12)} ${s.username.padEnd(30)} ${lastUsed}`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
cmd
|
|
195
|
+
.command('disconnect <id>')
|
|
196
|
+
.description('Remove a saved platform session')
|
|
197
|
+
.action(async (id: string) => {
|
|
198
|
+
const sessions = await loadSessions();
|
|
199
|
+
const idx = sessions.findIndex(s => s.id === id);
|
|
200
|
+
if (idx < 0) {
|
|
201
|
+
console.error(`Session not found: ${id}`);
|
|
202
|
+
console.log('Run "browse platform list" to see available sessions.');
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
sessions.splice(idx, 1);
|
|
206
|
+
await saveSessions(sessions);
|
|
207
|
+
console.log(`Disconnected: ${id}`);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return cmd;
|
|
211
|
+
}
|
package/src/cli/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface CommandContext {
|
|
2
|
+
args: string[];
|
|
3
|
+
flags: ParsedFlags;
|
|
4
|
+
cwd: string;
|
|
5
|
+
interactive: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ParsedFlags {
|
|
9
|
+
[key: string]: string | boolean | number | string[];
|
|
10
|
+
_: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Command {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
aliases?: string[];
|
|
17
|
+
subcommands?: Command[];
|
|
18
|
+
options?: CommandOption[];
|
|
19
|
+
examples?: CommandExample[];
|
|
20
|
+
action?: CommandAction;
|
|
21
|
+
hidden?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CommandOption {
|
|
25
|
+
name: string;
|
|
26
|
+
short?: string;
|
|
27
|
+
description: string;
|
|
28
|
+
type: 'string' | 'boolean' | 'number' | 'array';
|
|
29
|
+
default?: unknown;
|
|
30
|
+
required?: boolean;
|
|
31
|
+
choices?: string[];
|
|
32
|
+
validate?: (value: unknown) => boolean | string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CommandExample {
|
|
36
|
+
command: string;
|
|
37
|
+
description: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CommandAction = (ctx: CommandContext) => Promise<CommandResult | void>;
|
|
41
|
+
|
|
42
|
+
export interface CommandResult {
|
|
43
|
+
success: boolean;
|
|
44
|
+
message?: string;
|
|
45
|
+
data?: unknown;
|
|
46
|
+
exitCode?: number;
|
|
47
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* monobrowse — standalone CLI entry point
|
|
4
|
+
* Dispatches to the same command tree used by `monomind browse`
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
import browseCommand from './cli/commands.js';
|
|
9
|
+
import { output } from './cli/output.js';
|
|
10
|
+
import type { Command, CommandContext, CommandOption, ParsedFlags } from './cli/types.js';
|
|
11
|
+
|
|
12
|
+
const _require = createRequire(import.meta.url);
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Subcommand lookup (supports aliases)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function findSub(name: string, subs: Command[]): Command | undefined {
|
|
19
|
+
return subs.find(s => s.name === name || s.aliases?.includes(name));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Minimal argv parser — resolves short flags from the target subcommand's opts
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function buildShortMap(cmd: Command | undefined): Record<string, string> {
|
|
27
|
+
const m: Record<string, string> = {};
|
|
28
|
+
if (!cmd?.options) return m;
|
|
29
|
+
for (const opt of cmd.options) {
|
|
30
|
+
if (opt.short) m[opt.short] = opt.name;
|
|
31
|
+
}
|
|
32
|
+
return m;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isBooleanOpt(name: string, cmd: Command | undefined): boolean {
|
|
36
|
+
const opt = cmd?.options?.find(o => o.name === name);
|
|
37
|
+
return opt?.type === 'boolean';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgv(argv: string[], sub: Command | undefined): { args: string[]; flags: ParsedFlags } {
|
|
41
|
+
const shortMap = buildShortMap(sub);
|
|
42
|
+
const flags: ParsedFlags = { _: [] };
|
|
43
|
+
const positional: string[] = [];
|
|
44
|
+
let i = 0;
|
|
45
|
+
|
|
46
|
+
while (i < argv.length) {
|
|
47
|
+
const arg = argv[i];
|
|
48
|
+
|
|
49
|
+
if (arg === '--') {
|
|
50
|
+
positional.push(...argv.slice(i + 1));
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (arg.startsWith('--')) {
|
|
55
|
+
const raw = arg.slice(2);
|
|
56
|
+
if (raw.startsWith('no-')) {
|
|
57
|
+
flags[raw.slice(3)] = false;
|
|
58
|
+
} else if (raw.includes('=')) {
|
|
59
|
+
const eq = raw.indexOf('=');
|
|
60
|
+
flags[raw.slice(0, eq)] = raw.slice(eq + 1);
|
|
61
|
+
} else if (isBooleanOpt(raw, sub) || i + 1 >= argv.length || argv[i + 1].startsWith('-')) {
|
|
62
|
+
flags[raw] = true;
|
|
63
|
+
} else {
|
|
64
|
+
flags[raw] = argv[++i];
|
|
65
|
+
}
|
|
66
|
+
} else if (arg.startsWith('-') && arg.length >= 2) {
|
|
67
|
+
const short = arg[1];
|
|
68
|
+
const long = shortMap[short] ?? short;
|
|
69
|
+
if (arg.length > 2) {
|
|
70
|
+
flags[long] = arg.slice(2);
|
|
71
|
+
} else if (isBooleanOpt(long, sub) || i + 1 >= argv.length || argv[i + 1].startsWith('-')) {
|
|
72
|
+
flags[long] = true;
|
|
73
|
+
} else {
|
|
74
|
+
flags[long] = argv[++i];
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
positional.push(arg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Coerce number options
|
|
84
|
+
for (const opt of (sub?.options ?? [])) {
|
|
85
|
+
if (opt.type === 'number' && typeof flags[opt.name] === 'string') {
|
|
86
|
+
flags[opt.name] = Number(flags[opt.name]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { args: positional, flags };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Help printer
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function printHelp(cmd: Command, prefix = 'monobrowse'): void {
|
|
98
|
+
const usage = cmd.name === 'browse'
|
|
99
|
+
? `${prefix} <subcommand> [options]`
|
|
100
|
+
: `${prefix} ${cmd.name} [subcommand] [options]`;
|
|
101
|
+
console.log(`\nUsage: ${usage}\n`);
|
|
102
|
+
console.log(cmd.description + '\n');
|
|
103
|
+
|
|
104
|
+
if (cmd.subcommands?.length) {
|
|
105
|
+
console.log('Subcommands:');
|
|
106
|
+
for (const s of cmd.subcommands) {
|
|
107
|
+
if (!s.hidden) console.log(` ${s.name.padEnd(20)} ${s.description}`);
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (cmd.options?.length) {
|
|
113
|
+
console.log('Options:');
|
|
114
|
+
for (const o of cmd.options) {
|
|
115
|
+
const shortPart = o.short ? `-${o.short}, ` : ' ';
|
|
116
|
+
const defaultPart = o.default !== undefined ? ` (default: ${o.default})` : '';
|
|
117
|
+
console.log(` ${shortPart}--${o.name.padEnd(18)} ${o.description}${defaultPart}`);
|
|
118
|
+
}
|
|
119
|
+
console.log('');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (cmd.examples?.length) {
|
|
123
|
+
console.log('Examples:');
|
|
124
|
+
for (const ex of cmd.examples) {
|
|
125
|
+
const adjusted = ex.command.replace(/^monomind browse/, prefix);
|
|
126
|
+
console.log(` ${adjusted} ${ex.description}`);
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Dispatch: find action and build context
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
async function dispatch(cmd: Command, argv: string[]): Promise<void> {
|
|
137
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
138
|
+
printHelp(cmd);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const subName = argv[0];
|
|
143
|
+
const rest = argv.slice(1);
|
|
144
|
+
|
|
145
|
+
const sub = findSub(subName, cmd.subcommands ?? []);
|
|
146
|
+
if (!sub) {
|
|
147
|
+
output.printError(`Unknown subcommand: ${subName}`);
|
|
148
|
+
output.printInfo('Run `monobrowse --help` to see available subcommands.');
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (rest[0] === '--help' || rest[0] === '-h') {
|
|
154
|
+
printHelp(sub, 'monobrowse');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if first remaining arg matches a nested subcommand
|
|
159
|
+
if (sub.subcommands?.length && rest.length > 0 && !rest[0].startsWith('-')) {
|
|
160
|
+
const nested = findSub(rest[0], sub.subcommands);
|
|
161
|
+
if (nested?.action) {
|
|
162
|
+
const { args, flags } = parseArgv(rest.slice(1), nested);
|
|
163
|
+
const ctx: CommandContext = { args, flags, cwd: process.cwd(), interactive: !!process.stdout.isTTY };
|
|
164
|
+
const result = await nested.action(ctx);
|
|
165
|
+
if (result && !result.success) process.exitCode = result.exitCode ?? 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// No nested match — invoke the subcommand's own action
|
|
171
|
+
if (!sub.action) {
|
|
172
|
+
output.printInfo(`Run \`monobrowse ${subName} --help\` for usage.`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { args, flags } = parseArgv(rest, sub);
|
|
177
|
+
const ctx: CommandContext = { args, flags, cwd: process.cwd(), interactive: !!process.stdout.isTTY };
|
|
178
|
+
const result = await sub.action(ctx);
|
|
179
|
+
if (result && !result.success) process.exitCode = result.exitCode ?? 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Main
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
async function main(): Promise<void> {
|
|
187
|
+
const userArgs = process.argv.slice(2);
|
|
188
|
+
|
|
189
|
+
if (userArgs.length === 0 || userArgs[0] === '--help' || userArgs[0] === '-h') {
|
|
190
|
+
printHelp(browseCommand);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (userArgs[0] === '--version' || userArgs[0] === '-V') {
|
|
195
|
+
const pkg = _require('../package.json') as { version: string };
|
|
196
|
+
console.log(pkg.version);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await dispatch(browseCommand, userArgs);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
main().then(() => {
|
|
204
|
+
process.exit(process.exitCode ?? 0);
|
|
205
|
+
}).catch(err => {
|
|
206
|
+
output.printError(err instanceof Error ? err.message : String(err));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
});
|