@adhdev/daemon-core 0.9.82-rc.164 → 0.9.82-rc.166
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/commands/handler.d.ts +61 -17
- package/dist/index.js +1335 -764
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1333 -762
- package/dist/index.mjs.map +1 -1
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/provider-loader.d.ts +7 -3
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/sdk/v1/index.d.ts +1 -1
- package/dist/providers/sdk/v1/validators/manifest.d.ts +1 -1
- package/dist/providers/sdk/v1/validators/taint.d.ts +1 -1
- package/dist/providers/spec/cli-adapter.d.ts +9 -0
- package/dist/shared-types.d.ts +27 -1
- package/package.json +1 -1
- package/src/commands/cli-manager.ts +19 -0
- package/src/commands/handler.ts +286 -24
- package/src/providers/cli-provider-instance.ts +9 -1
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/provider-loader.ts +180 -34
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/sdk/v1/index.ts +1 -1
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +1 -1
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +4 -3
- package/src/providers/sdk/v1/validators/manifest.ts +1 -1
- package/src/providers/sdk/v1/validators/taint.ts +1 -1
- package/src/providers/spec/cli-adapter.ts +9 -0
- package/src/providers/spec/native-history-executor.ts +13 -2
- package/src/shared-types.ts +33 -1
- package/src/status/snapshot.ts +49 -14
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface ExternalSource {
|
|
2
|
+
/** Unique short identifier, e.g. "@vendor-x". User-supplied or derived from the URL. */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Full git URL (https://, git@, …). */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Branch / tag / commit-ish to track. Defaults to `main`. */
|
|
7
|
+
ref: string;
|
|
8
|
+
/** ISO timestamp when first registered. */
|
|
9
|
+
addedAt: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ExternalSourcesFile {
|
|
12
|
+
/** Schema version — bump if shape changes. */
|
|
13
|
+
schema: 1;
|
|
14
|
+
sources: ExternalSource[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Per-type active source selection. Only types reachable from more than
|
|
18
|
+
* one source need an entry — single-source types have no ambiguity.
|
|
19
|
+
*
|
|
20
|
+
* Shape: { active: { "<type>": "<source-name>" } }
|
|
21
|
+
*/
|
|
22
|
+
export interface ProvidersActiveFile {
|
|
23
|
+
schema: 1;
|
|
24
|
+
active: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
export declare function externalRoot(): string;
|
|
27
|
+
export declare function sourcesFilePath(): string;
|
|
28
|
+
export declare function activeFilePath(): string;
|
|
29
|
+
export declare function loadExternalSources(): ExternalSourcesFile;
|
|
30
|
+
export declare function saveExternalSources(file: ExternalSourcesFile): void;
|
|
31
|
+
export declare function loadProvidersActive(): ProvidersActiveFile;
|
|
32
|
+
export declare function saveProvidersActive(file: ProvidersActiveFile): void;
|
|
33
|
+
/**
|
|
34
|
+
* Derive a short identifier from a git URL when the user didn't supply one.
|
|
35
|
+
* https://github.com/vendor/extra-providers.git → "@vendor-extra-providers"
|
|
36
|
+
* git@github.com:vendor/extra-providers → "@vendor-extra-providers"
|
|
37
|
+
*
|
|
38
|
+
* Idempotent; safe to call before validation since it produces a string
|
|
39
|
+
* regardless of input shape.
|
|
40
|
+
*/
|
|
41
|
+
export declare function deriveSourceName(url: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Return the per-source category/type tree currently on disk under
|
|
44
|
+
* ~/.adhdev/external/<source>/. Used by the conflict detector and by
|
|
45
|
+
* list_provider_sources.
|
|
46
|
+
*/
|
|
47
|
+
export interface SourceInventoryEntry {
|
|
48
|
+
sourceName: string;
|
|
49
|
+
/** Map: category → list of provider types. */
|
|
50
|
+
providers: Record<string, string[]>;
|
|
51
|
+
}
|
|
52
|
+
export declare function inventoryExternalSources(): SourceInventoryEntry[];
|
|
53
|
+
/**
|
|
54
|
+
* For a given category+type, list every source that currently exposes it.
|
|
55
|
+
* Returns source names in disk-walk order; callers can use the first one
|
|
56
|
+
* when no explicit active selection exists.
|
|
57
|
+
*/
|
|
58
|
+
export declare function sourcesProviding(category: string, type: string): string[];
|
|
59
|
+
/**
|
|
60
|
+
* Resolve which source should be active for a given category+type.
|
|
61
|
+
* - If exactly one source provides it → that source.
|
|
62
|
+
* - If multiple sources provide it → the one named in providers-active.json
|
|
63
|
+
* (when present) or null (ambiguous; loader should warn and pick
|
|
64
|
+
* the first deterministically so daemon doesn't refuse to boot).
|
|
65
|
+
* - If none → null.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveActiveSource(category: string, type: string, activeFile?: ProvidersActiveFile): {
|
|
68
|
+
source: string | null;
|
|
69
|
+
ambiguous: boolean;
|
|
70
|
+
candidates: string[];
|
|
71
|
+
};
|
|
@@ -86,6 +86,7 @@ export declare class ProviderLoader {
|
|
|
86
86
|
*/
|
|
87
87
|
probeStarts?: string[];
|
|
88
88
|
});
|
|
89
|
+
private migrateMarketplaceDirToExternal;
|
|
89
90
|
private log;
|
|
90
91
|
private debugLog;
|
|
91
92
|
/**
|
|
@@ -130,9 +131,12 @@ export declare class ProviderLoader {
|
|
|
130
131
|
resolveProviderFile(type: string, ...segments: string[]): string | null;
|
|
131
132
|
/**
|
|
132
133
|
* Load all providers (3-tier priority)
|
|
133
|
-
* 1.
|
|
134
|
-
* 2.
|
|
135
|
-
*
|
|
134
|
+
* 1. ~/.adhdev/providers/.upstream/ — official git, auto-synced
|
|
135
|
+
* 2. ~/.adhdev/external/ — 3rd-party git sources, user-added,
|
|
136
|
+
* bundled providers may include arbitrary JS (untrusted by default)
|
|
137
|
+
* 3. ~/.adhdev/providers/ (excluding .upstream) — user-authored customs,
|
|
138
|
+
* always wins
|
|
139
|
+
* Highest priority listed last (overwrites earlier loads).
|
|
136
140
|
* If .upstream/ is empty, call fetchLatest() before loadAll().
|
|
137
141
|
*/
|
|
138
142
|
loadAll(): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ProviderTrust = 'user-custom' | 'trusted' | 'trusted-with-scripts' | 'external-safe' | 'external-untrusted';
|
|
2
|
+
export type ProviderLayer = 'user' | 'upstream' | 'external';
|
|
3
|
+
export interface ProviderManifestShape {
|
|
4
|
+
/** Has a `tui` block — SDK builders consume it as code paths. */
|
|
5
|
+
hasTui: boolean;
|
|
6
|
+
/** Has a non-empty `overrides` object — JS override paths. */
|
|
7
|
+
hasOverrides: boolean;
|
|
8
|
+
/** compatibility[].scriptDir or defaultScriptDir is set — JS scripts dir. */
|
|
9
|
+
hasScriptDir: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Inspect a manifest to decide whether it ships JavaScript hooks.
|
|
13
|
+
* Cheap; pure; safe to call on every provider load.
|
|
14
|
+
*/
|
|
15
|
+
export declare function inspectManifestShape(manifest: Record<string, unknown>): ProviderManifestShape;
|
|
16
|
+
/**
|
|
17
|
+
* Classify trust given the layer the provider was loaded from + the
|
|
18
|
+
* manifest's JS-hook footprint.
|
|
19
|
+
*/
|
|
20
|
+
export declare function classifyTrust(layer: ProviderLayer, shape: ProviderManifestShape): ProviderTrust;
|
|
21
|
+
/**
|
|
22
|
+
* Returns true when activation should require an explicit user confirm.
|
|
23
|
+
* Today only `external-untrusted` qualifies; future trust levels may
|
|
24
|
+
* fold in here.
|
|
25
|
+
*/
|
|
26
|
+
export declare function requiresConfirmation(trust: ProviderTrust): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Render a short human-readable rationale for the trust tag. Used by
|
|
29
|
+
* the dashboard's confirmation modal and the provider catalog tooltip.
|
|
30
|
+
*/
|
|
31
|
+
export declare function describeTrust(trust: ProviderTrust): string;
|
|
@@ -24,7 +24,7 @@ export declare const V1_PRIMITIVE_CATALOG: Readonly<{
|
|
|
24
24
|
readonly common: readonly ["adhdev:common/setting-boolean@1", "adhdev:common/setting-number@1", "adhdev:common/setting-string@1", "adhdev:common/setting-select@1", "adhdev:common/capability-input@1", "adhdev:common/capability-output@1", "adhdev:common/capability-controls@1", "adhdev:common/auth-env-var@1", "adhdev:common/auth-cli-command@1", "adhdev:common/spawn@1", "adhdev:common/timeouts@1", "adhdev:common/resume@1", "adhdev:common/mesh-coordinator@1"];
|
|
25
25
|
readonly override: readonly ["adhdev:override/cli-parse-session@1", "adhdev:override/cli-detect-status@1", "adhdev:override/cli-parse-approval@1", "adhdev:override/cli-parse-output@1", "adhdev:override/cli-read-native-history@1", "adhdev:override/cli-list-native-history@1", "adhdev:override/cli-capability-handler@1"];
|
|
26
26
|
}>;
|
|
27
|
-
/** Aggregate flat list — for
|
|
27
|
+
/** Aggregate flat list — for provider catalog endpoints. */
|
|
28
28
|
export declare const V1_ALL_PRIMITIVES: ReadonlyArray<string>;
|
|
29
29
|
/** Catalog version label exposed at `registry.adhf.dev/primitives`. */
|
|
30
30
|
export declare const V1_CONTRACT_VERSION: "1.0.0";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* offending field without guessing.
|
|
10
10
|
*
|
|
11
11
|
* Validation lives in the SDK layer (not in provider-loader) so dashboards,
|
|
12
|
-
* registry workers, and the
|
|
12
|
+
* registry workers, and the provider publish flow can all reuse the
|
|
13
13
|
* same code path and produce identical error messages.
|
|
14
14
|
*/
|
|
15
15
|
export interface ManifestValidationIssue {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Static taint analyzer for extended-tier override JS.
|
|
3
3
|
*
|
|
4
4
|
* Goal: classify the override JS shipped with an extended-tier provider into
|
|
5
|
-
* one of three risk tiers, so the
|
|
5
|
+
* one of three risk tiers, so the dashboard + daemon trust prompt can
|
|
6
6
|
* surface accurate language to the operator instead of a single generic
|
|
7
7
|
* "this provider contains JS" warning.
|
|
8
8
|
*
|
|
@@ -4,6 +4,15 @@ export declare class SpecCliAdapter implements CliAdapter {
|
|
|
4
4
|
readonly cliType: string;
|
|
5
5
|
readonly cliName: string;
|
|
6
6
|
readonly workingDir: string;
|
|
7
|
+
/**
|
|
8
|
+
* Marker the daemon's finalization gate checks: `getStatus()` returns
|
|
9
|
+
* `messages: []` by design here (chat history lives in the daemon's
|
|
10
|
+
* native-history pipeline, not the adapter). Without this flag,
|
|
11
|
+
* cli-provider-instance's `missing_final_assistant` gate would stall
|
|
12
|
+
* every turn until the 30s safety timeout because it expects the
|
|
13
|
+
* adapter to surface the final assistant message.
|
|
14
|
+
*/
|
|
15
|
+
readonly chatMessagesOwnedExternally: true;
|
|
7
16
|
private driver;
|
|
8
17
|
private spec;
|
|
9
18
|
private lastEvent;
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -422,7 +422,33 @@ export interface AvailableProviderInfo {
|
|
|
422
422
|
lastVerification?: MachineProviderCheckResult;
|
|
423
423
|
/** Provider-declared Repo Mesh coordinator/MCP behavior. */
|
|
424
424
|
meshCoordinator?: ProviderMeshCoordinatorConfig;
|
|
425
|
-
|
|
425
|
+
/**
|
|
426
|
+
* Provider trust classification — derived from the on-disk layer the
|
|
427
|
+
* manifest came from and the shape of the manifest. Dashboards use
|
|
428
|
+
* this to render a trust badge and gate activation of
|
|
429
|
+
* `external-untrusted` providers behind a confirm modal.
|
|
430
|
+
*/
|
|
431
|
+
trust?: ProviderTrust;
|
|
432
|
+
/** Daemon-side human-readable description of the trust value. */
|
|
433
|
+
trustDescription?: string;
|
|
434
|
+
/** True when activation needs a user-side confirmation step. */
|
|
435
|
+
requiresConfirmation?: boolean;
|
|
436
|
+
/** Which on-disk layer the manifest lives in. */
|
|
437
|
+
sourceLayer?: 'user' | 'upstream' | 'external';
|
|
438
|
+
/** For external providers, the source-name namespace it came from. */
|
|
439
|
+
sourceName?: string | null;
|
|
440
|
+
/** Manifest-declared version, e.g. "1.2.1". */
|
|
441
|
+
providerVersion?: string;
|
|
442
|
+
/** Underlying executable name (CLI/binary providers). */
|
|
443
|
+
binary?: string;
|
|
444
|
+
/** Lifecycle label from the manifest: "Stable", "Beta", … */
|
|
445
|
+
status?: string;
|
|
446
|
+
/** One-line provider description from the manifest. */
|
|
447
|
+
details?: string;
|
|
448
|
+
/** Manifest-declared links: homepage, docs, repo, … */
|
|
449
|
+
links?: Record<string, string>;
|
|
450
|
+
}
|
|
451
|
+
export type ProviderTrust = 'user-custom' | 'trusted' | 'trusted-with-scripts' | 'external-safe' | 'external-untrusted';
|
|
426
452
|
export interface MachineProviderCheckResult {
|
|
427
453
|
ok: boolean;
|
|
428
454
|
stage?: 'detection' | 'runnable' | 'verification';
|
package/package.json
CHANGED
|
@@ -1099,6 +1099,25 @@ export class DaemonCliManager {
|
|
|
1099
1099
|
env: args?.env,
|
|
1100
1100
|
})
|
|
1101
1101
|
: null;
|
|
1102
|
+
// Untrusted-provider gate: an external source that ships JS
|
|
1103
|
+
// hooks needs explicit user confirmation before its first
|
|
1104
|
+
// launch. Dashboards add `confirmExternalUntrusted: true` to
|
|
1105
|
+
// the launch args after showing the trust modal. Without
|
|
1106
|
+
// that ack we refuse to spawn and tell the caller why.
|
|
1107
|
+
const provLookup = this.providerLoader.getMeta(this.providerLoader.resolveAlias(cliType)) as any;
|
|
1108
|
+
const provTrust = provLookup?._sourceTrust;
|
|
1109
|
+
if (provTrust === 'external-untrusted' && args?.confirmExternalUntrusted !== true) {
|
|
1110
|
+
return {
|
|
1111
|
+
success: false,
|
|
1112
|
+
error: 'untrusted_external_provider',
|
|
1113
|
+
provider: {
|
|
1114
|
+
type: provLookup?.type ?? cliType,
|
|
1115
|
+
sourceName: provLookup?._sourceName ?? null,
|
|
1116
|
+
trust: provTrust,
|
|
1117
|
+
},
|
|
1118
|
+
hint: 'Resend launch_cli with confirmExternalUntrusted=true after the user explicitly approves running JavaScript from this 3rd-party source.',
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1102
1121
|
const started = await this.startSession(
|
|
1103
1122
|
cliType,
|
|
1104
1123
|
dir,
|
package/src/commands/handler.ts
CHANGED
|
@@ -515,6 +515,10 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
515
515
|
case 'uninstall_provider_manifest': return this.handleUninstallProviderManifest(args);
|
|
516
516
|
case 'check_provider_updates': return this.handleCheckProviderUpdates(args);
|
|
517
517
|
case 'list_installed_providers': return this.handleListInstalledProviders(args);
|
|
518
|
+
case 'add_provider_source': return this.handleAddProviderSource(args);
|
|
519
|
+
case 'remove_provider_source': return this.handleRemoveProviderSource(args);
|
|
520
|
+
case 'list_provider_sources': return this.handleListProviderSources(args);
|
|
521
|
+
case 'set_active_provider_source': return this.handleSetActiveProviderSource(args);
|
|
518
522
|
|
|
519
523
|
// ─── Stream commands (stream-commands.ts) ───────────
|
|
520
524
|
case 'select_session': return Stream.handleSelectSession(this, args);
|
|
@@ -575,18 +579,23 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
575
579
|
}
|
|
576
580
|
|
|
577
581
|
/**
|
|
578
|
-
* Return per-provider availability so
|
|
579
|
-
* "Installed" badges. Reuses the existing detection state from
|
|
582
|
+
* Return per-provider availability so the dashboard's provider catalog
|
|
583
|
+
* can show "Installed" badges. Reuses the existing detection state from
|
|
580
584
|
* ProviderLoader.getMachineProviderStatus() — no probing is triggered.
|
|
581
585
|
*/
|
|
582
586
|
private handleListProviderAvailability(_args: any): CommandResult {
|
|
583
587
|
if (!this._ctx.providerLoader) {
|
|
584
588
|
return { success: false, error: 'ProviderLoader not initialized' };
|
|
585
589
|
}
|
|
590
|
+
const { describeTrust, requiresConfirmation } =
|
|
591
|
+
require('../providers/provider-trust.js') as typeof import('../providers/provider-trust.js');
|
|
586
592
|
const loader = this._ctx.providerLoader;
|
|
587
593
|
const items = loader.getAll().map((provider) => {
|
|
588
594
|
const machineConfig = loader.getMachineProviderConfig(provider.type);
|
|
589
595
|
const lastDetection = machineConfig.lastDetection;
|
|
596
|
+
const trust = (provider as any)._sourceTrust ?? 'trusted';
|
|
597
|
+
const layer = (provider as any)._sourceLayer ?? 'upstream';
|
|
598
|
+
const sourceName = (provider as any)._sourceName ?? null;
|
|
590
599
|
return {
|
|
591
600
|
type: provider.type,
|
|
592
601
|
category: provider.category,
|
|
@@ -594,32 +603,41 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
594
603
|
installed: lastDetection?.ok === true,
|
|
595
604
|
detectedPath: lastDetection?.path ?? null,
|
|
596
605
|
checkedAt: lastDetection?.checkedAt ?? null,
|
|
606
|
+
trust,
|
|
607
|
+
trustDescription: describeTrust(trust),
|
|
608
|
+
requiresConfirmation: requiresConfirmation(trust),
|
|
609
|
+
sourceLayer: layer,
|
|
610
|
+
sourceName,
|
|
597
611
|
};
|
|
598
612
|
});
|
|
599
613
|
return { success: true, providers: items };
|
|
600
614
|
}
|
|
601
615
|
|
|
602
616
|
/**
|
|
603
|
-
* Compute the *
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
617
|
+
* Compute the *upstream cache root*. install_provider_manifest writes
|
|
618
|
+
* official-registry manifests here so the daemon's standard upstream
|
|
619
|
+
* layer picks them up — no special handling needed at load time, and
|
|
620
|
+
* the manifests inherit the official-trust badge instead of the
|
|
621
|
+
* untrusted-external one.
|
|
622
|
+
*
|
|
623
|
+
* Path matches ProviderLoader.upstreamDir but we recompute it from
|
|
624
|
+
* homedir() so this method stays usable in dev where userDir can
|
|
625
|
+
* point at a sibling git checkout.
|
|
609
626
|
*/
|
|
610
|
-
private
|
|
627
|
+
private getUpstreamInstallRoot(): string {
|
|
611
628
|
const os = require('os') as typeof import('os');
|
|
612
629
|
const path = require('path') as typeof import('path');
|
|
613
|
-
return path.join(os.homedir(), '.adhdev', '
|
|
630
|
+
return path.join(os.homedir(), '.adhdev', 'providers', '.upstream');
|
|
614
631
|
}
|
|
615
632
|
|
|
616
633
|
/**
|
|
617
634
|
* Download a single provider manifest from the registry and write it to
|
|
618
|
-
* ~/.adhdev/
|
|
635
|
+
* ~/.adhdev/providers/.upstream/{category}/{type}/provider.json.
|
|
619
636
|
*
|
|
620
|
-
* Used by
|
|
621
|
-
*
|
|
622
|
-
* the
|
|
637
|
+
* Used by standalone onboarding to seed the upstream cache with the
|
|
638
|
+
* default provider set on first launch. Verifies SHA-256 checksum
|
|
639
|
+
* against the registry meta before persisting. Refuses to write
|
|
640
|
+
* outside the upstream root.
|
|
623
641
|
*
|
|
624
642
|
* Args: { type: string, category?: string, version?: string }
|
|
625
643
|
* If category/version are omitted, looks up the latest from the registry.
|
|
@@ -678,13 +696,13 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
678
696
|
return { success: false, error: `checksum mismatch: expected ${meta.checksum}, got ${actualChecksum}` };
|
|
679
697
|
}
|
|
680
698
|
|
|
681
|
-
// 4. Write to the
|
|
699
|
+
// 4. Write to the upstream cache root, NOT to ProviderLoader.getUserDir():
|
|
682
700
|
// in dev, userDir points at the sibling adhdev-providers git checkout.
|
|
683
|
-
const installRoot = this.
|
|
701
|
+
const installRoot = this.getUpstreamInstallRoot();
|
|
684
702
|
const installRootResolved = path.resolve(installRoot);
|
|
685
703
|
const targetDir = path.resolve(path.join(installRoot, category, type));
|
|
686
704
|
if (!targetDir.startsWith(installRootResolved + path.sep)) {
|
|
687
|
-
return { success: false, error: 'install path escaped
|
|
705
|
+
return { success: false, error: 'install path escaped upstream root' };
|
|
688
706
|
}
|
|
689
707
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
690
708
|
// v1 vs v0 manifest selection — v1 manifests carry an SDK
|
|
@@ -792,6 +810,17 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
792
810
|
if (Array.isArray(manifest.compatibility)) {
|
|
793
811
|
for (const c of manifest.compatibility) {
|
|
794
812
|
if (typeof c?.scriptDir === 'string') scriptDirs.add(c.scriptDir);
|
|
813
|
+
// Spec-driven providers (claude/codex/agy/…) point at a
|
|
814
|
+
// single specs/<version>.json instead of a scriptDir.
|
|
815
|
+
// The whole specs/ directory needs to come down so the
|
|
816
|
+
// spec adapter can resolve the file at runtime — without
|
|
817
|
+
// this, install_provider_manifest leaves the marketplace
|
|
818
|
+
// copy spec-less and provider-loader falls back to the
|
|
819
|
+
// legacy tui-based ProviderCliAdapter.
|
|
820
|
+
if (typeof c?.spec === 'string' && c.spec.includes('/')) {
|
|
821
|
+
const dir = c.spec.substring(0, c.spec.lastIndexOf('/'));
|
|
822
|
+
if (dir) scriptDirs.add(dir);
|
|
823
|
+
}
|
|
795
824
|
}
|
|
796
825
|
}
|
|
797
826
|
if (manifest.overrides && typeof manifest.overrides === 'object' && !Array.isArray(manifest.overrides)) {
|
|
@@ -956,9 +985,12 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
956
985
|
}
|
|
957
986
|
|
|
958
987
|
/**
|
|
959
|
-
* Remove a provider manifest from the
|
|
960
|
-
* (~/.adhdev/
|
|
961
|
-
* outside that root.
|
|
988
|
+
* Remove a provider manifest from the upstream cache root
|
|
989
|
+
* (~/.adhdev/providers/.upstream/{category}/{type}/). Refuses to touch
|
|
990
|
+
* anything outside that root. Used by onboarding to opt out of a
|
|
991
|
+
* provider the user doesn't want; the dashboard no longer exposes a
|
|
992
|
+
* per-provider uninstall button (external sources are removed as a
|
|
993
|
+
* whole via remove_provider_source).
|
|
962
994
|
*/
|
|
963
995
|
private async handleUninstallProviderManifest(args: any): Promise<CommandResult> {
|
|
964
996
|
const type = typeof args?.type === 'string' ? args.type : '';
|
|
@@ -975,12 +1007,12 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
975
1007
|
const path = require('path') as typeof import('path');
|
|
976
1008
|
|
|
977
1009
|
try {
|
|
978
|
-
const installRoot = this.
|
|
1010
|
+
const installRoot = this.getUpstreamInstallRoot();
|
|
979
1011
|
const installRootResolved = path.resolve(installRoot);
|
|
980
1012
|
const targetDir = path.resolve(path.join(installRoot, category, type));
|
|
981
1013
|
|
|
982
1014
|
if (!targetDir.startsWith(installRootResolved + path.sep)) {
|
|
983
|
-
return { success: false, error: 'refusing to delete outside
|
|
1015
|
+
return { success: false, error: 'refusing to delete outside upstream root' };
|
|
984
1016
|
}
|
|
985
1017
|
if (!fs.existsSync(targetDir)) {
|
|
986
1018
|
return { success: false, error: 'not installed' };
|
|
@@ -1000,7 +1032,7 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
1000
1032
|
}
|
|
1001
1033
|
|
|
1002
1034
|
/**
|
|
1003
|
-
* Return everything currently installed in
|
|
1035
|
+
* Return everything currently installed in the upstream cache with its
|
|
1004
1036
|
* version. This is the "what does this daemon have" answer used both by
|
|
1005
1037
|
* the UI and by the update checker.
|
|
1006
1038
|
*/
|
|
@@ -1008,7 +1040,7 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
1008
1040
|
const fs = require('fs') as typeof import('fs');
|
|
1009
1041
|
const path = require('path') as typeof import('path');
|
|
1010
1042
|
|
|
1011
|
-
const installRoot = this.
|
|
1043
|
+
const installRoot = this.getUpstreamInstallRoot();
|
|
1012
1044
|
if (!fs.existsSync(installRoot)) return { success: true, providers: [] };
|
|
1013
1045
|
|
|
1014
1046
|
const CATEGORIES = ['cli', 'ide', 'extension', 'acp'] as const;
|
|
@@ -1101,6 +1133,236 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
1101
1133
|
return { success: true, providers: checks };
|
|
1102
1134
|
}
|
|
1103
1135
|
|
|
1136
|
+
// ─── External provider sources (3rd-party git URLs) ──────────────
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Register a new external provider source. The daemon clones the repo
|
|
1140
|
+
* to ~/.adhdev/external/<name>/, walks it once to detect provided
|
|
1141
|
+
* types, and surfaces any conflicts with already-installed types so
|
|
1142
|
+
* the dashboard can ask the user how to resolve them.
|
|
1143
|
+
*
|
|
1144
|
+
* Args: { url: string, ref?: string, name?: string }
|
|
1145
|
+
* - url: https://, git@, or any git-cloneable URL
|
|
1146
|
+
* - ref: branch/tag/commit (default "main")
|
|
1147
|
+
* - name: short identifier (default derived from URL)
|
|
1148
|
+
*
|
|
1149
|
+
* Returns: { source, providers, conflicts }
|
|
1150
|
+
* - conflicts: list of types this new source provides that another
|
|
1151
|
+
* source already exposes. UI uses this to prompt for active-source
|
|
1152
|
+
* selection before the load takes effect.
|
|
1153
|
+
*/
|
|
1154
|
+
private async handleAddProviderSource(args: any): Promise<CommandResult> {
|
|
1155
|
+
const url = typeof args?.url === 'string' ? args.url.trim() : '';
|
|
1156
|
+
if (!url) return { success: false, error: 'url is required' };
|
|
1157
|
+
const ref = typeof args?.ref === 'string' && args.ref.trim() ? args.ref.trim() : 'main';
|
|
1158
|
+
|
|
1159
|
+
// Defense in depth against argv flag-smuggling: reject anything that
|
|
1160
|
+
// looks like a git option in either positional. The `--` end-of-options
|
|
1161
|
+
// sentinel below catches accidental cases, but rejecting early gives
|
|
1162
|
+
// a clear error message and stops obviously malicious inputs from
|
|
1163
|
+
// even touching git.
|
|
1164
|
+
if (url.startsWith('-')) return { success: false, error: 'url must not start with "-"' };
|
|
1165
|
+
if (ref.startsWith('-')) return { success: false, error: 'ref must not start with "-"' };
|
|
1166
|
+
// Whitelist the protocols we'll forward to git. Anything else
|
|
1167
|
+
// (file://, ext-protocol-handlers, …) is refused outright.
|
|
1168
|
+
if (!/^(https?:\/\/|git@[a-z0-9._-]+:)[a-z0-9._@:/~\-]+$/i.test(url)) {
|
|
1169
|
+
return { success: false, error: 'url must be https://… or git@host:… and contain only URL-safe characters' };
|
|
1170
|
+
}
|
|
1171
|
+
// Refs are git refnames — letters, digits, slashes, dots, underscores,
|
|
1172
|
+
// dashes. Rejects e.g. spaces, semicolons, backticks, shell metas.
|
|
1173
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(ref)) {
|
|
1174
|
+
return { success: false, error: 'ref must contain only [A-Za-z0-9._/-]' };
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const ext = require('../providers/external-sources.js') as typeof import('../providers/external-sources.js');
|
|
1178
|
+
const requestedName = typeof args?.name === 'string' && args.name.trim() ? args.name.trim() : ext.deriveSourceName(url);
|
|
1179
|
+
if (!/^@[a-z0-9_-]+$/i.test(requestedName)) {
|
|
1180
|
+
return { success: false, error: 'name must match @[a-z0-9_-]+' };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const fs = require('node:fs') as typeof import('node:fs');
|
|
1184
|
+
const path = require('node:path') as typeof import('node:path');
|
|
1185
|
+
const { spawnSync } = require('node:child_process') as typeof import('node:child_process');
|
|
1186
|
+
|
|
1187
|
+
const file = ext.loadExternalSources();
|
|
1188
|
+
if (file.sources.some(s => s.name === requestedName)) {
|
|
1189
|
+
return { success: false, error: `source name "${requestedName}" is already registered` };
|
|
1190
|
+
}
|
|
1191
|
+
if (file.sources.some(s => s.url === url && s.ref === ref)) {
|
|
1192
|
+
return { success: false, error: `source url+ref already registered (use a different name to track another ref)` };
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const sourceDir = path.join(ext.externalRoot(), requestedName);
|
|
1196
|
+
if (!fs.existsSync(ext.externalRoot())) fs.mkdirSync(ext.externalRoot(), { recursive: true });
|
|
1197
|
+
if (fs.existsSync(sourceDir)) {
|
|
1198
|
+
return { success: false, error: `directory already exists: ${sourceDir} (rename or remove first)` };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// `--` sentinel after the option list so any future regex-bypassing
|
|
1202
|
+
// url that *did* start with `-` would still be treated as a path
|
|
1203
|
+
// by git rather than an option.
|
|
1204
|
+
const clone = spawnSync('git', ['clone', '--depth=1', '--branch', ref, '--', url, sourceDir], {
|
|
1205
|
+
encoding: 'utf-8',
|
|
1206
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1207
|
+
timeout: 60_000,
|
|
1208
|
+
});
|
|
1209
|
+
if (clone.status !== 0) {
|
|
1210
|
+
try { fs.rmSync(sourceDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
1211
|
+
return { success: false, error: `git clone failed: ${(clone.stderr || clone.stdout || '').trim() || 'unknown error'}` };
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const source: import('../providers/external-sources.js').ExternalSource = {
|
|
1215
|
+
name: requestedName,
|
|
1216
|
+
url,
|
|
1217
|
+
ref,
|
|
1218
|
+
addedAt: new Date().toISOString(),
|
|
1219
|
+
};
|
|
1220
|
+
ext.saveExternalSources({ schema: 1, sources: [...file.sources, source] });
|
|
1221
|
+
|
|
1222
|
+
// Detect type-level conflicts with what's already on disk after this clone.
|
|
1223
|
+
const inventory = ext.inventoryExternalSources();
|
|
1224
|
+
const conflicts: { category: string; type: string; sources: string[] }[] = [];
|
|
1225
|
+
const newEntry = inventory.find(e => e.sourceName === requestedName);
|
|
1226
|
+
if (newEntry) {
|
|
1227
|
+
for (const [category, types] of Object.entries(newEntry.providers)) {
|
|
1228
|
+
for (const type of types) {
|
|
1229
|
+
const sources = ext.sourcesProviding(category, type);
|
|
1230
|
+
if (sources.length > 1) conflicts.push({ category, type, sources });
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Hot-reload so the daemon picks up the new providers immediately.
|
|
1236
|
+
if (this._ctx.providerLoader) {
|
|
1237
|
+
this._ctx.providerLoader.reload();
|
|
1238
|
+
this._ctx.providerLoader.registerToDetector();
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return {
|
|
1242
|
+
success: true,
|
|
1243
|
+
source,
|
|
1244
|
+
providers: newEntry?.providers ?? {},
|
|
1245
|
+
conflicts,
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Remove a registered external source. Deletes the clone directory and
|
|
1251
|
+
* any active-source entry pointing to it.
|
|
1252
|
+
*
|
|
1253
|
+
* Args: { name: string }
|
|
1254
|
+
*/
|
|
1255
|
+
private async handleRemoveProviderSource(args: any): Promise<CommandResult> {
|
|
1256
|
+
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
1257
|
+
if (!name) return { success: false, error: 'name is required' };
|
|
1258
|
+
const ext = require('../providers/external-sources.js') as typeof import('../providers/external-sources.js');
|
|
1259
|
+
|
|
1260
|
+
const fs = require('node:fs') as typeof import('node:fs');
|
|
1261
|
+
const path = require('node:path') as typeof import('node:path');
|
|
1262
|
+
const file = ext.loadExternalSources();
|
|
1263
|
+
const match = file.sources.find(s => s.name === name);
|
|
1264
|
+
if (!match) return { success: false, error: `source "${name}" not registered` };
|
|
1265
|
+
|
|
1266
|
+
const sourceDir = path.join(ext.externalRoot(), name);
|
|
1267
|
+
if (fs.existsSync(sourceDir)) {
|
|
1268
|
+
try { fs.rmSync(sourceDir, { recursive: true, force: true }); }
|
|
1269
|
+
catch (e: any) { return { success: false, error: `failed to delete ${sourceDir}: ${e?.message || e}` }; }
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
ext.saveExternalSources({
|
|
1273
|
+
schema: 1,
|
|
1274
|
+
sources: file.sources.filter(s => s.name !== name),
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
// Drop any active-source entries that pointed at this source.
|
|
1278
|
+
const active = ext.loadProvidersActive();
|
|
1279
|
+
const filteredActive: Record<string, string> = {};
|
|
1280
|
+
for (const [type, src] of Object.entries(active.active)) {
|
|
1281
|
+
if (src !== name) filteredActive[type] = src;
|
|
1282
|
+
}
|
|
1283
|
+
ext.saveProvidersActive({ schema: 1, active: filteredActive });
|
|
1284
|
+
|
|
1285
|
+
if (this._ctx.providerLoader) {
|
|
1286
|
+
this._ctx.providerLoader.reload();
|
|
1287
|
+
this._ctx.providerLoader.registerToDetector();
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return { success: true, removed: { name } };
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* List registered external sources + each source's currently installed
|
|
1295
|
+
* providers + the active selection for any conflicting types. Used by
|
|
1296
|
+
* the dashboard's "Sources" tab.
|
|
1297
|
+
*/
|
|
1298
|
+
private handleListProviderSources(_args: any): CommandResult {
|
|
1299
|
+
const ext = require('../providers/external-sources.js') as typeof import('../providers/external-sources.js');
|
|
1300
|
+
const file = ext.loadExternalSources();
|
|
1301
|
+
const inventory = ext.inventoryExternalSources();
|
|
1302
|
+
const active = ext.loadProvidersActive();
|
|
1303
|
+
|
|
1304
|
+
// Build a per-source view + flag types that have ambiguity.
|
|
1305
|
+
const sources = file.sources.map(s => {
|
|
1306
|
+
const inv = inventory.find(e => e.sourceName === s.name);
|
|
1307
|
+
return {
|
|
1308
|
+
...s,
|
|
1309
|
+
providers: inv?.providers ?? {},
|
|
1310
|
+
};
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
// Compute conflicts globally — any type provided by ≥ 2 sources.
|
|
1314
|
+
const conflictMap = new Map<string, { category: string; sources: string[] }>();
|
|
1315
|
+
for (const inv of inventory) {
|
|
1316
|
+
for (const [category, types] of Object.entries(inv.providers)) {
|
|
1317
|
+
for (const type of types) {
|
|
1318
|
+
const candidates = ext.sourcesProviding(category, type);
|
|
1319
|
+
if (candidates.length > 1 && !conflictMap.has(type)) {
|
|
1320
|
+
conflictMap.set(type, { category, sources: candidates });
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const conflicts = [...conflictMap.entries()].map(([type, info]) => ({
|
|
1326
|
+
type,
|
|
1327
|
+
category: info.category,
|
|
1328
|
+
candidates: info.sources,
|
|
1329
|
+
active: active.active[type] ?? null,
|
|
1330
|
+
}));
|
|
1331
|
+
|
|
1332
|
+
return { success: true, sources, conflicts };
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Pick which source's copy of a conflicting provider type is active.
|
|
1337
|
+
* Other sources' copies stay on disk but the loader ignores them.
|
|
1338
|
+
*
|
|
1339
|
+
* Args: { type: string, sourceName: string }
|
|
1340
|
+
*/
|
|
1341
|
+
private handleSetActiveProviderSource(args: any): CommandResult {
|
|
1342
|
+
const type = typeof args?.type === 'string' ? args.type.trim() : '';
|
|
1343
|
+
const sourceName = typeof args?.sourceName === 'string' ? args.sourceName.trim() : '';
|
|
1344
|
+
if (!type || !sourceName) return { success: false, error: 'type and sourceName are required' };
|
|
1345
|
+
const ext = require('../providers/external-sources.js') as typeof import('../providers/external-sources.js');
|
|
1346
|
+
|
|
1347
|
+
// Validate: the source must actually provide that type.
|
|
1348
|
+
const inventory = ext.inventoryExternalSources();
|
|
1349
|
+
const entry = inventory.find(e => e.sourceName === sourceName);
|
|
1350
|
+
if (!entry) return { success: false, error: `source "${sourceName}" not found` };
|
|
1351
|
+
const provided = Object.values(entry.providers).some(types => types.includes(type));
|
|
1352
|
+
if (!provided) return { success: false, error: `source "${sourceName}" does not provide type "${type}"` };
|
|
1353
|
+
|
|
1354
|
+
const active = ext.loadProvidersActive();
|
|
1355
|
+
active.active[type] = sourceName;
|
|
1356
|
+
ext.saveProvidersActive(active);
|
|
1357
|
+
|
|
1358
|
+
if (this._ctx.providerLoader) {
|
|
1359
|
+
this._ctx.providerLoader.reload();
|
|
1360
|
+
this._ctx.providerLoader.registerToDetector();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
return { success: true, type, sourceName };
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1104
1366
|
// ─── DevServer HTTP proxy helpers ─────────────────
|
|
1105
1367
|
// These bridge WS commands to the DevServer REST API (localhost:19280)
|
|
1106
1368
|
|