@adhdev/daemon-core 0.5.3
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/index.d.ts +2662 -0
- package/dist/index.js +11341 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/providers/_builtin/.github/workflows/generate-registry.yml +57 -0
- package/providers/_builtin/COMPATIBILITY.md +217 -0
- package/providers/_builtin/CONTRIBUTING.md +200 -0
- package/providers/_builtin/README.md +119 -0
- package/providers/_builtin/_helpers/index.js +188 -0
- package/providers/_builtin/acp/agentpool/provider.json +54 -0
- package/providers/_builtin/acp/amp/provider.json +52 -0
- package/providers/_builtin/acp/auggie/provider.json +57 -0
- package/providers/_builtin/acp/autodev/provider.json +54 -0
- package/providers/_builtin/acp/autohand/provider.json +52 -0
- package/providers/_builtin/acp/blackbox-ai/provider.json +54 -0
- package/providers/_builtin/acp/claude-agent/provider.json +57 -0
- package/providers/_builtin/acp/cline-acp/provider.json +54 -0
- package/providers/_builtin/acp/codebuddy/provider.json +54 -0
- package/providers/_builtin/acp/codex-cli/provider.json +57 -0
- package/providers/_builtin/acp/corust-agent/provider.json +52 -0
- package/providers/_builtin/acp/crow-cli/provider.json +54 -0
- package/providers/_builtin/acp/cursor-acp/provider.json +54 -0
- package/providers/_builtin/acp/deepagents/provider.json +52 -0
- package/providers/_builtin/acp/dimcode/provider.json +54 -0
- package/providers/_builtin/acp/docker-cagent/provider.json +57 -0
- package/providers/_builtin/acp/factory-droid/provider.json +60 -0
- package/providers/_builtin/acp/fast-agent/provider.json +52 -0
- package/providers/_builtin/acp/gemini-cli/provider.json +114 -0
- package/providers/_builtin/acp/github-copilot/provider.json +54 -0
- package/providers/_builtin/acp/goose/provider.json +57 -0
- package/providers/_builtin/acp/junie/provider.json +52 -0
- package/providers/_builtin/acp/kilo/provider.json +54 -0
- package/providers/_builtin/acp/kimi-cli/provider.json +57 -0
- package/providers/_builtin/acp/minion-code/provider.json +52 -0
- package/providers/_builtin/acp/mistral-vibe/provider.json +57 -0
- package/providers/_builtin/acp/nova/provider.json +54 -0
- package/providers/_builtin/acp/openclaw/provider.json +54 -0
- package/providers/_builtin/acp/opencode/provider.json +52 -0
- package/providers/_builtin/acp/openhands/provider.json +54 -0
- package/providers/_builtin/acp/pi-acp/provider.json +52 -0
- package/providers/_builtin/acp/qoder/provider.json +54 -0
- package/providers/_builtin/acp/qwen-code/provider.json +60 -0
- package/providers/_builtin/acp/stakpak/provider.json +54 -0
- package/providers/_builtin/acp/vtcode/provider.json +54 -0
- package/providers/_builtin/cli/claude-cli/provider.json +100 -0
- package/providers/_builtin/cli/codex-cli/provider.json +89 -0
- package/providers/_builtin/cli/gemini-cli/provider.json +93 -0
- package/providers/_builtin/docs/CDP_SELECTOR_GUIDE.md +370 -0
- package/providers/_builtin/docs/PROVIDER_GUIDE.md +916 -0
- package/providers/_builtin/extension/cline/provider.json +35 -0
- package/providers/_builtin/extension/cline/scripts/focus_editor.js +48 -0
- package/providers/_builtin/extension/cline/scripts/list_chats.js +100 -0
- package/providers/_builtin/extension/cline/scripts/list_models.js +43 -0
- package/providers/_builtin/extension/cline/scripts/list_modes.js +35 -0
- package/providers/_builtin/extension/cline/scripts/new_session.js +85 -0
- package/providers/_builtin/extension/cline/scripts/open_panel.js +25 -0
- package/providers/_builtin/extension/cline/scripts/read_chat.js +257 -0
- package/providers/_builtin/extension/cline/scripts/resolve_action.js +83 -0
- package/providers/_builtin/extension/cline/scripts/send_message.js +95 -0
- package/providers/_builtin/extension/cline/scripts/set_mode.js +36 -0
- package/providers/_builtin/extension/cline/scripts/set_model.js +36 -0
- package/providers/_builtin/extension/cline/scripts/switch_session.js +206 -0
- package/providers/_builtin/extension/cline/scripts.js +73 -0
- package/providers/_builtin/extension/roo-code/provider.json +35 -0
- package/providers/_builtin/extension/roo-code/scripts.js +659 -0
- package/providers/_builtin/ide/antigravity/provider.json +68 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/focus_editor.js +20 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/list_chats.js +137 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/list_models.js +38 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/list_modes.js +48 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/new_session.js +75 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/read_chat.js +262 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/resolve_action.js +68 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/scripts.js +57 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/send_message.js +56 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/set_mode.js +34 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/set_model.js +47 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/switch_session.js +114 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/focus_editor.js +20 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/list_chats.js +137 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/list_models.js +61 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/list_modes.js +72 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/new_session.js +75 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/read_chat.js +262 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/resolve_action.js +68 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/scripts.js +67 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/send_message.js +56 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/set_mode.js +67 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/set_model.js +72 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/switch_session.js +114 -0
- package/providers/_builtin/ide/cursor/provider.json +70 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/dismiss_notification.js +30 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/focus_editor.js +13 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_models.js +78 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_modes.js +40 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_notifications.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_sessions.js +42 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/new_session.js +20 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/open_panel.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/read_chat.js +75 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/resolve_action.js +19 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/scripts.js +78 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/send_message.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/set_mode.js +38 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/set_model.js +81 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/switch_session.js +28 -0
- package/providers/_builtin/ide/kiro/provider.json +67 -0
- package/providers/_builtin/ide/kiro/scripts/focus_editor.js +20 -0
- package/providers/_builtin/ide/kiro/scripts/open_panel.js +47 -0
- package/providers/_builtin/ide/kiro/scripts/resolve_action.js +54 -0
- package/providers/_builtin/ide/kiro/scripts/send_message.js +29 -0
- package/providers/_builtin/ide/kiro/scripts/webview_list_models.js +39 -0
- package/providers/_builtin/ide/kiro/scripts/webview_list_modes.js +39 -0
- package/providers/_builtin/ide/kiro/scripts/webview_list_sessions.js +21 -0
- package/providers/_builtin/ide/kiro/scripts/webview_new_session.js +34 -0
- package/providers/_builtin/ide/kiro/scripts/webview_read_chat.js +68 -0
- package/providers/_builtin/ide/kiro/scripts/webview_send_message.js +72 -0
- package/providers/_builtin/ide/kiro/scripts/webview_set_mode.js +15 -0
- package/providers/_builtin/ide/kiro/scripts/webview_set_model.js +15 -0
- package/providers/_builtin/ide/kiro/scripts/webview_switch_session.js +26 -0
- package/providers/_builtin/ide/kiro/scripts.js +62 -0
- package/providers/_builtin/ide/pearai/provider.json +67 -0
- package/providers/_builtin/ide/pearai/scripts/focus_editor.js +20 -0
- package/providers/_builtin/ide/pearai/scripts/list_sessions.js +38 -0
- package/providers/_builtin/ide/pearai/scripts/new_session.js +55 -0
- package/providers/_builtin/ide/pearai/scripts/open_panel.js +46 -0
- package/providers/_builtin/ide/pearai/scripts/resolve_action.js +54 -0
- package/providers/_builtin/ide/pearai/scripts/send_message.js +29 -0
- package/providers/_builtin/ide/pearai/scripts/webview_list_models.js +43 -0
- package/providers/_builtin/ide/pearai/scripts/webview_list_modes.js +35 -0
- package/providers/_builtin/ide/pearai/scripts/webview_list_sessions.js +62 -0
- package/providers/_builtin/ide/pearai/scripts/webview_new_session.js +49 -0
- package/providers/_builtin/ide/pearai/scripts/webview_read_chat.js +92 -0
- package/providers/_builtin/ide/pearai/scripts/webview_resolve_action.js +59 -0
- package/providers/_builtin/ide/pearai/scripts/webview_send_message.js +72 -0
- package/providers/_builtin/ide/pearai/scripts/webview_set_mode.js +36 -0
- package/providers/_builtin/ide/pearai/scripts/webview_set_model.js +36 -0
- package/providers/_builtin/ide/pearai/scripts/webview_switch_session.js +34 -0
- package/providers/_builtin/ide/pearai/scripts.js +74 -0
- package/providers/_builtin/ide/trae/provider.json +66 -0
- package/providers/_builtin/ide/trae/scripts/focus_editor.js +20 -0
- package/providers/_builtin/ide/trae/scripts/list_chats.js +24 -0
- package/providers/_builtin/ide/trae/scripts/list_models.js +39 -0
- package/providers/_builtin/ide/trae/scripts/list_modes.js +39 -0
- package/providers/_builtin/ide/trae/scripts/new_session.js +30 -0
- package/providers/_builtin/ide/trae/scripts/open_panel.js +44 -0
- package/providers/_builtin/ide/trae/scripts/read_chat.js +113 -0
- package/providers/_builtin/ide/trae/scripts/resolve_action.js +54 -0
- package/providers/_builtin/ide/trae/scripts/send_message.js +69 -0
- package/providers/_builtin/ide/trae/scripts/set_mode.js +15 -0
- package/providers/_builtin/ide/trae/scripts/set_model.js +15 -0
- package/providers/_builtin/ide/trae/scripts/switch_session.js +23 -0
- package/providers/_builtin/ide/trae/scripts.js +57 -0
- package/providers/_builtin/ide/vscode/provider.json +64 -0
- package/providers/_builtin/ide/vscode-insiders/provider.json +62 -0
- package/providers/_builtin/ide/vscodium/provider.json +63 -0
- package/providers/_builtin/ide/windsurf/provider.json +53 -0
- package/providers/_builtin/ide/windsurf/scripts/focus_editor.js +30 -0
- package/providers/_builtin/ide/windsurf/scripts/list_chats.js +117 -0
- package/providers/_builtin/ide/windsurf/scripts/list_models.js +39 -0
- package/providers/_builtin/ide/windsurf/scripts/list_modes.js +39 -0
- package/providers/_builtin/ide/windsurf/scripts/new_session.js +69 -0
- package/providers/_builtin/ide/windsurf/scripts/open_panel.js +58 -0
- package/providers/_builtin/ide/windsurf/scripts/read_chat.js +297 -0
- package/providers/_builtin/ide/windsurf/scripts/resolve_action.js +68 -0
- package/providers/_builtin/ide/windsurf/scripts/send_message.js +87 -0
- package/providers/_builtin/ide/windsurf/scripts/set_mode.js +15 -0
- package/providers/_builtin/ide/windsurf/scripts/set_model.js +15 -0
- package/providers/_builtin/ide/windsurf/scripts/switch_session.js +58 -0
- package/providers/_builtin/ide/windsurf/scripts.js +57 -0
- package/providers/_builtin/registry.json +266 -0
- package/providers/_builtin/validate.js +156 -0
- package/src/agent-stream/index.ts +6 -0
- package/src/agent-stream/manager.ts +286 -0
- package/src/agent-stream/poller.ts +154 -0
- package/src/agent-stream/provider-adapter.ts +138 -0
- package/src/agent-stream/types.ts +61 -0
- package/src/boot/daemon-lifecycle.ts +252 -0
- package/src/cdp/devtools.ts +335 -0
- package/src/cdp/initializer.ts +191 -0
- package/src/cdp/manager.ts +897 -0
- package/src/cdp/scanner.ts +185 -0
- package/src/cdp/setup.ts +150 -0
- package/src/cli-adapter-types.ts +25 -0
- package/src/cli-adapters/provider-cli-adapter.ts +448 -0
- package/src/commands/cdp-commands.ts +208 -0
- package/src/commands/chat-commands.ts +675 -0
- package/src/commands/cli-manager.ts +353 -0
- package/src/commands/handler.ts +328 -0
- package/src/commands/router.ts +258 -0
- package/src/commands/stream-commands.ts +325 -0
- package/src/config/chat-history.ts +211 -0
- package/src/config/config.ts +219 -0
- package/src/daemon/dev-server.ts +2378 -0
- package/src/daemon/scaffold-template.ts +394 -0
- package/src/daemon-core.ts +50 -0
- package/src/detection/cli-detector.ts +89 -0
- package/src/detection/ide-detector.ts +157 -0
- package/src/index.ts +103 -0
- package/src/installer.ts +263 -0
- package/src/ipc-protocol.ts +133 -0
- package/src/launch.ts +433 -0
- package/src/logging/command-log.ts +180 -0
- package/src/logging/logger.ts +316 -0
- package/src/providers/acp-provider-instance.ts +1140 -0
- package/src/providers/cli-provider-instance.ts +207 -0
- package/src/providers/contracts.ts +524 -0
- package/src/providers/extension-provider-instance.ts +156 -0
- package/src/providers/ide-provider-instance.ts +377 -0
- package/src/providers/index.ts +18 -0
- package/src/providers/provider-instance-manager.ts +182 -0
- package/src/providers/provider-instance.ts +112 -0
- package/src/providers/provider-loader.ts +1031 -0
- package/src/providers/status-monitor.ts +125 -0
- package/src/providers/version-archive.ts +266 -0
- package/src/status/reporter.ts +294 -0
- package/src/types.ts +206 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProviderLoader — Provider discovery + OS/version override resolution
|
|
3
|
+
*
|
|
4
|
+
* Role:
|
|
5
|
+
* 1. Load provider.js from _builtin/ directory
|
|
6
|
+
* 2. Load user custom from ~/.adhdev/providers/ (overrides)
|
|
7
|
+
* 3. Apply OS/version overrides (process.platform + detected IDE version)
|
|
8
|
+
* 4. Hot-reload support (fs.watch)
|
|
9
|
+
*
|
|
10
|
+
* Design principles:
|
|
11
|
+
* - Load JS files via require() (CJS compatible)
|
|
12
|
+
* - User custom can override builtin
|
|
13
|
+
* - provider.js files are independent, so load order doesn't matter
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import * as os from 'os';
|
|
19
|
+
import { registerIDEDefinition } from '../detection/ide-detector.js';
|
|
20
|
+
import { LOG } from '../logging/logger.js';
|
|
21
|
+
import { VersionArchive } from './version-archive.js';
|
|
22
|
+
import type {
|
|
23
|
+
ProviderModule,
|
|
24
|
+
ProviderCategory,
|
|
25
|
+
ProviderScripts,
|
|
26
|
+
ProviderSettingSchema,
|
|
27
|
+
ResolvedProvider,
|
|
28
|
+
} from './contracts.js';
|
|
29
|
+
|
|
30
|
+
export class ProviderLoader {
|
|
31
|
+
private providers = new Map<string, ProviderModule>();
|
|
32
|
+
private builtinDirs: string[];
|
|
33
|
+
private userDir: string;
|
|
34
|
+
private upstreamDir: string;
|
|
35
|
+
private watchers: fs.FSWatcher[] = [];
|
|
36
|
+
private logFn: (msg: string) => void;
|
|
37
|
+
private versionArchive: VersionArchive | null = null;
|
|
38
|
+
private scriptsCache = new Map<string, Record<string, any>>();
|
|
39
|
+
|
|
40
|
+
/** Inject VersionArchive so resolve() can auto-detect installed versions */
|
|
41
|
+
setVersionArchive(archive: VersionArchive): void {
|
|
42
|
+
this.versionArchive = archive;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static readonly GITHUB_TARBALL_URL = 'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
|
|
46
|
+
private static readonly META_FILE = '.meta.json';
|
|
47
|
+
|
|
48
|
+
constructor(options?: {
|
|
49
|
+
builtinDir?: string | string[];
|
|
50
|
+
userDir?: string;
|
|
51
|
+
logFn?: (msg: string) => void;
|
|
52
|
+
}) {
|
|
53
|
+
// Builtin directories: providers/_builtin/
|
|
54
|
+
if (options?.builtinDir) {
|
|
55
|
+
this.builtinDirs = Array.isArray(options.builtinDir) ? options.builtinDir : [options.builtinDir];
|
|
56
|
+
} else {
|
|
57
|
+
this.builtinDirs = [path.resolve(__dirname, '../providers/_builtin')];
|
|
58
|
+
}
|
|
59
|
+
// User custom directory: ~/.adhdev/providers/
|
|
60
|
+
this.userDir = options?.userDir ||
|
|
61
|
+
path.join(os.homedir(), '.adhdev', 'providers');
|
|
62
|
+
// Upstream auto-download directory: ~/.adhdev/providers/.upstream/
|
|
63
|
+
this.upstreamDir = path.join(this.userDir, '.upstream');
|
|
64
|
+
this.logFn = options?.logFn || LOG.forComponent('Provider').asLogFn();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private log(msg: string): void {
|
|
68
|
+
this.logFn(`[ProviderLoader] ${msg}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Public API ────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load all providers (3-tier priority)
|
|
75
|
+
* 1. _builtin/ (bundled fallback, or multiple array dirs)
|
|
76
|
+
* 2. .upstream/ (GitHub auto-download)
|
|
77
|
+
* 3. User custom (~/.adhdev/providers/ excluding _upstream)
|
|
78
|
+
* Later loads override earlier ones, so user custom always wins.
|
|
79
|
+
*/
|
|
80
|
+
loadAll(): void {
|
|
81
|
+
this.providers.clear();
|
|
82
|
+
|
|
83
|
+
// 1. Load builtin (npm package bundle — lowest priority)
|
|
84
|
+
let builtinCount = 0;
|
|
85
|
+
for (const dir of this.builtinDirs) {
|
|
86
|
+
if (fs.existsSync(dir)) {
|
|
87
|
+
builtinCount += this.loadDir(dir);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this.log(`Loaded ${builtinCount} builtin providers`);
|
|
91
|
+
|
|
92
|
+
// 2. Load upstream (GitHub auto-download — overrides builtin)
|
|
93
|
+
let upstreamCount = 0;
|
|
94
|
+
if (fs.existsSync(this.upstreamDir)) {
|
|
95
|
+
upstreamCount = this.loadDir(this.upstreamDir);
|
|
96
|
+
if (upstreamCount > 0) {
|
|
97
|
+
this.log(`Loaded ${upstreamCount} upstream providers (auto-updated)`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. Load user custom (excluding _upstream — highest priority, never auto-updated)
|
|
102
|
+
if (fs.existsSync(this.userDir)) {
|
|
103
|
+
const userCount = this.loadDir(this.userDir, ['.upstream']);
|
|
104
|
+
if (userCount > 0) {
|
|
105
|
+
this.log(`Loaded ${userCount} user custom providers (never auto-updated)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.log(`Total: ${this.providers.size} providers [${[...this.providers.keys()].join(', ')}]`);
|
|
110
|
+
|
|
111
|
+
// ⚠️ Warning: using builtin fallback only, upstream not available
|
|
112
|
+
if (upstreamCount === 0 && builtinCount > 0) {
|
|
113
|
+
this.log(`⚠ Using bundled providers only (upstream not available). Run 'adhdev daemon' with internet to auto-update.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ❌ Error: no providers found anywhere
|
|
117
|
+
if (this.providers.size === 0) {
|
|
118
|
+
this.log(`❌ No providers loaded! Check builtinDirs.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get raw provider metadata by type (NO scripts loaded).
|
|
124
|
+
* Use resolve() when you need scripts (readChat, listModels, etc).
|
|
125
|
+
* @deprecated Use getMeta() for metadata or resolve() for scripts.
|
|
126
|
+
*/
|
|
127
|
+
get(type: string): ProviderModule | undefined {
|
|
128
|
+
return this.providers.get(type);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get raw provider metadata by type (NO scripts loaded).
|
|
133
|
+
* Safe for: category checks, icon, displayName, targetFilter, cdpPorts.
|
|
134
|
+
* NOT safe for: script execution (readChat, listModels, sendMessage).
|
|
135
|
+
* Use resolve() when scripts are needed.
|
|
136
|
+
*/
|
|
137
|
+
getMeta(type: string): ProviderModule | undefined {
|
|
138
|
+
return this.providers.get(type);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve provider type by alias
|
|
143
|
+
* 'claude' → 'claude-cli', 'codex' → 'codex-cli' etc
|
|
144
|
+
* Returns input as-is if no match found.
|
|
145
|
+
*/
|
|
146
|
+
resolveAlias(input: string): string {
|
|
147
|
+
// 1. directly match
|
|
148
|
+
if (this.providers.has(input)) return input;
|
|
149
|
+
// 2. alias match
|
|
150
|
+
for (const p of this.providers.values()) {
|
|
151
|
+
if (p.aliases?.includes(input)) return p.type;
|
|
152
|
+
}
|
|
153
|
+
return input;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get provider with alias resolution (get + alias fallback)
|
|
158
|
+
*/
|
|
159
|
+
getByAlias(input: string): ProviderModule | undefined {
|
|
160
|
+
return this.providers.get(this.resolveAlias(input));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Build CLI/ACP detection list (replaces cli-detector)
|
|
165
|
+
* Dynamically generated from provider.js spawn.command.
|
|
166
|
+
*/
|
|
167
|
+
getCliDetectionList(): { id: string; displayName: string; icon: string; command: string; category: string }[] {
|
|
168
|
+
const result: { id: string; displayName: string; icon: string; command: string; category: string }[] = [];
|
|
169
|
+
for (const p of this.providers.values()) {
|
|
170
|
+
if ((p.category === 'cli' || p.category === 'acp') && p.spawn?.command) {
|
|
171
|
+
result.push({
|
|
172
|
+
id: p.type,
|
|
173
|
+
displayName: p.displayName || p.name,
|
|
174
|
+
icon: p.icon || '🔧',
|
|
175
|
+
command: p.spawn.command,
|
|
176
|
+
category: p.category,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* List providers by category
|
|
185
|
+
*/
|
|
186
|
+
getByCategory(cat: ProviderCategory): ProviderModule[] {
|
|
187
|
+
return [...this.providers.values()].filter(p => p.category === cat);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extension Extension providers with extensionIdPattern only
|
|
192
|
+
* (used by discoverAgentWebviews in daemon-cdp.ts)
|
|
193
|
+
*/
|
|
194
|
+
getExtensionProviders(): ProviderModule[] {
|
|
195
|
+
return [...this.providers.values()].filter(
|
|
196
|
+
p => p.category === 'extension' && p.extensionIdPattern
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* All loaded providers
|
|
202
|
+
*/
|
|
203
|
+
getAll(): ProviderModule[] {
|
|
204
|
+
return [...this.providers.values()];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if a provider is enabled (per-IDE)
|
|
209
|
+
* Checks ideSettings[ideType].extensions[type].enabled.
|
|
210
|
+
* Default false (disabled) — user must explicitly enable.
|
|
211
|
+
* Always returns true when called without ideType.
|
|
212
|
+
*/
|
|
213
|
+
isEnabled(type: string, ideType?: string): boolean {
|
|
214
|
+
if (!ideType) return true;
|
|
215
|
+
try {
|
|
216
|
+
const { loadConfig } = require('../config/config.js');
|
|
217
|
+
const config = loadConfig();
|
|
218
|
+
const val = config.ideSettings?.[ideType]?.extensions?.[type]?.enabled;
|
|
219
|
+
return val === true; // undefined → false (default inactive)
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Save IDE extension enabled setting
|
|
227
|
+
*/
|
|
228
|
+
setIdeExtensionEnabled(ideType: string, extensionType: string, enabled: boolean): boolean {
|
|
229
|
+
try {
|
|
230
|
+
const { loadConfig, saveConfig } = require('../config/config.js');
|
|
231
|
+
const config = loadConfig();
|
|
232
|
+
if (!config.ideSettings) config.ideSettings = {};
|
|
233
|
+
if (!config.ideSettings[ideType]) config.ideSettings[ideType] = {};
|
|
234
|
+
if (!config.ideSettings[ideType].extensions) config.ideSettings[ideType].extensions = {};
|
|
235
|
+
config.ideSettings[ideType].extensions[extensionType] = { enabled };
|
|
236
|
+
saveConfig(config);
|
|
237
|
+
this.log(`IDE extension setting: ${ideType}.${extensionType}.enabled = ${enabled}`);
|
|
238
|
+
return true;
|
|
239
|
+
} catch (e) {
|
|
240
|
+
this.log(`Failed to save IDE extension setting: ${(e as Error).message}`);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Return only enabled providers by category (per-IDE)
|
|
247
|
+
*/
|
|
248
|
+
getEnabledByCategory(cat: ProviderCategory, ideType?: string): ProviderModule[] {
|
|
249
|
+
return this.getByCategory(cat).filter(p => this.isEnabled(p.type, ideType));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Extension Enabled extension providers with extensionIdPattern only (per-IDE)
|
|
254
|
+
*/
|
|
255
|
+
getEnabledExtensionProviders(ideType?: string): ProviderModule[] {
|
|
256
|
+
return this.getExtensionProviders().filter(p => this.isEnabled(p.type, ideType));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Return CDP port map for IDE providers
|
|
261
|
+
* Used by launch.ts, adhdev-daemon.ts
|
|
262
|
+
*/
|
|
263
|
+
getCdpPortMap(): Record<string, [number, number]> {
|
|
264
|
+
const map: Record<string, [number, number]> = {};
|
|
265
|
+
for (const p of this.providers.values()) {
|
|
266
|
+
if (p.category === 'ide' && p.cdpPorts) {
|
|
267
|
+
map[p.type] = p.cdpPorts as [number, number];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return map;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Return IDE process name map (macOS)
|
|
275
|
+
*/
|
|
276
|
+
getMacAppIdentifiers(): Record<string, string> {
|
|
277
|
+
const map: Record<string, string> = {};
|
|
278
|
+
for (const p of this.providers.values()) {
|
|
279
|
+
if (p.category === 'ide' && p.processNames?.darwin) {
|
|
280
|
+
map[p.type] = p.processNames.darwin as string;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return map;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Return IDE process name map (Windows)
|
|
288
|
+
*/
|
|
289
|
+
getWinProcessNames(): Record<string, string[]> {
|
|
290
|
+
const map: Record<string, string[]> = {};
|
|
291
|
+
for (const p of this.providers.values()) {
|
|
292
|
+
if (p.category === 'ide' && p.processNames?.win32) {
|
|
293
|
+
map[p.type] = p.processNames.win32 as string[];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return map;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Available IDE types (only those with cdpPorts)
|
|
301
|
+
*/
|
|
302
|
+
getAvailableIdeTypes(): string[] {
|
|
303
|
+
return [...this.providers.values()]
|
|
304
|
+
.filter(p => p.category === 'ide' && p.cdpPorts)
|
|
305
|
+
.map(p => p.type);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Register IDE providers to core/detector registry
|
|
310
|
+
* → Enables detectIDEs() to detect provider.js-based IDEs
|
|
311
|
+
*/
|
|
312
|
+
registerToDetector(): number {
|
|
313
|
+
let count = 0;
|
|
314
|
+
for (const p of this.providers.values()) {
|
|
315
|
+
if (p.category === 'ide' && p.cli && p.paths) {
|
|
316
|
+
registerIDEDefinition({
|
|
317
|
+
id: p.type,
|
|
318
|
+
name: p.name,
|
|
319
|
+
displayName: p.displayName || p.name,
|
|
320
|
+
icon: p.icon || '💻',
|
|
321
|
+
extensionSupport: 'full',
|
|
322
|
+
cli: p.cli,
|
|
323
|
+
paths: p.paths as { darwin?: string[]; win32?: string[]; linux?: string[] },
|
|
324
|
+
});
|
|
325
|
+
count++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
this.log(`Registered ${count} IDE providers to detector`);
|
|
329
|
+
return count;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Return final provider with OS/version overrides applied.
|
|
334
|
+
*
|
|
335
|
+
* Script resolution order:
|
|
336
|
+
* 1. compatibility array (new format — preferred)
|
|
337
|
+
* Provider.json defines: "compatibility": [{ "ideVersion": ">=1.107.0", "scriptDir": "scripts/1.107" }]
|
|
338
|
+
* First matching range wins. Fallback: defaultScriptDir.
|
|
339
|
+
* 2. versions field (legacy format — backward compat)
|
|
340
|
+
* "versions": { "< 1.107.0": { "__dir": "scripts/legacy" } }
|
|
341
|
+
* 3. Root scripts.js (original format — no versioning)
|
|
342
|
+
*
|
|
343
|
+
* Version source: context.version → VersionArchive → undefined
|
|
344
|
+
*/
|
|
345
|
+
resolve(type: string, context?: { os?: string; version?: string }): ResolvedProvider | undefined {
|
|
346
|
+
const base = this.providers.get(type);
|
|
347
|
+
if (!base) return undefined;
|
|
348
|
+
|
|
349
|
+
const currentOs = context?.os || process.platform;
|
|
350
|
+
const currentVersion = context?.version ??
|
|
351
|
+
this.versionArchive?.getLatest(type) ??
|
|
352
|
+
undefined;
|
|
353
|
+
|
|
354
|
+
// Deep clone to avoid mutating the original
|
|
355
|
+
const resolved: ResolvedProvider = JSON.parse(JSON.stringify(base));
|
|
356
|
+
// Restore RegExp from original (lost during JSON.parse)
|
|
357
|
+
if (base.extensionIdPattern) {
|
|
358
|
+
resolved.extensionIdPattern = base.extensionIdPattern;
|
|
359
|
+
}
|
|
360
|
+
// Restore script functions (lost during JSON.parse)
|
|
361
|
+
if (base.scripts) {
|
|
362
|
+
resolved.scripts = { ...base.scripts };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 1. Apply OS override
|
|
366
|
+
if (base.os?.[currentOs]) {
|
|
367
|
+
const osOverride = base.os[currentOs];
|
|
368
|
+
if (osOverride.scripts) {
|
|
369
|
+
resolved.scripts = { ...resolved.scripts, ...osOverride.scripts };
|
|
370
|
+
}
|
|
371
|
+
if (osOverride.inputMethod) resolved.inputMethod = osOverride.inputMethod;
|
|
372
|
+
if (osOverride.inputSelector) resolved.inputSelector = osOverride.inputSelector;
|
|
373
|
+
resolved._resolvedOs = currentOs;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 2. Apply version-based script selection
|
|
377
|
+
if (currentVersion) {
|
|
378
|
+
resolved._resolvedVersion = currentVersion;
|
|
379
|
+
|
|
380
|
+
// --- New format: compatibility array ---
|
|
381
|
+
if (Array.isArray((base as any).compatibility)) {
|
|
382
|
+
const compat = (base as any).compatibility as { ideVersion: string; scriptDir: string }[];
|
|
383
|
+
let matched = false;
|
|
384
|
+
|
|
385
|
+
for (const entry of compat) {
|
|
386
|
+
if (this.matchesVersion(currentVersion, entry.ideVersion)) {
|
|
387
|
+
const loaded = this.loadScriptsFromDir(type, entry.scriptDir);
|
|
388
|
+
if (loaded) {
|
|
389
|
+
resolved.scripts = loaded;
|
|
390
|
+
this.log(` [compatibility] ${type} v${currentVersion} → ${entry.scriptDir}`);
|
|
391
|
+
matched = true;
|
|
392
|
+
}
|
|
393
|
+
break; // first match wins
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// No compatibility match → defaultScriptDir
|
|
398
|
+
if (!matched && (base as any).defaultScriptDir) {
|
|
399
|
+
const loaded = this.loadScriptsFromDir(type, (base as any).defaultScriptDir);
|
|
400
|
+
if (loaded) {
|
|
401
|
+
resolved.scripts = loaded;
|
|
402
|
+
this.log(` [compatibility] ${type} v${currentVersion} → default: ${(base as any).defaultScriptDir}`);
|
|
403
|
+
}
|
|
404
|
+
resolved._versionWarning = `Version ${currentVersion} not in compatibility matrix. Using default scripts.`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// --- Legacy format: versions field ---
|
|
408
|
+
} else if (base.versions) {
|
|
409
|
+
for (const [range, override] of Object.entries(base.versions)) {
|
|
410
|
+
if (!this.matchesVersion(currentVersion, range)) continue;
|
|
411
|
+
|
|
412
|
+
const dirOverride = (override as any).__dir as string | undefined;
|
|
413
|
+
if (dirOverride) {
|
|
414
|
+
const loaded = this.loadScriptsFromDir(type, dirOverride);
|
|
415
|
+
if (loaded) {
|
|
416
|
+
resolved.scripts = loaded;
|
|
417
|
+
this.log(` [version override] ${type} ${range} → ${dirOverride}`);
|
|
418
|
+
}
|
|
419
|
+
} else if (override.scripts) {
|
|
420
|
+
resolved.scripts = { ...resolved.scripts, ...override.scripts };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} else if (Array.isArray((base as any).compatibility) && (base as any).defaultScriptDir) {
|
|
425
|
+
// No version detected but compatibility format → use defaultScriptDir
|
|
426
|
+
const loaded = this.loadScriptsFromDir(type, (base as any).defaultScriptDir);
|
|
427
|
+
if (loaded) {
|
|
428
|
+
resolved.scripts = loaded;
|
|
429
|
+
this.log(` [compatibility] ${type} no version detected → default: ${(base as any).defaultScriptDir}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 3. Composite override (OS + version)
|
|
434
|
+
if (base.overrides) {
|
|
435
|
+
for (const override of base.overrides) {
|
|
436
|
+
const osMatch = !override.when.os || override.when.os === currentOs;
|
|
437
|
+
const verMatch = !override.when.version || (currentVersion && this.matchesVersion(currentVersion, override.when.version));
|
|
438
|
+
if (osMatch && verMatch && override.scripts) {
|
|
439
|
+
resolved.scripts = { ...resolved.scripts, ...override.scripts };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return resolved;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Load scripts from a scriptDir within a provider directory.
|
|
449
|
+
* Tries scripts.js first, then individual .js files.
|
|
450
|
+
*/
|
|
451
|
+
private loadScriptsFromDir(type: string, scriptDir: string): Record<string, any> | null {
|
|
452
|
+
const providerDir = this.findProviderDir(type);
|
|
453
|
+
if (!providerDir) {
|
|
454
|
+
this.log(` [loadScriptsFromDir] ${type}: providerDir not found`);
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const dir = path.join(providerDir, scriptDir);
|
|
459
|
+
if (!fs.existsSync(dir)) {
|
|
460
|
+
this.log(` [loadScriptsFromDir] ${type}: dir not found: ${dir}`);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Return cached scripts if available (cleared on reload/watch)
|
|
465
|
+
const cached = this.scriptsCache.get(dir);
|
|
466
|
+
if (cached) return cached;
|
|
467
|
+
|
|
468
|
+
// Try scripts.js first
|
|
469
|
+
const scriptsJs = path.join(dir, 'scripts.js');
|
|
470
|
+
if (fs.existsSync(scriptsJs)) {
|
|
471
|
+
try {
|
|
472
|
+
delete require.cache[require.resolve(scriptsJs)];
|
|
473
|
+
const loaded = require(scriptsJs);
|
|
474
|
+
this.log(` [loadScriptsFromDir] ${type}: loaded scripts.js from ${dir} (${Object.keys(loaded).length} exports)`);
|
|
475
|
+
this.scriptsCache.set(dir, loaded);
|
|
476
|
+
return loaded;
|
|
477
|
+
} catch (e) {
|
|
478
|
+
this.log(` ⚠ scripts.js load failed: ${scriptsJs}: ${(e as Error).message}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Fallback: build from individual .js files
|
|
483
|
+
const result = this.buildScriptWrappersFromDir(dir) as Record<string, any>;
|
|
484
|
+
this.log(` [loadScriptsFromDir] ${type}: built wrappers from ${dir} (${Object.keys(result).length} scripts)`);
|
|
485
|
+
this.scriptsCache.set(dir, result);
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Hot-reload: start watching for file changes
|
|
491
|
+
*/
|
|
492
|
+
watch(): void {
|
|
493
|
+
this.stopWatch();
|
|
494
|
+
const watchDir = (dir: string) => {
|
|
495
|
+
if (!fs.existsSync(dir)) {
|
|
496
|
+
// Create directory if missing (so user can drop files)
|
|
497
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { return; }
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
const watcher = fs.watch(dir, { recursive: true }, (event, filename) => {
|
|
501
|
+
if (filename?.endsWith('.js') || filename?.endsWith('.json')) {
|
|
502
|
+
this.log(`File changed: ${filename}, reloading...`);
|
|
503
|
+
this.loadAll();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
this.watchers.push(watcher);
|
|
507
|
+
} catch (e) {
|
|
508
|
+
this.log(`Watch failed for ${dir}: ${(e as Error).message}`);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
this.builtinDirs.forEach(dir => watchDir(dir));
|
|
512
|
+
watchDir(this.userDir);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Stop hot-reload
|
|
517
|
+
*/
|
|
518
|
+
stopWatch(): void {
|
|
519
|
+
for (const w of this.watchers) {
|
|
520
|
+
try { w.close(); } catch { }
|
|
521
|
+
}
|
|
522
|
+
this.watchers = [];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Full reload
|
|
527
|
+
*/
|
|
528
|
+
reload(): void {
|
|
529
|
+
this.log('Reloading all providers...');
|
|
530
|
+
// Clear caches
|
|
531
|
+
this.scriptsCache.clear();
|
|
532
|
+
// Clear require cache (hot-reload)
|
|
533
|
+
for (const key of Object.keys(require.cache)) {
|
|
534
|
+
if (key.includes('providers') && (key.endsWith('.js') || key.endsWith('.json'))) {
|
|
535
|
+
delete require.cache[key];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
this.loadAll();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ─── Upstream Auto-Update ─────────────────────────
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Download latest providers tarball from GitHub → extract to .upstream/
|
|
545
|
+
* - ETag-based change detection (skip if unchanged)
|
|
546
|
+
* - Never touches user custom files in ~/.adhdev/providers/
|
|
547
|
+
* - Runs in background; existing providers are kept on failure
|
|
548
|
+
*
|
|
549
|
+
* @returns Whether an update occurred
|
|
550
|
+
*/
|
|
551
|
+
async fetchLatest(): Promise<{ updated: boolean; error?: string }> {
|
|
552
|
+
const https = require('https') as typeof import('https');
|
|
553
|
+
const { execSync } = require('child_process') as typeof import('child_process');
|
|
554
|
+
|
|
555
|
+
const metaPath = path.join(this.upstreamDir, ProviderLoader.META_FILE);
|
|
556
|
+
let prevEtag = '';
|
|
557
|
+
let prevTimestamp = 0;
|
|
558
|
+
|
|
559
|
+
// Read previous metadata
|
|
560
|
+
try {
|
|
561
|
+
if (fs.existsSync(metaPath)) {
|
|
562
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
563
|
+
prevEtag = meta.etag || '';
|
|
564
|
+
prevTimestamp = meta.timestamp || 0;
|
|
565
|
+
}
|
|
566
|
+
} catch { }
|
|
567
|
+
|
|
568
|
+
// Minimum 30-minute interval (prevent excessive checks)
|
|
569
|
+
const MIN_INTERVAL_MS = 30 * 60 * 1000;
|
|
570
|
+
if (prevTimestamp && (Date.now() - prevTimestamp) < MIN_INTERVAL_MS) {
|
|
571
|
+
this.log('Upstream check skipped (last check < 30min ago)');
|
|
572
|
+
return { updated: false };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
// Step 1: HEAD request to check ETag
|
|
577
|
+
const etag = await new Promise<string>((resolve, reject) => {
|
|
578
|
+
const options = {
|
|
579
|
+
method: 'HEAD',
|
|
580
|
+
hostname: 'github.com',
|
|
581
|
+
path: '/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz',
|
|
582
|
+
headers: { 'User-Agent': 'adhdev-launcher' },
|
|
583
|
+
timeout: 10000,
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const req = https.request(options, (res) => {
|
|
587
|
+
// GitHub 302 redirect → follow
|
|
588
|
+
if (res.statusCode === 302 && res.headers.location) {
|
|
589
|
+
const url = new URL(res.headers.location);
|
|
590
|
+
const req2 = https.request({
|
|
591
|
+
method: 'HEAD',
|
|
592
|
+
hostname: url.hostname,
|
|
593
|
+
path: url.pathname + (url.search || ''),
|
|
594
|
+
headers: { 'User-Agent': 'adhdev-launcher' },
|
|
595
|
+
timeout: 10000,
|
|
596
|
+
}, (res2) => {
|
|
597
|
+
resolve(res2.headers.etag || res2.headers['last-modified'] || '');
|
|
598
|
+
});
|
|
599
|
+
req2.on('error', reject);
|
|
600
|
+
req2.on('timeout', () => { req2.destroy(); reject(new Error('timeout')); });
|
|
601
|
+
req2.end();
|
|
602
|
+
} else {
|
|
603
|
+
resolve(res.headers.etag || res.headers['last-modified'] || '');
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
req.on('error', reject);
|
|
607
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
608
|
+
req.end();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Compare ETag — skip if unchanged
|
|
612
|
+
if (etag && etag === prevEtag) {
|
|
613
|
+
// Update timestamp only
|
|
614
|
+
this.writeMeta(metaPath, prevEtag, Date.now());
|
|
615
|
+
this.log('Upstream unchanged (ETag match)');
|
|
616
|
+
return { updated: false };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Step 2: Download + extract
|
|
620
|
+
this.log('Downloading latest providers from GitHub...');
|
|
621
|
+
|
|
622
|
+
const tmpTar = path.join(os.tmpdir(), `adhdev-providers-${Date.now()}.tar.gz`);
|
|
623
|
+
const tmpExtract = path.join(os.tmpdir(), `adhdev-providers-extract-${Date.now()}`);
|
|
624
|
+
|
|
625
|
+
// Download tarball
|
|
626
|
+
await this.downloadFile(ProviderLoader.GITHUB_TARBALL_URL, tmpTar);
|
|
627
|
+
|
|
628
|
+
// Extract
|
|
629
|
+
fs.mkdirSync(tmpExtract, { recursive: true });
|
|
630
|
+
execSync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
|
|
631
|
+
|
|
632
|
+
// Tarball internal structure: adhdev-providers-main/ide/... → strip 1 level
|
|
633
|
+
const extracted = fs.readdirSync(tmpExtract);
|
|
634
|
+
const rootDir = extracted.find(d =>
|
|
635
|
+
fs.statSync(path.join(tmpExtract, d)).isDirectory() && d.startsWith('adhdev-providers')
|
|
636
|
+
);
|
|
637
|
+
if (!rootDir) throw new Error('Unexpected tarball structure');
|
|
638
|
+
|
|
639
|
+
const sourceDir = path.join(tmpExtract, rootDir);
|
|
640
|
+
|
|
641
|
+
// .upstream replacement (atomic-ish: rename old → copy new → delete old)
|
|
642
|
+
const backupDir = this.upstreamDir + '.bak';
|
|
643
|
+
if (fs.existsSync(this.upstreamDir)) {
|
|
644
|
+
// Backup
|
|
645
|
+
if (fs.existsSync(backupDir)) fs.rmSync(backupDir, { recursive: true, force: true });
|
|
646
|
+
fs.renameSync(this.upstreamDir, backupDir);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
// Copy new upstream
|
|
651
|
+
this.copyDirRecursive(sourceDir, this.upstreamDir);
|
|
652
|
+
// Save metadata
|
|
653
|
+
this.writeMeta(metaPath, etag || `ts-${Date.now()}`, Date.now());
|
|
654
|
+
// Backup remove
|
|
655
|
+
if (fs.existsSync(backupDir)) fs.rmSync(backupDir, { recursive: true, force: true });
|
|
656
|
+
} catch (e) {
|
|
657
|
+
// Restore backup on copy failure
|
|
658
|
+
if (fs.existsSync(backupDir)) {
|
|
659
|
+
if (fs.existsSync(this.upstreamDir)) fs.rmSync(this.upstreamDir, { recursive: true, force: true });
|
|
660
|
+
fs.renameSync(backupDir, this.upstreamDir);
|
|
661
|
+
}
|
|
662
|
+
throw e;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Cleanup temp
|
|
666
|
+
try { fs.rmSync(tmpTar, { force: true }); } catch { }
|
|
667
|
+
try { fs.rmSync(tmpExtract, { recursive: true, force: true }); } catch { }
|
|
668
|
+
|
|
669
|
+
const upstreamCount = this.countProviders(this.upstreamDir);
|
|
670
|
+
this.log(`✅ Upstream updated: ${upstreamCount} providers`);
|
|
671
|
+
|
|
672
|
+
return { updated: true };
|
|
673
|
+
} catch (e: any) {
|
|
674
|
+
this.log(`⚠ Upstream fetch failed (using existing): ${e?.message}`);
|
|
675
|
+
// Update timestamp even on failure (prevent continuous retries)
|
|
676
|
+
this.writeMeta(metaPath, prevEtag, Date.now());
|
|
677
|
+
return { updated: false, error: e?.message };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** HTTP(S) file download (follows redirects) */
|
|
682
|
+
private downloadFile(url: string, destPath: string): Promise<void> {
|
|
683
|
+
const https = require('https') as typeof import('https');
|
|
684
|
+
const http = require('http') as typeof import('http');
|
|
685
|
+
|
|
686
|
+
return new Promise((resolve, reject) => {
|
|
687
|
+
const doRequest = (reqUrl: string, redirectCount = 0) => {
|
|
688
|
+
if (redirectCount > 5) { reject(new Error('Too many redirects')); return; }
|
|
689
|
+
const mod = reqUrl.startsWith('https') ? https : http;
|
|
690
|
+
const req = mod.get(reqUrl, { headers: { 'User-Agent': 'adhdev-launcher' }, timeout: 60000 }, (res) => {
|
|
691
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
692
|
+
doRequest(res.headers.location!, redirectCount + 1);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (res.statusCode !== 200) {
|
|
696
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const ws = fs.createWriteStream(destPath);
|
|
700
|
+
res.pipe(ws);
|
|
701
|
+
ws.on('finish', () => { ws.close(); resolve(); });
|
|
702
|
+
ws.on('error', reject);
|
|
703
|
+
});
|
|
704
|
+
req.on('error', reject);
|
|
705
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Download timeout')); });
|
|
706
|
+
};
|
|
707
|
+
doRequest(url);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Recursive directory copy */
|
|
712
|
+
private copyDirRecursive(src: string, dest: string): void {
|
|
713
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
714
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
715
|
+
const srcPath = path.join(src, entry.name);
|
|
716
|
+
const destPath = path.join(dest, entry.name);
|
|
717
|
+
if (entry.isDirectory()) {
|
|
718
|
+
this.copyDirRecursive(srcPath, destPath);
|
|
719
|
+
} else {
|
|
720
|
+
fs.copyFileSync(srcPath, destPath);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** .meta.json save */
|
|
726
|
+
private writeMeta(metaPath: string, etag: string, timestamp: number): void {
|
|
727
|
+
try {
|
|
728
|
+
fs.mkdirSync(path.dirname(metaPath), { recursive: true });
|
|
729
|
+
fs.writeFileSync(metaPath, JSON.stringify({
|
|
730
|
+
etag,
|
|
731
|
+
timestamp,
|
|
732
|
+
lastCheck: new Date(timestamp).toISOString(),
|
|
733
|
+
source: ProviderLoader.GITHUB_TARBALL_URL,
|
|
734
|
+
}, null, 2));
|
|
735
|
+
} catch { }
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Count provider files (provider.js or provider.json) */
|
|
739
|
+
private countProviders(dir: string): number {
|
|
740
|
+
if (!fs.existsSync(dir)) return 0;
|
|
741
|
+
let count = 0;
|
|
742
|
+
const scan = (d: string) => {
|
|
743
|
+
try {
|
|
744
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
745
|
+
if (entry.isDirectory()) scan(path.join(d, entry.name));
|
|
746
|
+
else if (entry.name === 'provider.json') count++;
|
|
747
|
+
}
|
|
748
|
+
} catch { }
|
|
749
|
+
};
|
|
750
|
+
scan(dir);
|
|
751
|
+
return count;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ─── Provider Settings API ─────────────────────────
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Get public settings schema for a provider (for dashboard UI rendering)
|
|
758
|
+
*/
|
|
759
|
+
getPublicSettings(type: string): ProviderSettingSchema[] {
|
|
760
|
+
const provider = this.providers.get(type);
|
|
761
|
+
if (!provider?.settings) return [];
|
|
762
|
+
return Object.entries(provider.settings)
|
|
763
|
+
.filter(([, def]) => (def as any).public === true)
|
|
764
|
+
.map(([key, def]) => ({ key, ...(def as any) }));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Get public settings schema for all providers
|
|
769
|
+
*/
|
|
770
|
+
getAllPublicSettings(): Record<string, ProviderSettingSchema[]> {
|
|
771
|
+
const result: Record<string, ProviderSettingSchema[]> = {};
|
|
772
|
+
for (const [type] of this.providers) {
|
|
773
|
+
const settings = this.getPublicSettings(type);
|
|
774
|
+
if (settings.length > 0) result[type] = settings;
|
|
775
|
+
}
|
|
776
|
+
return result;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Resolved setting value for a provider (default + user override)
|
|
781
|
+
*/
|
|
782
|
+
getSettingValue(type: string, key: string): any {
|
|
783
|
+
const provider = this.providers.get(type);
|
|
784
|
+
const schemaDef = provider?.settings?.[key];
|
|
785
|
+
const defaultVal = schemaDef ? (schemaDef as any).default : undefined;
|
|
786
|
+
|
|
787
|
+
// Load user-saved value
|
|
788
|
+
try {
|
|
789
|
+
const { loadConfig } = require('../config/config.js');
|
|
790
|
+
const config = loadConfig();
|
|
791
|
+
const userVal = config.providerSettings?.[type]?.[key];
|
|
792
|
+
return userVal !== undefined ? userVal : defaultVal;
|
|
793
|
+
} catch {
|
|
794
|
+
return defaultVal;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* All resolved settings for a provider (default + user override)
|
|
800
|
+
*/
|
|
801
|
+
getSettings(type: string): Record<string, any> {
|
|
802
|
+
const provider = this.providers.get(type);
|
|
803
|
+
if (!provider?.settings) return {};
|
|
804
|
+
const result: Record<string, any> = {};
|
|
805
|
+
for (const [key, def] of Object.entries(provider.settings)) {
|
|
806
|
+
result[key] = this.getSettingValue(type, key);
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Save provider setting value (writes to config.json)
|
|
813
|
+
*/
|
|
814
|
+
setSetting(type: string, key: string, value: any): boolean {
|
|
815
|
+
const provider = this.providers.get(type);
|
|
816
|
+
const schemaDef = provider?.settings?.[key] as any;
|
|
817
|
+
if (!schemaDef) return false;
|
|
818
|
+
|
|
819
|
+
// Non-public settings cannot be modified externally
|
|
820
|
+
if (!schemaDef.public) return false;
|
|
821
|
+
|
|
822
|
+
// Type validation
|
|
823
|
+
if (schemaDef.type === 'boolean' && typeof value !== 'boolean') return false;
|
|
824
|
+
if (schemaDef.type === 'number') {
|
|
825
|
+
if (typeof value !== 'number') return false;
|
|
826
|
+
if (schemaDef.min !== undefined && value < schemaDef.min) return false;
|
|
827
|
+
if (schemaDef.max !== undefined && value > schemaDef.max) return false;
|
|
828
|
+
}
|
|
829
|
+
if (schemaDef.type === 'select' && schemaDef.options && !schemaDef.options.includes(value)) return false;
|
|
830
|
+
|
|
831
|
+
try {
|
|
832
|
+
const { loadConfig, saveConfig } = require('../config/config.js');
|
|
833
|
+
const config = loadConfig();
|
|
834
|
+
if (!config.providerSettings) config.providerSettings = {};
|
|
835
|
+
if (!config.providerSettings[type]) config.providerSettings[type] = {};
|
|
836
|
+
config.providerSettings[type][key] = value;
|
|
837
|
+
saveConfig(config);
|
|
838
|
+
this.log(`Setting updated: ${type}.${key} = ${JSON.stringify(value)}`);
|
|
839
|
+
return true;
|
|
840
|
+
} catch (e) {
|
|
841
|
+
this.log(`Failed to save setting: ${(e as Error).message}`);
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ─── Private ───────────────────────────────────
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Find the on-disk directory for a provider by type.
|
|
850
|
+
* Checks builtinDir, upstreamDir, userDir in order.
|
|
851
|
+
*/
|
|
852
|
+
private findProviderDir(type: string): string | null {
|
|
853
|
+
const provider = this.providers.get(type);
|
|
854
|
+
if (!provider) return null;
|
|
855
|
+
const cat = provider.category;
|
|
856
|
+
|
|
857
|
+
const searchRoots = [this.userDir, this.upstreamDir, ...this.builtinDirs];
|
|
858
|
+
for (const root of searchRoots) {
|
|
859
|
+
if (!fs.existsSync(root)) continue;
|
|
860
|
+
// Direct: root/type or root/cat/type
|
|
861
|
+
for (const candidate of [path.join(root, type), path.join(root, cat, type)]) {
|
|
862
|
+
if (fs.existsSync(path.join(candidate, 'provider.json'))) return candidate;
|
|
863
|
+
}
|
|
864
|
+
// Scan category dir for type match
|
|
865
|
+
const catDir = path.join(root, cat);
|
|
866
|
+
if (fs.existsSync(catDir)) {
|
|
867
|
+
try {
|
|
868
|
+
for (const entry of fs.readdirSync(catDir, { withFileTypes: true })) {
|
|
869
|
+
if (!entry.isDirectory()) continue;
|
|
870
|
+
const jsonPath = path.join(catDir, entry.name, 'provider.json');
|
|
871
|
+
if (fs.existsSync(jsonPath)) {
|
|
872
|
+
try {
|
|
873
|
+
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
874
|
+
if (data.type === type) return path.join(catDir, entry.name);
|
|
875
|
+
} catch { /* skip */ }
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
} catch { /* skip */ }
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Build a scripts function map from individual .js files in a directory.
|
|
886
|
+
* Each file is wrapped as: (params?) => fs.readFileSync(filePath, 'utf-8')
|
|
887
|
+
* (template substitution is NOT applied here — scripts.js handles that)
|
|
888
|
+
*/
|
|
889
|
+
private buildScriptWrappersFromDir(dir: string): Partial<ProviderScripts> {
|
|
890
|
+
// Use a dedicated scripts.js in the alt dir if present
|
|
891
|
+
const scriptsJs = path.join(dir, 'scripts.js');
|
|
892
|
+
if (fs.existsSync(scriptsJs)) {
|
|
893
|
+
try {
|
|
894
|
+
delete require.cache[require.resolve(scriptsJs)];
|
|
895
|
+
return require(scriptsJs);
|
|
896
|
+
} catch { /* fall through to individual file loading */ }
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Individual files: list_models.js → scripts.listModels, etc.
|
|
900
|
+
const toCamel = (name: string) =>
|
|
901
|
+
name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
902
|
+
|
|
903
|
+
const result: Partial<ProviderScripts> = {};
|
|
904
|
+
try {
|
|
905
|
+
for (const file of fs.readdirSync(dir)) {
|
|
906
|
+
if (!file.endsWith('.js')) continue;
|
|
907
|
+
const scriptName = toCamel(file.replace('.js', ''));
|
|
908
|
+
const filePath = path.join(dir, file);
|
|
909
|
+
(result as any)[scriptName] = (..._args: any[]): string => {
|
|
910
|
+
try { return fs.readFileSync(filePath, 'utf-8'); } catch { return ''; }
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
} catch { /* ignore */ }
|
|
914
|
+
return result;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Recursively scan directory to load provider files
|
|
919
|
+
* Supports two formats:
|
|
920
|
+
* 1. provider.json (metadata) + scripts.js (optional CDP scripts)
|
|
921
|
+
* 2. provider.js (legacy — everything in one file)
|
|
922
|
+
* Structure: dir/category/agent-name/provider.{json,js}
|
|
923
|
+
*/
|
|
924
|
+
private loadDir(dir: string, excludeDirs?: string[]): number {
|
|
925
|
+
if (!fs.existsSync(dir)) return 0;
|
|
926
|
+
let count = 0;
|
|
927
|
+
|
|
928
|
+
const scan = (d: string) => {
|
|
929
|
+
let entries: fs.Dirent[];
|
|
930
|
+
try {
|
|
931
|
+
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
932
|
+
} catch {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Check if this directory has provider.json
|
|
937
|
+
const hasJson = entries.some(e => e.name === 'provider.json');
|
|
938
|
+
|
|
939
|
+
if (hasJson) {
|
|
940
|
+
const jsonPath = path.join(d, 'provider.json');
|
|
941
|
+
try {
|
|
942
|
+
const raw = fs.readFileSync(jsonPath, 'utf-8');
|
|
943
|
+
const mod = JSON.parse(raw) as ProviderModule;
|
|
944
|
+
|
|
945
|
+
if (!mod.type || !mod.name || !mod.category) {
|
|
946
|
+
this.log(`⚠ Invalid provider at ${jsonPath}: missing type/name/category`);
|
|
947
|
+
} else {
|
|
948
|
+
// Restore RegExp fields from JSON (extensionIdPattern)
|
|
949
|
+
if ((mod as any).extensionIdPattern && typeof (mod as any).extensionIdPattern === 'string') {
|
|
950
|
+
const flags = (mod as any).extensionIdPattern_flags || '';
|
|
951
|
+
(mod as any).extensionIdPattern = new RegExp((mod as any).extensionIdPattern, flags);
|
|
952
|
+
delete (mod as any).extensionIdPattern_flags;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Load scripts.js if exists (IDE/Extension)
|
|
956
|
+
// Skip for compatibility-format providers — scripts loaded lazily in resolve()
|
|
957
|
+
const hasCompatibility = Array.isArray((mod as any).compatibility);
|
|
958
|
+
const scriptsPath = path.join(d, 'scripts.js');
|
|
959
|
+
if (!hasCompatibility && fs.existsSync(scriptsPath)) {
|
|
960
|
+
try {
|
|
961
|
+
delete require.cache[require.resolve(scriptsPath)];
|
|
962
|
+
const scripts = require(scriptsPath);
|
|
963
|
+
mod.scripts = scripts;
|
|
964
|
+
} catch (e) {
|
|
965
|
+
this.log(`⚠ Failed to load scripts: ${scriptsPath}: ${(e as Error).message}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const existed = this.providers.has(mod.type);
|
|
970
|
+
this.providers.set(mod.type, mod);
|
|
971
|
+
count++;
|
|
972
|
+
// Identify source tier for debugging
|
|
973
|
+
const source = d.startsWith(this.userDir) && !d.includes('.upstream')
|
|
974
|
+
? 'user' : d.startsWith(this.upstreamDir) ? 'upstream' : 'builtin';
|
|
975
|
+
const overrideWarning = existed && source === 'user' ? ' ⚠ OVERRIDES builtin/upstream' : '';
|
|
976
|
+
this.log(` ${existed ? '🔄' : '✅'} ${mod.type} (${mod.category}) — ${mod.name} [${source}]${overrideWarning}`);
|
|
977
|
+
}
|
|
978
|
+
} catch (e) {
|
|
979
|
+
this.log(`⚠ Failed to load ${jsonPath}: ${(e as Error).message}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Continue scanning subdirectories (only for dirs without provider.json)
|
|
984
|
+
if (!hasJson) {
|
|
985
|
+
for (const entry of entries) {
|
|
986
|
+
if (!entry.isDirectory()) continue;
|
|
987
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
988
|
+
if (excludeDirs && d === dir && excludeDirs.includes(entry.name)) continue;
|
|
989
|
+
scan(path.join(d, entry.name));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
scan(dir);
|
|
995
|
+
return count;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Simple semver range matching
|
|
1000
|
+
* Supported formats: '>=4.0.0', '<3.0.0', '>=2.1.0'
|
|
1001
|
+
*/
|
|
1002
|
+
private matchesVersion(current: string, range: string): boolean {
|
|
1003
|
+
const match = range.match(/^([><=!]+)\s*(\d+\.\d+\.\d+)$/);
|
|
1004
|
+
if (!match) return false;
|
|
1005
|
+
|
|
1006
|
+
const [, op, target] = match;
|
|
1007
|
+
const cmp = this.compareVersions(current, target);
|
|
1008
|
+
|
|
1009
|
+
switch (op) {
|
|
1010
|
+
case '>=': return cmp >= 0;
|
|
1011
|
+
case '>': return cmp > 0;
|
|
1012
|
+
case '<=': return cmp <= 0;
|
|
1013
|
+
case '<': return cmp < 0;
|
|
1014
|
+
case '=':
|
|
1015
|
+
case '==': return cmp === 0;
|
|
1016
|
+
case '!=': return cmp !== 0;
|
|
1017
|
+
default: return false;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private compareVersions(a: string, b: string): number {
|
|
1022
|
+
const pa = a.split('.').map(Number);
|
|
1023
|
+
const pb = b.split('.').map(Number);
|
|
1024
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1025
|
+
const va = pa[i] || 0;
|
|
1026
|
+
const vb = pb[i] || 0;
|
|
1027
|
+
if (va !== vb) return va - vb;
|
|
1028
|
+
}
|
|
1029
|
+
return 0;
|
|
1030
|
+
}
|
|
1031
|
+
}
|