@adhdev/daemon-core 0.9.82-rc.455 → 0.9.82-rc.457
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/med-family/mesh-crud.d.ts +19 -0
- package/dist/config/config.d.ts +14 -0
- package/dist/config/registry-resolver.d.ts +54 -0
- package/dist/index.js +369 -67
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +369 -67
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/preview-freshness.d.ts +11 -1
- package/dist/mesh/worktree-bootstrap-config.d.ts +9 -0
- package/dist/providers/approval-utils.d.ts +25 -0
- package/dist/providers/cli-provider-instance.d.ts +30 -1
- package/dist/providers/manual-attendance.d.ts +16 -0
- package/dist/providers/provider-instance.d.ts +8 -1
- package/dist/providers/provider-loader.d.ts +17 -2
- package/dist/providers/spec/fsm-driver.d.ts +5 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +2 -0
- package/src/commands/handler.ts +9 -5
- package/src/commands/low-family/daemon-lifecycle.ts +14 -1
- package/src/commands/med-family/mesh-crud.ts +83 -49
- package/src/config/config.ts +18 -0
- package/src/config/registry-resolver.ts +100 -0
- package/src/mesh/preview-freshness.ts +46 -1
- package/src/mesh/worktree-bootstrap-config.ts +1 -1
- package/src/providers/approval-utils.ts +42 -0
- package/src/providers/cli-provider-instance.ts +246 -13
- package/src/providers/manual-attendance.ts +20 -0
- package/src/providers/provider-instance.ts +6 -1
- package/src/providers/provider-loader.ts +36 -9
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +13 -2
- package/src/providers/spec/fsm-driver.ts +49 -2
- package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +0 -198
|
@@ -15,4 +15,14 @@ export interface PreviewFreshness {
|
|
|
15
15
|
}>;
|
|
16
16
|
nextAction: string;
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Gate: does this repository actually configure the preview-deploy pipeline?
|
|
20
|
+
*
|
|
21
|
+
* The preview-freshness surface embeds this project's private release-pipeline
|
|
22
|
+
* instructions (`npm run deploy:preview`, smoke preview, …). Those are only
|
|
23
|
+
* meaningful in a repo that ships the pipeline. An external repo joined to a
|
|
24
|
+
* mesh must not have that guidance leak into its coordinator prompt, so this
|
|
25
|
+
* gate keeps the surface off unless a concrete pipeline artifact is present.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isPreviewPipelineConfigured(repoRoot: string): boolean;
|
|
28
|
+
export declare function buildPreviewFreshness(repoRoot: string): PreviewFreshness | null;
|
|
@@ -29,6 +29,15 @@ export interface WorktreeBootstrapState extends MeshAsyncJobLifecycle {
|
|
|
29
29
|
staleReason?: string;
|
|
30
30
|
}
|
|
31
31
|
export declare const WORKTREE_BOOTSTRAP_STALE_RUNNING_MS: number;
|
|
32
|
+
/**
|
|
33
|
+
* Enumerate registered submodule paths for a worktree, generically (no hardcoded
|
|
34
|
+
* 'oss'/'adhdev-providers'). Reads `.gitmodules` via `git config --file .gitmodules
|
|
35
|
+
* --get-regexp path`, whose lines are `submodule.<name>.path <relativePath>`. Paths
|
|
36
|
+
* are returned normalized to forward slashes (git porcelain always emits '/'),
|
|
37
|
+
* trailing slash stripped. Returns an empty set when there are no submodules or the
|
|
38
|
+
* lookup fails — callers must then treat any change as dirty (conservative).
|
|
39
|
+
*/
|
|
40
|
+
export declare function getRegisteredSubmodulePaths(workspace: string): Set<string>;
|
|
32
41
|
export declare function isWorktreeBootstrapStaleRunning(node: {
|
|
33
42
|
worktreeBootstrap?: {
|
|
34
43
|
status?: string;
|
|
@@ -6,6 +6,31 @@ import type { ProviderModule } from './contracts.js';
|
|
|
6
6
|
* decline, which distinguishes it from a generic numbered menu or prose list.
|
|
7
7
|
*/
|
|
8
8
|
export declare function hasNegativeApprovalOption(buttons: string[] | null | undefined): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* True when a button reliably identifies a tool-CONSENT modal on its own — a
|
|
11
|
+
* scoped permission-grant affirmative such as:
|
|
12
|
+
* - "Yes, allow all edits in tmp/ during this session"
|
|
13
|
+
* - "Yes, and don't ask again for example.com"
|
|
14
|
+
* - "Yes, allow reading from etc/ from this project"
|
|
15
|
+
* - "Always allow"
|
|
16
|
+
*
|
|
17
|
+
* These options only ever appear in a genuine approval/permission prompt; a
|
|
18
|
+
* /model or /mode picker ("1. Default 2. Opus 3. Sonnet") never offers a
|
|
19
|
+
* "grant this scope" choice. They therefore serve as a SECOND reliable
|
|
20
|
+
* structural anchor alongside {@link hasNegativeApprovalOption}.
|
|
21
|
+
*
|
|
22
|
+
* Why this exists (tall-diff fallback, #137): when a Write/Edit diff is tall,
|
|
23
|
+
* the trailing decline option ("3. No") can scroll off the bottom of the
|
|
24
|
+
* captured PTY frame, leaving only "1. Yes" + "2. Yes, allow … this session".
|
|
25
|
+
* hasNegativeApprovalOption then reads false and the auto-approve gate bails —
|
|
26
|
+
* a delegated worker sits forever on a modal it could safely have approved. The
|
|
27
|
+
* grant-scope affirmative lets the gate recognize the consent modal WITHOUT
|
|
28
|
+
* seeing the off-frame decline. The gate still selects the plain "Yes"
|
|
29
|
+
* (allow-once) via pickApprovalButton, never the broader grant, and the settle
|
|
30
|
+
* gate still requires a stable modal — so a half-rendered frame never fires.
|
|
31
|
+
* Kept deliberately narrow so no picker/confirm modal can trip it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function hasReliableApprovalAffirmative(buttons: string[] | null | undefined): boolean;
|
|
9
34
|
export declare function getApprovalPositiveHints(provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null): string[];
|
|
10
35
|
export declare function pickApprovalButton(buttons: string[] | null | undefined, provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null): {
|
|
11
36
|
index: number;
|
|
@@ -115,6 +115,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
115
115
|
private autoApproveSettleTimer;
|
|
116
116
|
private autoApproveInactiveSince;
|
|
117
117
|
private autoApproveMaskSince;
|
|
118
|
+
private stalledApprovalNudgeEpisode;
|
|
118
119
|
private readonly manualAttendance;
|
|
119
120
|
private controlValues;
|
|
120
121
|
private summaryMetadata;
|
|
@@ -298,6 +299,10 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
298
299
|
*/
|
|
299
300
|
private completingTurnTaskId;
|
|
300
301
|
private meshTraceCtx;
|
|
302
|
+
private completionTraceOn;
|
|
303
|
+
private fsmTraceOn;
|
|
304
|
+
private recordCompletionGateTrace;
|
|
305
|
+
private recordFsmTransitionTrace;
|
|
301
306
|
private flushCompletedDebounceIfFinalized;
|
|
302
307
|
private maybeAutoApproveStatus;
|
|
303
308
|
/**
|
|
@@ -355,7 +360,9 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
355
360
|
get cliName(): string;
|
|
356
361
|
private shouldAutoApprove;
|
|
357
362
|
/** @see ProviderInstance.noteManualInteraction */
|
|
358
|
-
noteManualInteraction(now?: number
|
|
363
|
+
noteManualInteraction(now?: number, opts?: {
|
|
364
|
+
passive?: boolean;
|
|
365
|
+
}): void;
|
|
359
366
|
/**
|
|
360
367
|
* Whether auto-approve should be treated as active *right now* for display
|
|
361
368
|
* and firing decisions: the configured intent AND the user is not currently
|
|
@@ -366,6 +373,28 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
366
373
|
*/
|
|
367
374
|
private autoApproveEffectivelyActive;
|
|
368
375
|
private autoApproveMaskStalled;
|
|
376
|
+
/**
|
|
377
|
+
* NOTIF-APPROVAL-MASKED (Q1b): surface a delegated worker's STALLED auto-approve modal
|
|
378
|
+
* to the mesh COORDINATOR, decoupled from the dashboard visible-status mask.
|
|
379
|
+
*
|
|
380
|
+
* When auto-approve is configured but the episode never settles (modal parse miss / the
|
|
381
|
+
* settle gate never satisfied), getState()/detectStatusTransition() fold the raw
|
|
382
|
+
* `waiting_approval` into `generating` to suppress dashboard flicker — so
|
|
383
|
+
* detectStatusTransition()'s `waiting_approval` arm never runs and NO agent:waiting_approval
|
|
384
|
+
* event is emitted. The coordinator's real-time approval-nudge delivery then has no input and
|
|
385
|
+
* the worker's stuck modal is never surfaced (the live ~25s stall). The dashboard mask is
|
|
386
|
+
* intentional and stays; this emits the coordinator nudge exactly ONCE, gated on the SAME
|
|
387
|
+
* raw-waiting_approval + mask-stalled signal resolveModalParkStatus() distinguishes, the
|
|
388
|
+
* instant the mask-stall threshold trips (the same moment getState un-folds the mask).
|
|
389
|
+
*
|
|
390
|
+
* Only delegated worker sessions qualify: a foreground session has no coordinator to notify,
|
|
391
|
+
* and its own dashboard mask already reveals the modal on stall. A normally-resolving
|
|
392
|
+
* auto-approve never reaches AUTO_APPROVE_MASK_STALL_MS, so it emits nothing here; and if a
|
|
393
|
+
* masked approval clears just as this fires, rc.455's isApprovalNudgeResolved stale-drop
|
|
394
|
+
* discards the nudge coordinator-side without noise. Dedup is per-episode (keyed on the
|
|
395
|
+
* mask-clock value) so a modal that flaps between parsed/unparsed states is announced once.
|
|
396
|
+
*/
|
|
397
|
+
private maybeEmitStalledApprovalNudge;
|
|
369
398
|
private recordAutoApproval;
|
|
370
399
|
recordApprovalSelection(buttonText: string): void;
|
|
371
400
|
private formatMarkerTimestamp;
|
|
@@ -61,3 +61,19 @@ export declare class ManualAttendanceTracker {
|
|
|
61
61
|
* pure read commands (read_chat / list_chats — passive polling, not driving).
|
|
62
62
|
*/
|
|
63
63
|
export declare const MANUAL_ATTENDANCE_COMMANDS: ReadonlySet<string>;
|
|
64
|
+
/**
|
|
65
|
+
* The subset of {@link MANUAL_ATTENDANCE_COMMANDS} that are PASSIVE view-only
|
|
66
|
+
* actions — foregrounding a session's tab / opening its panel. They convey "I am
|
|
67
|
+
* looking at this session", not "I am driving it", and carry no user input.
|
|
68
|
+
*
|
|
69
|
+
* For a foreground (base-node) session these still attend: a user who
|
|
70
|
+
* foregrounds their own session should get the quiet window so an incoming
|
|
71
|
+
* approval stays visible for them to act on. But for a DELEGATED worker session
|
|
72
|
+
* a passive peek must NOT attend — a coordinator merely opening a worker's panel
|
|
73
|
+
* to watch progress would otherwise suppress that worker's delegated
|
|
74
|
+
* auto-approve for the whole window (secondary cause, #137). The per-instance
|
|
75
|
+
* hook decides: it drops a passive stamp only when the session is a delegated
|
|
76
|
+
* worker, so explicit input (controlbar / resolve_action / pty_input) still
|
|
77
|
+
* attends a worker and a foreground session is unaffected.
|
|
78
|
+
*/
|
|
79
|
+
export declare const MANUAL_ATTENDANCE_PASSIVE_VIEW_COMMANDS: ReadonlySet<string>;
|
|
@@ -200,8 +200,15 @@ export interface ProviderInstance {
|
|
|
200
200
|
* input). Provider-common signal that suppresses auto-approve for a short
|
|
201
201
|
* window so the user can drive the session manually; background mesh worker
|
|
202
202
|
* sessions never receive it, so their delegated auto-approve is unaffected.
|
|
203
|
+
*
|
|
204
|
+
* `opts.passive` marks a view-only action (select_session / open_panel). A
|
|
205
|
+
* delegated worker session ignores passive stamps so a coordinator merely
|
|
206
|
+
* watching its panel does not suppress its delegated auto-approve; explicit
|
|
207
|
+
* input still attends. Foreground sessions attend on passive views too.
|
|
203
208
|
*/
|
|
204
|
-
noteManualInteraction?(now?: number
|
|
209
|
+
noteManualInteraction?(now?: number, opts?: {
|
|
210
|
+
passive?: boolean;
|
|
211
|
+
}): void;
|
|
205
212
|
/** cleanup */
|
|
206
213
|
dispose(): void;
|
|
207
214
|
}
|
|
@@ -55,10 +55,15 @@ export declare class ProviderLoader {
|
|
|
55
55
|
private logFn;
|
|
56
56
|
private versionArchive;
|
|
57
57
|
private scriptsCache;
|
|
58
|
+
/**
|
|
59
|
+
* Resolved registry base URL and provider tarball URL. Resolution order:
|
|
60
|
+
* explicit config field (constructor option) → env var → vendor default.
|
|
61
|
+
* See `config/registry-resolver.ts`.
|
|
62
|
+
*/
|
|
63
|
+
private readonly registryBaseUrl;
|
|
64
|
+
private readonly providerTarballUrl;
|
|
58
65
|
/** Inject VersionArchive so resolve() can auto-detect installed versions */
|
|
59
66
|
setVersionArchive(archive: VersionArchive): void;
|
|
60
|
-
private static readonly GITHUB_TARBALL_URL;
|
|
61
|
-
private static readonly REGISTRY_BASE_URL;
|
|
62
67
|
private static readonly META_FILE;
|
|
63
68
|
private static readonly REGISTRY_META_FILE;
|
|
64
69
|
private static readonly REPO_PROVIDER_DIRNAME;
|
|
@@ -85,6 +90,16 @@ export declare class ProviderLoader {
|
|
|
85
90
|
* probing; production code should leave this unset.
|
|
86
91
|
*/
|
|
87
92
|
probeStarts?: string[];
|
|
93
|
+
/**
|
|
94
|
+
* Explicit provider registry base URL override (config.registryUrl).
|
|
95
|
+
* Highest-priority resolver source, ahead of ADHDEV_REGISTRY_URL + default.
|
|
96
|
+
*/
|
|
97
|
+
registryUrl?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Explicit provider tarball URL override (config.providerTarballUrl).
|
|
100
|
+
* Highest-priority resolver source, ahead of ADHDEV_PROVIDER_TARBALL_URL + default.
|
|
101
|
+
*/
|
|
102
|
+
providerTarballUrl?: string;
|
|
88
103
|
});
|
|
89
104
|
private migrateMarketplaceDirToExternal;
|
|
90
105
|
private log;
|
|
@@ -181,6 +181,11 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
181
181
|
* (−1 = whole screen), or a `section:<id>` / `<region>#ignore:<pat>` string
|
|
182
182
|
* when the clause scopes to a section or declares an ignore_lines filter. */
|
|
183
183
|
private regionLastChangedAt;
|
|
184
|
+
/** COMPLETION-EARLYNOTIFY stable-eval trace: last stable/not-stable verdict
|
|
185
|
+
* recorded per stable region, so the trace fires only when the verdict FLIPS
|
|
186
|
+
* (not every quiet frame). Cleared on every transition alongside
|
|
187
|
+
* regionLastChangedAt. Diagnostic-only — never consulted by the FSM. */
|
|
188
|
+
private stableVerdictCache;
|
|
184
189
|
/** Timer that re-runs evaluate() when a time-condition would flip true
|
|
185
190
|
* with no PTY frame to trigger it. */
|
|
186
191
|
private wakeTimer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.457",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
50
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.457",
|
|
50
|
+
"@adhdev/session-host-core": "0.9.82-rc.457",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
53
53
|
"ajv-formats": "^3.0.1",
|
|
@@ -180,6 +180,8 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
180
180
|
logFn: config.providerLogFn,
|
|
181
181
|
sourceMode: providerSourceMode,
|
|
182
182
|
userDir: appConfig.providerDir,
|
|
183
|
+
registryUrl: appConfig.registryUrl,
|
|
184
|
+
providerTarballUrl: appConfig.providerTarballUrl,
|
|
183
185
|
});
|
|
184
186
|
|
|
185
187
|
// Boot-time auto-sync is intentionally disabled. The user picks which
|
package/src/commands/handler.ts
CHANGED
|
@@ -20,13 +20,14 @@ import type { ProviderModule, ProviderScripts } from '../providers/contracts.js'
|
|
|
20
20
|
import type { DaemonAgentStreamManager } from '../agent-stream/index.js';
|
|
21
21
|
import type { CliAdapter } from '../cli-adapter-types.js';
|
|
22
22
|
import { loadConfig } from '../config/config.js';
|
|
23
|
+
import { resolveRegistryBaseUrl } from '../config/registry-resolver.js';
|
|
23
24
|
import { ChatHistoryWriter } from '../config/chat-history.js';
|
|
24
25
|
import type { SessionRegistry, SessionRuntimeTarget } from '../sessions/registry.js';
|
|
25
26
|
import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
|
|
26
27
|
import { LOG } from '../logging/logger.js';
|
|
27
28
|
import { resolveLegacyProviderScript, type LegacyStringScript } from './provider-script-resolver.js';
|
|
28
29
|
import { sha256Hex } from '../system/hash.js';
|
|
29
|
-
import { MANUAL_ATTENDANCE_COMMANDS } from '../providers/manual-attendance.js';
|
|
30
|
+
import { MANUAL_ATTENDANCE_COMMANDS, MANUAL_ATTENDANCE_PASSIVE_VIEW_COMMANDS } from '../providers/manual-attendance.js';
|
|
30
31
|
|
|
31
32
|
// Sub-module imports
|
|
32
33
|
import * as Chat from './chat-commands.js';
|
|
@@ -393,15 +394,18 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
393
394
|
*/
|
|
394
395
|
private noteManualAttendanceIfApplicable(cmd: string, args: any): void {
|
|
395
396
|
if (!MANUAL_ATTENDANCE_COMMANDS.has(cmd)) return;
|
|
397
|
+
// Passive view-only actions (select_session / open_panel) attend a
|
|
398
|
+
// foreground session but NOT a delegated worker — the instance decides.
|
|
399
|
+
const passive = MANUAL_ATTENDANCE_PASSIVE_VIEW_COMMANDS.has(cmd);
|
|
396
400
|
const sessionId = this._currentRoute.session?.sessionId
|
|
397
401
|
|| (typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '');
|
|
398
402
|
if (!sessionId) return;
|
|
399
403
|
const session = this._ctx.sessionRegistry?.get(sessionId);
|
|
400
404
|
const instanceKey = session?.adapterKey || session?.instanceKey || sessionId;
|
|
401
405
|
const instance = this._ctx.instanceManager?.getInstance(instanceKey) as
|
|
402
|
-
{ noteManualInteraction?: (now?: number) => void } | undefined;
|
|
406
|
+
{ noteManualInteraction?: (now?: number, opts?: { passive?: boolean }) => void } | undefined;
|
|
403
407
|
try {
|
|
404
|
-
instance?.noteManualInteraction?.();
|
|
408
|
+
instance?.noteManualInteraction?.(undefined, { passive });
|
|
405
409
|
} catch {
|
|
406
410
|
// attendance is best-effort — never block command dispatch
|
|
407
411
|
}
|
|
@@ -689,7 +693,7 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
689
693
|
const https = require('https') as typeof import('https');
|
|
690
694
|
const fs = require('fs') as typeof import('fs');
|
|
691
695
|
const path = require('path') as typeof import('path');
|
|
692
|
-
const REGISTRY =
|
|
696
|
+
const REGISTRY = resolveRegistryBaseUrl(loadConfig().registryUrl);
|
|
693
697
|
|
|
694
698
|
function fetchText(url: string, timeoutMs: number): Promise<string> {
|
|
695
699
|
return new Promise((resolve, reject) => {
|
|
@@ -1118,7 +1122,7 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
1118
1122
|
if (!installed.success) return installed;
|
|
1119
1123
|
|
|
1120
1124
|
const https = require('https') as typeof import('https');
|
|
1121
|
-
const REGISTRY =
|
|
1125
|
+
const REGISTRY = resolveRegistryBaseUrl(loadConfig().registryUrl);
|
|
1122
1126
|
|
|
1123
1127
|
function fetchJson(url: string): Promise<any> {
|
|
1124
1128
|
return new Promise((resolve, reject) => {
|
|
@@ -19,6 +19,10 @@ const CHANNEL_SERVER_URL: Record<ReleaseChannel, string> = {
|
|
|
19
19
|
stable: 'https://api.adhf.dev',
|
|
20
20
|
preview: 'https://api-preview.adhf.dev',
|
|
21
21
|
};
|
|
22
|
+
// Vendor-managed serverUrls. A serverUrl equal to one of these (or unset) is
|
|
23
|
+
// steered to the channel default on upgrade; any other value is a user-set
|
|
24
|
+
// custom/self-host URL that must be preserved across upgrades.
|
|
25
|
+
const VENDOR_SERVER_URLS = new Set<string>(Object.values(CHANNEL_SERVER_URL));
|
|
22
26
|
|
|
23
27
|
function normalizeReleaseChannel(value: unknown): ReleaseChannel | null {
|
|
24
28
|
if (typeof value !== 'string') return null;
|
|
@@ -51,7 +55,16 @@ export const daemonLifecycleHandlers: Record<string, LowFamilyHandler> = {
|
|
|
51
55
|
// Check channel-pinned dist-tag and resolve it to a concrete install version.
|
|
52
56
|
const latest = String(execNpmCommandSync(['view', `${pkgName}@${npmTag}`, 'version'], { encoding: 'utf-8', timeout: 10000 }, npmSurface)).trim();
|
|
53
57
|
LOG.info('Upgrade', `Latest ${pkgName}@${npmTag}: v${latest}`);
|
|
54
|
-
|
|
58
|
+
// Only steer serverUrl to the channel's vendor default when the current
|
|
59
|
+
// value is unset or already a vendor default. A self-hoster's custom
|
|
60
|
+
// serverUrl must survive the upgrade instead of being clobbered.
|
|
61
|
+
const currentServerUrl = typeof loadConfig().serverUrl === 'string' ? loadConfig().serverUrl.trim() : '';
|
|
62
|
+
const useVendorServerUrl = currentServerUrl === '' || VENDOR_SERVER_URLS.has(currentServerUrl);
|
|
63
|
+
updateConfig(
|
|
64
|
+
useVendorServerUrl
|
|
65
|
+
? { updateChannel: channel, serverUrl: CHANNEL_SERVER_URL[channel] } as any
|
|
66
|
+
: { updateChannel: channel } as any,
|
|
67
|
+
);
|
|
55
68
|
let currentInstalled: string | null = null;
|
|
56
69
|
try {
|
|
57
70
|
const currentJson = String(execNpmCommandSync(['ls', '-g', pkgName, '--depth=0', '--json'], {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { daemonIdsEquivalent, meshNodeIdMatches, normalizeMeshNodeId } from '@adhdev/mesh-shared';
|
|
13
13
|
import { resolveMeshHostStatus, normalizeMeshDaemonRole } from '../../mesh/mesh-host-ownership.js';
|
|
14
14
|
import {
|
|
15
|
+
getRegisteredSubmodulePaths,
|
|
15
16
|
loadMeshWorktreeBootstrapConfig,
|
|
16
17
|
runMeshWorktreeBootstrap,
|
|
17
18
|
type WorktreeBootstrapState,
|
|
@@ -81,6 +82,83 @@ export async function decideOssCloneSync(
|
|
|
81
82
|
return 'skip_diverged';
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Sync every registered submodule of a freshly-cloned worktree to its clone source
|
|
87
|
+
* node's working submodule HEAD, applying decideOssCloneSync's origin-tip-priority
|
|
88
|
+
* rewind guard per submodule.
|
|
89
|
+
*
|
|
90
|
+
* Generic over the submodule set: the paths come from `.gitmodules` via
|
|
91
|
+
* getRegisteredSubmodulePaths, so this operates identically over EVERY registered
|
|
92
|
+
* submodule (oss, adhdev-providers, …) instead of a hardcoded 'oss' literal. In a
|
|
93
|
+
* repo whose only synced submodule is `oss` the emitted git commands are byte-identical
|
|
94
|
+
* to the original oss-only path.
|
|
95
|
+
*
|
|
96
|
+
* Best-effort by design: a failure on one submodule is logged and skipped; it never
|
|
97
|
+
* blocks the other submodules or the clone. A submodule is only ever advanced to a
|
|
98
|
+
* STRICTLY-NEWER source SHA — the fresh (origin/main-derived) worktree tip is never
|
|
99
|
+
* rewound onto a behind/diverged source.
|
|
100
|
+
*/
|
|
101
|
+
export async function syncClonedWorktreeSubmodules(
|
|
102
|
+
worktreePath: string,
|
|
103
|
+
sourceWorkspace: string,
|
|
104
|
+
rg: (ctx: GitRepoIdentity, argv: string[], opts?: { timeoutMs?: number }) => Promise<unknown>,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
const submodulePaths = getRegisteredSubmodulePaths(worktreePath);
|
|
107
|
+
if (submodulePaths.size === 0) return;
|
|
108
|
+
|
|
109
|
+
const sourceCtx: GitRepoIdentity = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
|
|
110
|
+
const worktreeCtx: GitRepoIdentity = { workspace: worktreePath, repoRoot: worktreePath, isGitRepo: true };
|
|
111
|
+
const readStdout = (out: unknown): string =>
|
|
112
|
+
(typeof out === 'string' ? out : (out as any)?.stdout ?? '').trim();
|
|
113
|
+
|
|
114
|
+
for (const submodulePath of submodulePaths) {
|
|
115
|
+
try {
|
|
116
|
+
// Read the source node's working submodule SHA.
|
|
117
|
+
const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', submodulePath], { timeoutMs: 10000 });
|
|
118
|
+
const sourceSha = readStdout(sourceStatusOut).match(/^[+\- ]?([0-9a-f]{40})/)?.[1];
|
|
119
|
+
if (!sourceSha) continue;
|
|
120
|
+
|
|
121
|
+
// Read the worktree's freshly-checked-out submodule HEAD.
|
|
122
|
+
const subCtx: GitRepoIdentity = {
|
|
123
|
+
workspace: `${worktreePath}/${submodulePath}`,
|
|
124
|
+
repoRoot: `${worktreePath}/${submodulePath}`,
|
|
125
|
+
isGitRepo: true,
|
|
126
|
+
};
|
|
127
|
+
const worktreeSubSha = readStdout(await rg(subCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 }));
|
|
128
|
+
if (!worktreeSubSha || worktreeSubSha === sourceSha) continue;
|
|
129
|
+
|
|
130
|
+
// Bring the source node's submodule HEAD into the worktree submodule object
|
|
131
|
+
// DB so both SHAs are resolvable for the ancestry (rewind) guard below.
|
|
132
|
+
await rg(subCtx, ['fetch', `${sourceWorkspace}/${submodulePath}`, 'HEAD'], { timeoutMs: 60000 });
|
|
133
|
+
|
|
134
|
+
// Rewind guard: the worktree submodule HEAD was just checked out from the
|
|
135
|
+
// FRESH (origin/main-derived) root base. Only advance to the source SHA when
|
|
136
|
+
// it is strictly newer — never rewind to a stale source.
|
|
137
|
+
let action: OssCloneSyncAction;
|
|
138
|
+
try {
|
|
139
|
+
action = await decideOssCloneSync(subCtx, worktreeSubSha, sourceSha, rg);
|
|
140
|
+
} catch (decideErr: any) {
|
|
141
|
+
action = 'skip_diverged';
|
|
142
|
+
console.warn(`[mesh] ${submodulePath} submodule sync guard could not resolve ancestry (kept fresh worktree HEAD): ${decideErr?.message ?? decideErr}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (action === 'advance') {
|
|
146
|
+
await rg(subCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
|
|
147
|
+
await rg(worktreeCtx, ['add', submodulePath], { timeoutMs: 10000 });
|
|
148
|
+
await rg(worktreeCtx, ['commit', '-m', `chore: sync ${submodulePath} to source node HEAD on clone`], { timeoutMs: 10000 });
|
|
149
|
+
console.log(`[mesh] Advanced ${submodulePath} submodule to newer source HEAD ${sourceSha.slice(0, 8)} in worktree`);
|
|
150
|
+
} else if (action === 'skip_rewind') {
|
|
151
|
+
console.warn(`[mesh] Skipped ${submodulePath} submodule rewind on clone: source node ${submodulePath} ${sourceSha.slice(0, 8)} is an ancestor of the fresh worktree ${submodulePath} ${worktreeSubSha.slice(0, 8)} — kept fresher worktree HEAD`);
|
|
152
|
+
} else if (action === 'skip_diverged') {
|
|
153
|
+
console.warn(`[mesh] Skipped ${submodulePath} submodule sync on clone: source node ${submodulePath} ${sourceSha.slice(0, 8)} diverged from the fresh worktree ${submodulePath} ${worktreeSubSha.slice(0, 8)} — kept worktree HEAD (coordinator reconciles)`);
|
|
154
|
+
}
|
|
155
|
+
} catch (subErr: any) {
|
|
156
|
+
// Per-submodule best-effort: never let one submodule's failure block the rest.
|
|
157
|
+
console.warn(`[mesh] ${submodulePath} submodule sync to source HEAD failed (best-effort):`, subErr?.message ?? subErr);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
84
162
|
export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
85
163
|
list_meshes: async (ctx: MedFamilyContext, _args: any) => {
|
|
86
164
|
try {
|
|
@@ -1019,57 +1097,13 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
1019
1097
|
);
|
|
1020
1098
|
submodulesInitialized = true;
|
|
1021
1099
|
|
|
1022
|
-
// Sync
|
|
1100
|
+
// Sync every registered submodule to the clone source node's
|
|
1101
|
+
// working HEAD (best-effort, generic over .gitmodules — no
|
|
1102
|
+
// hardcoded 'oss'; the rewind guard is applied per submodule).
|
|
1023
1103
|
const sourceWorkspace = sourceNode.repoRoot || sourceNode.workspace;
|
|
1024
1104
|
if (sourceWorkspace) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const sourceCtx = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
|
|
1028
|
-
const worktreeCtx = { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true };
|
|
1029
|
-
|
|
1030
|
-
// Read source node's oss submodule SHA
|
|
1031
|
-
const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', 'oss'], { timeoutMs: 10000 });
|
|
1032
|
-
const sourceStatusLine = (typeof sourceStatusOut === 'string' ? sourceStatusOut : (sourceStatusOut as any)?.stdout ?? '').trim();
|
|
1033
|
-
const sourceShaMatch = sourceStatusLine.match(/^[+\- ]?([0-9a-f]{40})/);
|
|
1034
|
-
const sourceSha = sourceShaMatch?.[1];
|
|
1035
|
-
|
|
1036
|
-
if (sourceSha) {
|
|
1037
|
-
// Read worktree's current oss HEAD
|
|
1038
|
-
const ossCtx = { workspace: `${result.worktreePath}/oss`, repoRoot: `${result.worktreePath}/oss`, isGitRepo: true };
|
|
1039
|
-
const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
|
|
1040
|
-
const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
|
|
1041
|
-
|
|
1042
|
-
if (worktreeOssSha && worktreeOssSha !== sourceSha) {
|
|
1043
|
-
// Bring the source node's oss HEAD into the worktree object DB so
|
|
1044
|
-
// both SHAs are resolvable for the ancestry (rewind) guard below.
|
|
1045
|
-
await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
|
|
1046
|
-
|
|
1047
|
-
// Rewind guard: the worktree oss HEAD was just checked out from the
|
|
1048
|
-
// FRESH (origin/main-derived) root base. Only advance to the source
|
|
1049
|
-
// SHA when it is strictly newer — never rewind to a stale source.
|
|
1050
|
-
let ossAction: OssCloneSyncAction;
|
|
1051
|
-
try {
|
|
1052
|
-
ossAction = await decideOssCloneSync(ossCtx, worktreeOssSha, sourceSha, rg);
|
|
1053
|
-
} catch (decideErr: any) {
|
|
1054
|
-
ossAction = 'skip_diverged';
|
|
1055
|
-
console.warn(`[mesh] oss submodule sync guard could not resolve ancestry (kept fresh worktree HEAD): ${decideErr?.message ?? decideErr}`);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (ossAction === 'advance') {
|
|
1059
|
-
await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
|
|
1060
|
-
await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
|
|
1061
|
-
await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
|
|
1062
|
-
console.log(`[mesh] Advanced oss submodule to newer source HEAD ${sourceSha.slice(0, 8)} in worktree`);
|
|
1063
|
-
} else if (ossAction === 'skip_rewind') {
|
|
1064
|
-
console.warn(`[mesh] Skipped oss submodule rewind on clone: source node oss ${sourceSha.slice(0, 8)} is an ancestor of the fresh worktree oss ${worktreeOssSha.slice(0, 8)} — kept fresher worktree HEAD`);
|
|
1065
|
-
} else if (ossAction === 'skip_diverged') {
|
|
1066
|
-
console.warn(`[mesh] Skipped oss submodule sync on clone: source node oss ${sourceSha.slice(0, 8)} diverged from the fresh worktree oss ${worktreeOssSha.slice(0, 8)} — kept worktree HEAD (coordinator reconciles)`);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
} catch (ossErr: any) {
|
|
1071
|
-
console.warn('[mesh] oss submodule sync to source HEAD failed (best-effort):', ossErr.message);
|
|
1072
|
-
}
|
|
1105
|
+
const { runGit: rg } = await import('../../git/git-executor.js');
|
|
1106
|
+
await syncClonedWorktreeSubmodules(result.worktreePath, sourceWorkspace, rg);
|
|
1073
1107
|
}
|
|
1074
1108
|
} catch (subErr: any) {
|
|
1075
1109
|
// Submodule init is best-effort; don't fail the clone
|
package/src/config/config.ts
CHANGED
|
@@ -123,6 +123,22 @@ export interface ADHDevConfig {
|
|
|
123
123
|
// Optional explicit provider override root (for example a local adhdev-providers checkout)
|
|
124
124
|
providerDir?: string;
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Optional provider registry base URL override (for example a self-hosted
|
|
128
|
+
* registry). Highest-priority source in the registry resolver, ahead of the
|
|
129
|
+
* `ADHDEV_REGISTRY_URL` env var and the vendor default. See
|
|
130
|
+
* `config/registry-resolver.ts`.
|
|
131
|
+
*/
|
|
132
|
+
registryUrl?: string;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Optional provider tarball (archive) URL override so self-hosters can point
|
|
136
|
+
* the daemon at their own provider mirror instead of the vendor GitHub repo.
|
|
137
|
+
* Highest-priority source, ahead of `ADHDEV_PROVIDER_TARBALL_URL` and the
|
|
138
|
+
* vendor default. See `config/registry-resolver.ts`.
|
|
139
|
+
*/
|
|
140
|
+
providerTarballUrl?: string;
|
|
141
|
+
|
|
126
142
|
/** Preferred daemon update channel. Defaults to stable/latest. */
|
|
127
143
|
updateChannel?: ReleaseChannel;
|
|
128
144
|
|
|
@@ -233,6 +249,8 @@ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: str
|
|
|
233
249
|
ideSettings: isPlainObject(parsed.ideSettings) ? parsed.ideSettings : {},
|
|
234
250
|
providerSourceMode: resolveProviderSourceMode(parsed.providerSourceMode, parsed.disableUpstream),
|
|
235
251
|
providerDir: asOptionalString(parsed.providerDir),
|
|
252
|
+
registryUrl: asOptionalString(parsed.registryUrl),
|
|
253
|
+
providerTarballUrl: asOptionalString(parsed.providerTarballUrl),
|
|
236
254
|
updateChannel: parsed.updateChannel === 'preview' ? 'preview' : 'stable',
|
|
237
255
|
terminalSizingMode: parsed.terminalSizingMode === 'fit' ? 'fit' : 'measured',
|
|
238
256
|
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry / provider-distribution URL resolver.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the ADHDev provider *registry* base URL and the
|
|
5
|
+
* provider *tarball* (GitHub) URL. Self-hosters can repoint both away from the
|
|
6
|
+
* vendor defaults so the daemon never phones home to `api.adhf.dev` /
|
|
7
|
+
* `github.com/vilmire/adhdev-providers`.
|
|
8
|
+
*
|
|
9
|
+
* Resolution priority (highest first):
|
|
10
|
+
* 1. Explicit config field — `config.registryUrl` / `config.providerTarballUrl`
|
|
11
|
+
* 2. Environment variable — `ADHDEV_REGISTRY_URL` / `ADHDEV_PROVIDER_TARBALL_URL`
|
|
12
|
+
* 3. Vendor default — existing URLs (unchanged for default users)
|
|
13
|
+
*
|
|
14
|
+
* Default users get byte-identical behavior: the vendor defaults equal the
|
|
15
|
+
* literals these functions replaced.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Vendor default registry base URL (no trailing slash). */
|
|
19
|
+
export const DEFAULT_REGISTRY_BASE_URL = 'https://api.adhf.dev/api/v1/registry';
|
|
20
|
+
|
|
21
|
+
/** Vendor default provider tarball URL (GitHub main branch archive). */
|
|
22
|
+
export const DEFAULT_PROVIDER_TARBALL_URL =
|
|
23
|
+
'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
|
|
24
|
+
|
|
25
|
+
/** Env var that overrides the registry base URL. */
|
|
26
|
+
export const REGISTRY_URL_ENV_VAR = 'ADHDEV_REGISTRY_URL';
|
|
27
|
+
|
|
28
|
+
/** Env var that overrides the provider tarball URL. */
|
|
29
|
+
export const PROVIDER_TARBALL_URL_ENV_VAR = 'ADHDEV_PROVIDER_TARBALL_URL';
|
|
30
|
+
|
|
31
|
+
function cleanString(value: unknown): string | undefined {
|
|
32
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stripTrailingSlashes(url: string): string {
|
|
36
|
+
return url.replace(/\/+$/, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the provider registry base URL.
|
|
41
|
+
*
|
|
42
|
+
* @param configuredUrl explicit config value (config.registryUrl); highest priority.
|
|
43
|
+
* @param env process env source (defaults to process.env; injectable for tests).
|
|
44
|
+
* @returns the resolved base URL with any trailing slash stripped, so callers
|
|
45
|
+
* can safely append `/providers`, `/providers/<type>`, etc.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveRegistryBaseUrl(
|
|
48
|
+
configuredUrl?: string | null,
|
|
49
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
50
|
+
): string {
|
|
51
|
+
const resolved =
|
|
52
|
+
cleanString(configuredUrl) ??
|
|
53
|
+
cleanString(env[REGISTRY_URL_ENV_VAR]) ??
|
|
54
|
+
DEFAULT_REGISTRY_BASE_URL;
|
|
55
|
+
return stripTrailingSlashes(resolved);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the provider tarball (archive) URL.
|
|
60
|
+
*
|
|
61
|
+
* @param configuredUrl explicit config value (config.providerTarballUrl); highest priority.
|
|
62
|
+
* @param env process env source (defaults to process.env; injectable for tests).
|
|
63
|
+
*/
|
|
64
|
+
export function resolveProviderTarballUrl(
|
|
65
|
+
configuredUrl?: string | null,
|
|
66
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
67
|
+
): string {
|
|
68
|
+
return (
|
|
69
|
+
cleanString(configuredUrl) ??
|
|
70
|
+
cleanString(env[PROVIDER_TARBALL_URL_ENV_VAR]) ??
|
|
71
|
+
DEFAULT_PROVIDER_TARBALL_URL
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Parsed tarball request target for callers that issue raw hostname/path requests. */
|
|
76
|
+
export interface TarballRequestTarget {
|
|
77
|
+
/** Full resolved tarball URL. */
|
|
78
|
+
url: string;
|
|
79
|
+
/** Hostname component (e.g. `github.com`). */
|
|
80
|
+
hostname: string;
|
|
81
|
+
/** Path component including any query string (e.g. `/vilmire/...tar.gz`). */
|
|
82
|
+
path: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve the provider tarball URL and split it into `{ url, hostname, path }`
|
|
87
|
+
* for callers that build raw `https.request` options (e.g. HEAD ETag probes).
|
|
88
|
+
*/
|
|
89
|
+
export function resolveProviderTarballTarget(
|
|
90
|
+
configuredUrl?: string | null,
|
|
91
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
92
|
+
): TarballRequestTarget {
|
|
93
|
+
const url = resolveProviderTarballUrl(configuredUrl, env);
|
|
94
|
+
const parsed = new URL(url);
|
|
95
|
+
return {
|
|
96
|
+
url,
|
|
97
|
+
hostname: parsed.hostname,
|
|
98
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
99
|
+
};
|
|
100
|
+
}
|