@adhdev/daemon-core 0.9.82-rc.15 → 0.9.82-rc.152
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/boot/process-hardening.d.ts +50 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +5 -1
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +104 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +27 -0
- package/dist/commands/handler.d.ts +66 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +5 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +68 -1
- package/dist/git/git-commands.d.ts +5 -1
- package/dist/index.d.ts +20 -6
- package/dist/index.js +13153 -3031
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13099 -3023
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +72 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-registry.d.ts +25 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-events.d.ts +77 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +58 -1
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-work-queue.d.ts +44 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
- package/dist/providers/approval-utils.d.ts +9 -0
- package/dist/providers/cli-provider-instance.d.ts +6 -1
- package/dist/providers/contracts.d.ts +50 -5
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/index.d.ts +11 -0
- package/dist/providers/provider-loader.d.ts +19 -1
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +64 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/repo-mesh-types.d.ts +67 -0
- package/dist/shared-types.d.ts +12 -0
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +6 -2
- package/src/boot/daemon-lifecycle.ts +17 -10
- package/src/boot/process-hardening.ts +89 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +14 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +3 -1
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1086 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +664 -1137
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +57 -11
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +1451 -52
- package/src/commands/cli-manager.ts +184 -3
- package/src/commands/handler.ts +547 -2
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3273 -428
- package/src/config/chat-history.ts +79 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +249 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +17 -5
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +72 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +479 -0
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +40 -22
- package/src/mesh/coordinator-registry.ts +75 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-events.ts +820 -61
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +457 -104
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-work-queue.ts +216 -158
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +247 -0
- package/src/providers/approval-utils.ts +39 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +380 -44
- package/src/providers/contracts.ts +52 -5
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/index.ts +23 -0
- package/src/providers/provider-loader.ts +294 -28
- package/src/providers/provider-schema.ts +31 -13
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +245 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +444 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +119 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +210 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +77 -0
- package/src/shared-types.ts +9 -0
- package/src/status/builders.ts +23 -6
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +35 -11
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
package/src/commands/handler.ts
CHANGED
|
@@ -408,12 +408,19 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
408
408
|
'invoke_provider_script',
|
|
409
409
|
]);
|
|
410
410
|
|
|
411
|
+
// read_chat and get_chat_debug_bundle can serve historical transcript data even
|
|
412
|
+
// when the live session record is gone (stopped/destroyed). Allow the fallback
|
|
413
|
+
// when the provider type is known and any session identity hint is present:
|
|
414
|
+
// an explicit providerSessionId/historySessionId, or the targetSessionId itself
|
|
415
|
+
// (which getHistorySessionId already uses as a fallback history key).
|
|
416
|
+
const isReadOrDebugCmd = cmd === 'read_chat' || cmd === 'get_chat_debug_bundle';
|
|
411
417
|
const allowsInactiveReadChatFallback =
|
|
412
|
-
|
|
418
|
+
isReadOrDebugCmd
|
|
413
419
|
&& !!this._currentRoute.providerType
|
|
414
420
|
&& (
|
|
415
421
|
(typeof args?.providerSessionId === 'string' && args.providerSessionId.trim().length > 0)
|
|
416
422
|
|| (typeof args?.historySessionId === 'string' && args.historySessionId.trim().length > 0)
|
|
423
|
+
|| (typeof args?.targetSessionId === 'string' && args.targetSessionId.trim().length > 0)
|
|
417
424
|
);
|
|
418
425
|
|
|
419
426
|
if (this._currentRoute.sessionLookupFailed && sessionScopedCommands.has(cmd) && !allowsInactiveReadChatFallback) {
|
|
@@ -503,6 +510,11 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
503
510
|
|
|
504
511
|
// ─── Script manage ───────────────────
|
|
505
512
|
case 'refresh_scripts': return this.handleRefreshScripts(args);
|
|
513
|
+
case 'list_provider_availability': return this.handleListProviderAvailability(args);
|
|
514
|
+
case 'install_provider_manifest': return this.handleInstallProviderManifest(args);
|
|
515
|
+
case 'uninstall_provider_manifest': return this.handleUninstallProviderManifest(args);
|
|
516
|
+
case 'check_provider_updates': return this.handleCheckProviderUpdates(args);
|
|
517
|
+
case 'list_installed_providers': return this.handleListInstalledProviders(args);
|
|
506
518
|
|
|
507
519
|
// ─── Stream commands (stream-commands.ts) ───────────
|
|
508
520
|
case 'select_session': return Stream.handleSelectSession(this, args);
|
|
@@ -538,9 +550,15 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
538
550
|
|
|
539
551
|
// ─── Misc (kept in handler — too small to extract) ───────
|
|
540
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Reload providers from disk. Does NOT pull from the registry — the user
|
|
555
|
+
* controls installs explicitly via install_provider_manifest. To upgrade
|
|
556
|
+
* an installed provider, call install_provider_manifest again with the
|
|
557
|
+
* desired version (or with no version to pick up the latest from
|
|
558
|
+
* registry), or use check_provider_updates to see what is out of date.
|
|
559
|
+
*/
|
|
541
560
|
private async handleRefreshScripts(_args: any): Promise<CommandResult> {
|
|
542
561
|
if (this._ctx.providerLoader) {
|
|
543
|
-
await this._ctx.providerLoader.fetchLatest().catch(() => {});
|
|
544
562
|
this._ctx.providerLoader.reload();
|
|
545
563
|
this._ctx.providerLoader.registerToDetector();
|
|
546
564
|
const refreshedInstances = this._ctx.instanceManager
|
|
@@ -556,6 +574,533 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
556
574
|
return { success: false, error: 'ProviderLoader not initialized' };
|
|
557
575
|
}
|
|
558
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Return per-provider availability so a Marketplace UI can show
|
|
579
|
+
* "Installed" badges. Reuses the existing detection state from
|
|
580
|
+
* ProviderLoader.getMachineProviderStatus() — no probing is triggered.
|
|
581
|
+
*/
|
|
582
|
+
private handleListProviderAvailability(_args: any): CommandResult {
|
|
583
|
+
if (!this._ctx.providerLoader) {
|
|
584
|
+
return { success: false, error: 'ProviderLoader not initialized' };
|
|
585
|
+
}
|
|
586
|
+
const loader = this._ctx.providerLoader;
|
|
587
|
+
const items = loader.getAll().map((provider) => {
|
|
588
|
+
const machineConfig = loader.getMachineProviderConfig(provider.type);
|
|
589
|
+
const lastDetection = machineConfig.lastDetection;
|
|
590
|
+
return {
|
|
591
|
+
type: provider.type,
|
|
592
|
+
category: provider.category,
|
|
593
|
+
status: loader.getMachineProviderStatus(provider.type),
|
|
594
|
+
installed: lastDetection?.ok === true,
|
|
595
|
+
detectedPath: lastDetection?.path ?? null,
|
|
596
|
+
checkedAt: lastDetection?.checkedAt ?? null,
|
|
597
|
+
};
|
|
598
|
+
});
|
|
599
|
+
return { success: true, providers: items };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Compute the *Marketplace install root*. This is always
|
|
604
|
+
* `~/.adhdev/marketplace/` regardless of how ProviderLoader resolved its
|
|
605
|
+
* userDir (which can point to a sibling adhdev-providers git checkout in
|
|
606
|
+
* dev). Marketplace-installed manifests must never overwrite files in a
|
|
607
|
+
* developer checkout, and they should be isolated from upstream/registry
|
|
608
|
+
* sync so they survive `refresh_scripts`.
|
|
609
|
+
*/
|
|
610
|
+
private getMarketplaceInstallRoot(): string {
|
|
611
|
+
const os = require('os') as typeof import('os');
|
|
612
|
+
const path = require('path') as typeof import('path');
|
|
613
|
+
return path.join(os.homedir(), '.adhdev', 'marketplace');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Download a single provider manifest from the registry and write it to
|
|
618
|
+
* ~/.adhdev/marketplace/{category}/{type}/provider.json.
|
|
619
|
+
*
|
|
620
|
+
* Used by the Marketplace UI's Install button. Verifies SHA-256 checksum
|
|
621
|
+
* against the registry meta before persisting. Refuses to write outside
|
|
622
|
+
* the marketplace root.
|
|
623
|
+
*
|
|
624
|
+
* Args: { type: string, category?: string, version?: string }
|
|
625
|
+
* If category/version are omitted, looks up the latest from the registry.
|
|
626
|
+
*/
|
|
627
|
+
private async handleInstallProviderManifest(args: any): Promise<CommandResult> {
|
|
628
|
+
if (!this._ctx.providerLoader) {
|
|
629
|
+
return { success: false, error: 'ProviderLoader not initialized' };
|
|
630
|
+
}
|
|
631
|
+
const type = typeof args?.type === 'string' ? args.type : '';
|
|
632
|
+
if (!type) return { success: false, error: 'type is required' };
|
|
633
|
+
// Defense in depth: reject any obvious path-traversal in the type.
|
|
634
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(type)) {
|
|
635
|
+
return { success: false, error: 'invalid type' };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const https = require('https') as typeof import('https');
|
|
639
|
+
const fs = require('fs') as typeof import('fs');
|
|
640
|
+
const path = require('path') as typeof import('path');
|
|
641
|
+
const crypto = require('crypto') as typeof import('crypto');
|
|
642
|
+
const REGISTRY = 'https://api.adhf.dev/api/v1/registry';
|
|
643
|
+
|
|
644
|
+
function fetchText(url: string, timeoutMs: number): Promise<string> {
|
|
645
|
+
return new Promise((resolve, reject) => {
|
|
646
|
+
const req = https.get(url, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: timeoutMs }, (res) => {
|
|
647
|
+
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
|
|
648
|
+
const chunks: Buffer[] = [];
|
|
649
|
+
res.on('data', (c: Buffer) => chunks.push(c));
|
|
650
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
651
|
+
});
|
|
652
|
+
req.on('error', reject);
|
|
653
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
// 1. Look up provider metadata so we know the expected category, version, checksum.
|
|
659
|
+
const metaBody = await fetchText(`${REGISTRY}/providers/${encodeURIComponent(type)}`, 10000);
|
|
660
|
+
const meta = JSON.parse(metaBody) as { type: string; category: string; version: string; checksum: string };
|
|
661
|
+
const category = typeof args?.category === 'string' ? args.category : meta.category;
|
|
662
|
+
const version = typeof args?.version === 'string' ? args.version : meta.version;
|
|
663
|
+
|
|
664
|
+
// Defense in depth on category as well — only known categories.
|
|
665
|
+
if (!['cli', 'ide', 'extension', 'acp'].includes(category)) {
|
|
666
|
+
return { success: false, error: `unknown category: ${category}` };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 2. Download the manifest body.
|
|
670
|
+
const manifestBody = await fetchText(
|
|
671
|
+
`${REGISTRY}/providers/${encodeURIComponent(type)}/${encodeURIComponent(version)}/download`,
|
|
672
|
+
30000
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
// 3. Verify checksum.
|
|
676
|
+
const actualChecksum = crypto.createHash('sha256').update(manifestBody, 'utf-8').digest('hex');
|
|
677
|
+
if (actualChecksum !== meta.checksum) {
|
|
678
|
+
return { success: false, error: `checksum mismatch: expected ${meta.checksum}, got ${actualChecksum}` };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 4. Write to the marketplace install root, NOT to ProviderLoader.getUserDir():
|
|
682
|
+
// in dev, userDir points at the sibling adhdev-providers git checkout.
|
|
683
|
+
const installRoot = this.getMarketplaceInstallRoot();
|
|
684
|
+
const installRootResolved = path.resolve(installRoot);
|
|
685
|
+
const targetDir = path.resolve(path.join(installRoot, category, type));
|
|
686
|
+
if (!targetDir.startsWith(installRootResolved + path.sep)) {
|
|
687
|
+
return { success: false, error: 'install path escaped marketplace root' };
|
|
688
|
+
}
|
|
689
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
690
|
+
// v1 vs v0 manifest selection — v1 manifests carry an SDK
|
|
691
|
+
// $schema URL or v1-only keys (tui, overrides-as-object,
|
|
692
|
+
// source, canonicalHistory). The loader prefers
|
|
693
|
+
// provider.v1.json when both exist, so writing v1 manifests
|
|
694
|
+
// under the v0 name would shadow them. Detect and write to
|
|
695
|
+
// the right file.
|
|
696
|
+
let manifestProbe: Record<string, any> = {};
|
|
697
|
+
try { manifestProbe = JSON.parse(manifestBody) as Record<string, any>; } catch { /* validation below */ }
|
|
698
|
+
const isV1 = typeof manifestProbe?.$schema === 'string' && manifestProbe.$schema.includes('/v1/')
|
|
699
|
+
|| (manifestProbe?.overrides && typeof manifestProbe.overrides === 'object' && !Array.isArray(manifestProbe.overrides))
|
|
700
|
+
|| !!manifestProbe?.tui;
|
|
701
|
+
|
|
702
|
+
// Reject v1 manifests that don't match the schema at install
|
|
703
|
+
// time so the daemon never persists a known-bad manifest.
|
|
704
|
+
// The provider-loader keeps a permissive warn-only behavior
|
|
705
|
+
// for manifests already on disk, but the install path is the
|
|
706
|
+
// right place to fail fast.
|
|
707
|
+
if (isV1 && manifestProbe?.category === 'cli') {
|
|
708
|
+
try {
|
|
709
|
+
const { validateCliProviderManifest, formatManifestValidationIssues } =
|
|
710
|
+
require('../providers/sdk/v1/validators/manifest.js') as typeof import('../providers/sdk/v1/validators/manifest.js');
|
|
711
|
+
const validation = validateCliProviderManifest(manifestProbe);
|
|
712
|
+
if (!validation.ok) {
|
|
713
|
+
return {
|
|
714
|
+
success: false,
|
|
715
|
+
error: `manifest failed v1 schema validation:\n${formatManifestValidationIssues(validation.issues)}`,
|
|
716
|
+
validationIssues: validation.issues,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
} catch (e: any) {
|
|
720
|
+
// Validator load failure shouldn't block install — log
|
|
721
|
+
// and continue. The loader's warn-only path will
|
|
722
|
+
// surface the same issue at boot if it's real.
|
|
723
|
+
LOG.warn('Command', `[install_provider_manifest] schema validator unavailable: ${e?.message || e}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const targetFile = isV1 ? 'provider.v1.json' : 'provider.json';
|
|
728
|
+
const targetPath = path.join(targetDir, targetFile);
|
|
729
|
+
fs.writeFileSync(targetPath, manifestBody, 'utf-8');
|
|
730
|
+
|
|
731
|
+
// 5. If the manifest declares a `source` GitHub repo, fetch the
|
|
732
|
+
// script directories listed in the manifest (defaultScriptDir +
|
|
733
|
+
// each compatibility[].scriptDir). Extended-tier providers
|
|
734
|
+
// bundle their override JS this way — the registry intentionally
|
|
735
|
+
// only stores the manifest JSON, not the script bytes, so a
|
|
736
|
+
// third-party can publish a manifest pointing at their own fork
|
|
737
|
+
// without pushing files into our R2 bucket.
|
|
738
|
+
const manifestJson = JSON.parse(manifestBody) as Record<string, any>;
|
|
739
|
+
const scriptFetch = await this.fetchProviderSources(
|
|
740
|
+
manifestJson,
|
|
741
|
+
category,
|
|
742
|
+
type,
|
|
743
|
+
targetDir,
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
// Hot-reload so the daemon picks up the new manifest.
|
|
747
|
+
this._ctx.providerLoader.reload();
|
|
748
|
+
this._ctx.providerLoader.registerToDetector();
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
success: true,
|
|
752
|
+
installed: {
|
|
753
|
+
type, category, version, checksum: actualChecksum, path: targetPath,
|
|
754
|
+
scriptsFetched: scriptFetch.fetchedCount,
|
|
755
|
+
scriptSource: scriptFetch.source,
|
|
756
|
+
scriptErrors: scriptFetch.errors,
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
} catch (e: any) {
|
|
760
|
+
return { success: false, error: `install failed: ${e?.message || e}` };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* If `manifest.source = { type:'github', repo, ref, subdir? }` is set,
|
|
766
|
+
* walk each script directory the manifest references and download every
|
|
767
|
+
* file from the public GitHub raw endpoint. Returns a small summary so
|
|
768
|
+
* the caller can report what was fetched.
|
|
769
|
+
*
|
|
770
|
+
* Best-effort: failures don't reject the install — the manifest itself is
|
|
771
|
+
* usable for declarative-only providers, and the user still gets a clear
|
|
772
|
+
* error string back if a needed script is missing.
|
|
773
|
+
*/
|
|
774
|
+
private async fetchProviderSources(
|
|
775
|
+
manifest: Record<string, any>,
|
|
776
|
+
category: string,
|
|
777
|
+
type: string,
|
|
778
|
+
targetDir: string,
|
|
779
|
+
): Promise<{ fetchedCount: number; source: string | null; errors: string[] }> {
|
|
780
|
+
const errors: string[] = [];
|
|
781
|
+
const source = manifest?.source;
|
|
782
|
+
if (!source || source.type !== 'github' || typeof source.repo !== 'string' || typeof source.ref !== 'string') {
|
|
783
|
+
return { fetchedCount: 0, source: null, errors };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Collect every script directory the manifest references. v1 manifests
|
|
787
|
+
// use `defaultScriptDir` and `compatibility[].scriptDir`. We also pull
|
|
788
|
+
// any override path's directory (e.g. overrides.detectStatus.path =
|
|
789
|
+
// "scripts/v1/detect_status.js" → fetch the scripts/v1/ directory too).
|
|
790
|
+
const scriptDirs = new Set<string>();
|
|
791
|
+
if (typeof manifest.defaultScriptDir === 'string') scriptDirs.add(manifest.defaultScriptDir);
|
|
792
|
+
if (Array.isArray(manifest.compatibility)) {
|
|
793
|
+
for (const c of manifest.compatibility) {
|
|
794
|
+
if (typeof c?.scriptDir === 'string') scriptDirs.add(c.scriptDir);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (manifest.overrides && typeof manifest.overrides === 'object' && !Array.isArray(manifest.overrides)) {
|
|
798
|
+
for (const override of Object.values(manifest.overrides) as Array<Record<string, unknown>>) {
|
|
799
|
+
const overridePath = override?.path;
|
|
800
|
+
if (typeof overridePath === 'string' && overridePath.includes('/')) {
|
|
801
|
+
const dir = overridePath.substring(0, overridePath.lastIndexOf('/'));
|
|
802
|
+
if (dir) scriptDirs.add(dir);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (scriptDirs.size === 0) {
|
|
807
|
+
return { fetchedCount: 0, source: `${source.repo}@${source.ref}`, errors };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const subdir: string = typeof source.subdir === 'string' && source.subdir.length > 0
|
|
811
|
+
? source.subdir
|
|
812
|
+
: `${category}/${type}`;
|
|
813
|
+
const repo: string = source.repo;
|
|
814
|
+
const ref: string = source.ref;
|
|
815
|
+
|
|
816
|
+
const https = require('https') as typeof import('https');
|
|
817
|
+
const fs = require('fs') as typeof import('fs');
|
|
818
|
+
const path = require('path') as typeof import('path');
|
|
819
|
+
|
|
820
|
+
function fetchJson(url: string, timeoutMs: number): Promise<any> {
|
|
821
|
+
return new Promise((resolve, reject) => {
|
|
822
|
+
const req = https.get(url, {
|
|
823
|
+
headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/vnd.github+json' },
|
|
824
|
+
timeout: timeoutMs,
|
|
825
|
+
}, (res) => {
|
|
826
|
+
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
|
|
827
|
+
const chunks: Buffer[] = [];
|
|
828
|
+
res.on('data', (c: Buffer) => chunks.push(c));
|
|
829
|
+
res.on('end', () => {
|
|
830
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8'))); }
|
|
831
|
+
catch (e) { reject(e); }
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
req.on('error', reject);
|
|
835
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function fetchBinary(url: string, timeoutMs: number): Promise<Buffer> {
|
|
840
|
+
return new Promise((resolve, reject) => {
|
|
841
|
+
const req = https.get(url, {
|
|
842
|
+
headers: { 'User-Agent': 'adhdev-daemon' },
|
|
843
|
+
timeout: timeoutMs,
|
|
844
|
+
}, (res) => {
|
|
845
|
+
// Raw endpoint redirects through codeload — follow the redirect.
|
|
846
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
847
|
+
if (res.headers.location) {
|
|
848
|
+
return fetchBinary(res.headers.location, timeoutMs).then(resolve, reject);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
|
|
852
|
+
const chunks: Buffer[] = [];
|
|
853
|
+
res.on('data', (c: Buffer) => chunks.push(c));
|
|
854
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
855
|
+
});
|
|
856
|
+
req.on('error', reject);
|
|
857
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
let fetchedCount = 0;
|
|
862
|
+
|
|
863
|
+
// The shared helpers directory (cli/_shared/) is referenced by most
|
|
864
|
+
// CLI providers via `require('../../../_shared/...')`. Fetch it once
|
|
865
|
+
// alongside the provider's own scripts so node's require resolution
|
|
866
|
+
// succeeds at runtime. Best-effort — silent if the source repo has no
|
|
867
|
+
// _shared dir (e.g. ACP-only repos).
|
|
868
|
+
const sharedDirRel = `${category}/_shared`;
|
|
869
|
+
const sharedTargetDir = path.resolve(path.join(targetDir, '../_shared'));
|
|
870
|
+
const installRootResolved = path.resolve(path.join(targetDir, '../..'));
|
|
871
|
+
if (sharedTargetDir.startsWith(installRootResolved + path.sep)) {
|
|
872
|
+
const sharedStack: string[] = [sharedDirRel];
|
|
873
|
+
while (sharedStack.length) {
|
|
874
|
+
const relDir = sharedStack.pop()!;
|
|
875
|
+
const apiUrl = `https://api.github.com/repos/${repo}/contents/${encodeURI(relDir)}?ref=${encodeURIComponent(ref)}`;
|
|
876
|
+
let entries: Array<{ type: string; path: string; name: string; download_url: string | null }>;
|
|
877
|
+
try {
|
|
878
|
+
entries = await fetchJson(apiUrl, 15000);
|
|
879
|
+
} catch (e: any) {
|
|
880
|
+
// Silent: _shared may not exist on third-party repos
|
|
881
|
+
if (relDir === sharedDirRel) break;
|
|
882
|
+
errors.push(`list shared ${relDir}: ${e?.message ?? e}`);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (!Array.isArray(entries)) continue;
|
|
886
|
+
for (const entry of entries) {
|
|
887
|
+
if (entry.type === 'dir') { sharedStack.push(entry.path); continue; }
|
|
888
|
+
if (entry.type !== 'file' || !entry.download_url) continue;
|
|
889
|
+
try {
|
|
890
|
+
const body = await fetchBinary(entry.download_url, 30000);
|
|
891
|
+
// entry.path is like 'cli/_shared/foo.js' — strip 'cli/_shared/' prefix
|
|
892
|
+
const relInside = entry.path.startsWith(sharedDirRel + '/')
|
|
893
|
+
? entry.path.slice(sharedDirRel.length + 1)
|
|
894
|
+
: entry.path;
|
|
895
|
+
const outPath = path.resolve(path.join(sharedTargetDir, relInside));
|
|
896
|
+
if (!outPath.startsWith(path.resolve(sharedTargetDir) + path.sep)) continue;
|
|
897
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
898
|
+
fs.writeFileSync(outPath, body);
|
|
899
|
+
fetchedCount++;
|
|
900
|
+
} catch (e: any) {
|
|
901
|
+
errors.push(`fetch shared ${entry.path}: ${e?.message ?? e}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
for (const scriptDir of scriptDirs) {
|
|
908
|
+
// GitHub Contents API returns the file list under the dir. We
|
|
909
|
+
// recurse into subdirectories so e.g. scripts/v1/helpers/foo.js is
|
|
910
|
+
// captured too.
|
|
911
|
+
const stack: string[] = [`${subdir}/${scriptDir}`];
|
|
912
|
+
while (stack.length) {
|
|
913
|
+
const relDir = stack.pop()!;
|
|
914
|
+
const apiUrl = `https://api.github.com/repos/${repo}/contents/${encodeURI(relDir)}?ref=${encodeURIComponent(ref)}`;
|
|
915
|
+
let entries: Array<{ type: string; path: string; name: string; download_url: string | null }>;
|
|
916
|
+
try {
|
|
917
|
+
entries = await fetchJson(apiUrl, 15000);
|
|
918
|
+
} catch (e: any) {
|
|
919
|
+
errors.push(`list ${relDir}: ${e?.message ?? e}`);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (!Array.isArray(entries)) {
|
|
923
|
+
errors.push(`list ${relDir}: unexpected response shape`);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
for (const entry of entries) {
|
|
927
|
+
if (entry.type === 'dir') {
|
|
928
|
+
stack.push(entry.path);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (entry.type !== 'file' || !entry.download_url) continue;
|
|
932
|
+
try {
|
|
933
|
+
const body = await fetchBinary(entry.download_url, 30000);
|
|
934
|
+
// entry.path is relative to repo root → strip the repo subdir prefix
|
|
935
|
+
// so the path inside targetDir matches the layout the loader expects.
|
|
936
|
+
const relInsideProvider = entry.path.startsWith(subdir + '/')
|
|
937
|
+
? entry.path.slice(subdir.length + 1)
|
|
938
|
+
: entry.path;
|
|
939
|
+
const outPath = path.resolve(path.join(targetDir, relInsideProvider));
|
|
940
|
+
// Path-traversal guard.
|
|
941
|
+
if (!outPath.startsWith(path.resolve(targetDir) + path.sep)) {
|
|
942
|
+
errors.push(`refusing to write outside targetDir: ${entry.path}`);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
946
|
+
fs.writeFileSync(outPath, body);
|
|
947
|
+
fetchedCount++;
|
|
948
|
+
} catch (e: any) {
|
|
949
|
+
errors.push(`fetch ${entry.path}: ${e?.message ?? e}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return { fetchedCount, source: `${repo}@${ref}`, errors };
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Remove a provider manifest from the marketplace install root
|
|
960
|
+
* (~/.adhdev/marketplace/{category}/{type}/). Refuses to touch anything
|
|
961
|
+
* outside that root.
|
|
962
|
+
*/
|
|
963
|
+
private async handleUninstallProviderManifest(args: any): Promise<CommandResult> {
|
|
964
|
+
const type = typeof args?.type === 'string' ? args.type : '';
|
|
965
|
+
const category = typeof args?.category === 'string' ? args.category : '';
|
|
966
|
+
if (!type || !category) return { success: false, error: 'type and category are required' };
|
|
967
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(type)) {
|
|
968
|
+
return { success: false, error: 'invalid type' };
|
|
969
|
+
}
|
|
970
|
+
if (!['cli', 'ide', 'extension', 'acp'].includes(category)) {
|
|
971
|
+
return { success: false, error: `unknown category: ${category}` };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const fs = require('fs') as typeof import('fs');
|
|
975
|
+
const path = require('path') as typeof import('path');
|
|
976
|
+
|
|
977
|
+
try {
|
|
978
|
+
const installRoot = this.getMarketplaceInstallRoot();
|
|
979
|
+
const installRootResolved = path.resolve(installRoot);
|
|
980
|
+
const targetDir = path.resolve(path.join(installRoot, category, type));
|
|
981
|
+
|
|
982
|
+
if (!targetDir.startsWith(installRootResolved + path.sep)) {
|
|
983
|
+
return { success: false, error: 'refusing to delete outside marketplace root' };
|
|
984
|
+
}
|
|
985
|
+
if (!fs.existsSync(targetDir)) {
|
|
986
|
+
return { success: false, error: 'not installed' };
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
990
|
+
|
|
991
|
+
if (this._ctx.providerLoader) {
|
|
992
|
+
this._ctx.providerLoader.reload();
|
|
993
|
+
this._ctx.providerLoader.registerToDetector();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return { success: true, removed: { type, category, path: targetDir } };
|
|
997
|
+
} catch (e: any) {
|
|
998
|
+
return { success: false, error: `uninstall failed: ${e?.message || e}` };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Return everything currently installed in ~/.adhdev/marketplace/ with its
|
|
1004
|
+
* version. This is the "what does this daemon have" answer used both by
|
|
1005
|
+
* the UI and by the update checker.
|
|
1006
|
+
*/
|
|
1007
|
+
private handleListInstalledProviders(_args: any): CommandResult {
|
|
1008
|
+
const fs = require('fs') as typeof import('fs');
|
|
1009
|
+
const path = require('path') as typeof import('path');
|
|
1010
|
+
|
|
1011
|
+
const installRoot = this.getMarketplaceInstallRoot();
|
|
1012
|
+
if (!fs.existsSync(installRoot)) return { success: true, providers: [] };
|
|
1013
|
+
|
|
1014
|
+
const CATEGORIES = ['cli', 'ide', 'extension', 'acp'] as const;
|
|
1015
|
+
const items: Array<{ type: string; category: string; version: string; path: string }> = [];
|
|
1016
|
+
|
|
1017
|
+
for (const category of CATEGORIES) {
|
|
1018
|
+
const categoryDir = path.join(installRoot, category);
|
|
1019
|
+
if (!fs.existsSync(categoryDir)) continue;
|
|
1020
|
+
let entries: string[];
|
|
1021
|
+
try { entries = fs.readdirSync(categoryDir); } catch { continue; }
|
|
1022
|
+
for (const type of entries) {
|
|
1023
|
+
// v1 manifest takes precedence over v0 when both are present.
|
|
1024
|
+
const v1Path = path.join(categoryDir, type, 'provider.v1.json');
|
|
1025
|
+
const v0Path = path.join(categoryDir, type, 'provider.json');
|
|
1026
|
+
const manifestPath = fs.existsSync(v1Path) ? v1Path : (fs.existsSync(v0Path) ? v0Path : null);
|
|
1027
|
+
if (!manifestPath) continue;
|
|
1028
|
+
try {
|
|
1029
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
1030
|
+
items.push({
|
|
1031
|
+
type,
|
|
1032
|
+
category,
|
|
1033
|
+
version: typeof m.providerVersion === 'string' ? m.providerVersion : '0.0.0',
|
|
1034
|
+
path: manifestPath,
|
|
1035
|
+
});
|
|
1036
|
+
} catch {
|
|
1037
|
+
// Corrupt manifest — skip but don't fail the whole listing.
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return { success: true, providers: items };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* For each installed provider, ask the registry for its current latest
|
|
1046
|
+
* version and report whether an update is available. The user can then
|
|
1047
|
+
* call install_provider_manifest to upgrade (it overwrites the file).
|
|
1048
|
+
*
|
|
1049
|
+
* Returns { providers: [{ type, category, installedVersion, latestVersion,
|
|
1050
|
+
* updateAvailable, error? }] }
|
|
1051
|
+
*/
|
|
1052
|
+
private async handleCheckProviderUpdates(_args: any): Promise<CommandResult> {
|
|
1053
|
+
const installed = this.handleListInstalledProviders({});
|
|
1054
|
+
if (!installed.success) return installed;
|
|
1055
|
+
|
|
1056
|
+
const https = require('https') as typeof import('https');
|
|
1057
|
+
const REGISTRY = 'https://api.adhf.dev/api/v1/registry';
|
|
1058
|
+
|
|
1059
|
+
function fetchJson(url: string): Promise<any> {
|
|
1060
|
+
return new Promise((resolve, reject) => {
|
|
1061
|
+
const req = https.get(url, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 10000 }, (res) => {
|
|
1062
|
+
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
|
|
1063
|
+
const chunks: Buffer[] = [];
|
|
1064
|
+
res.on('data', (c: Buffer) => chunks.push(c));
|
|
1065
|
+
res.on('end', () => {
|
|
1066
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8'))); }
|
|
1067
|
+
catch (e) { reject(e); }
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
req.on('error', reject);
|
|
1071
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const installedList = (installed as unknown as { providers: Array<{ type: string; category: string; version: string }> }).providers;
|
|
1076
|
+
const checks = await Promise.all(
|
|
1077
|
+
installedList.map(async (p) => {
|
|
1078
|
+
try {
|
|
1079
|
+
const remote = await fetchJson(`${REGISTRY}/providers/${encodeURIComponent(p.type)}`);
|
|
1080
|
+
const latestVersion = String(remote?.version ?? '');
|
|
1081
|
+
return {
|
|
1082
|
+
type: p.type,
|
|
1083
|
+
category: p.category,
|
|
1084
|
+
installedVersion: p.version,
|
|
1085
|
+
latestVersion,
|
|
1086
|
+
updateAvailable: latestVersion !== '' && latestVersion !== p.version,
|
|
1087
|
+
};
|
|
1088
|
+
} catch (e: any) {
|
|
1089
|
+
return {
|
|
1090
|
+
type: p.type,
|
|
1091
|
+
category: p.category,
|
|
1092
|
+
installedVersion: p.version,
|
|
1093
|
+
latestVersion: null,
|
|
1094
|
+
updateAvailable: false,
|
|
1095
|
+
error: e?.message ?? String(e),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
})
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
return { success: true, providers: checks };
|
|
1102
|
+
}
|
|
1103
|
+
|
|
559
1104
|
// ─── DevServer HTTP proxy helpers ─────────────────
|
|
560
1105
|
// These bridge WS commands to the DevServer REST API (localhost:19280)
|
|
561
1106
|
|