@adhdev/daemon-core 0.9.81 → 0.9.82-rc.10
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/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/router.d.ts +2 -0
- package/dist/git/git-commands.d.ts +1 -0
- package/dist/git/git-status.d.ts +5 -0
- package/dist/git/git-types.d.ts +10 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +415 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +414 -78
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +3 -0
- package/dist/repo-mesh-types.d.ts +121 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +3 -0
- package/src/commands/router.ts +373 -67
- package/src/git/git-commands.ts +3 -3
- package/src/git/git-status.ts +97 -6
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +11 -0
- package/src/index.ts +9 -1
- package/src/mesh/mesh-events.ts +14 -3
- package/src/repo-mesh-types.ts +131 -0
package/src/commands/router.ts
CHANGED
|
@@ -26,6 +26,8 @@ import { getSavedProviderSessions } from '../config/saved-sessions.js';
|
|
|
26
26
|
import { listProviderHistorySessions } from '../config/chat-history.js';
|
|
27
27
|
import { detectIDEs } from '../detection/ide-detector.js';
|
|
28
28
|
import { detectCLI } from '../detection/cli-detector.js';
|
|
29
|
+
import { getGitRepoStatus } from '../git/git-status.js';
|
|
30
|
+
import type { GitSubmoduleStatus } from '../git/git-types.js';
|
|
29
31
|
import { SessionRegistry } from '../sessions/registry.js';
|
|
30
32
|
import { LOG } from '../logging/logger.js';
|
|
31
33
|
import { logCommand } from '../logging/command-log.js';
|
|
@@ -85,6 +87,276 @@ function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
|
85
87
|
});
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
function readObjectRecord(value: unknown): Record<string, any> {
|
|
91
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
92
|
+
? value as Record<string, any>
|
|
93
|
+
: {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readStringValue(...values: unknown[]): string | undefined {
|
|
97
|
+
for (const value of values) {
|
|
98
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readNumberValue(...values: unknown[]): number | undefined {
|
|
104
|
+
for (const value of values) {
|
|
105
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
111
|
+
for (const value of values) {
|
|
112
|
+
if (typeof value === 'boolean') return value;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
|
|
118
|
+
if (!Array.isArray(value)) return undefined;
|
|
119
|
+
const submodules = value
|
|
120
|
+
.map(entry => {
|
|
121
|
+
const submodule = readObjectRecord(entry);
|
|
122
|
+
const path = readStringValue(submodule.path);
|
|
123
|
+
const commit = readStringValue(submodule.commit);
|
|
124
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root);
|
|
125
|
+
if (!path || !commit || !repoPath) return null;
|
|
126
|
+
return {
|
|
127
|
+
path,
|
|
128
|
+
commit,
|
|
129
|
+
repoPath,
|
|
130
|
+
dirty: readBooleanValue(submodule.dirty) ?? false,
|
|
131
|
+
outOfSync: readBooleanValue(submodule.outOfSync, submodule.out_of_sync) ?? false,
|
|
132
|
+
lastCheckedAt: readNumberValue(submodule.lastCheckedAt, submodule.last_checked_at) ?? Date.now(),
|
|
133
|
+
...(readStringValue(submodule.error) ? { error: readStringValue(submodule.error) } : {}),
|
|
134
|
+
};
|
|
135
|
+
})
|
|
136
|
+
.filter((entry): entry is GitSubmoduleStatus => entry !== null);
|
|
137
|
+
return submodules.length > 0 ? submodules : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
141
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
142
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
143
|
+
if (Object.keys(cachedGit).length) {
|
|
144
|
+
const conflictFiles = Array.isArray(cachedGit.conflictFiles)
|
|
145
|
+
? cachedGit.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
|
|
146
|
+
: [];
|
|
147
|
+
const conflictCount = readNumberValue(cachedGit.conflicts) ?? conflictFiles.length;
|
|
148
|
+
const hasConflicts = readBooleanValue(cachedGit.hasConflicts) ?? conflictCount > 0;
|
|
149
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
150
|
+
if (isGitRepo !== undefined) {
|
|
151
|
+
const submodules = readGitSubmodules(cachedGit.submodules);
|
|
152
|
+
return {
|
|
153
|
+
workspace: readStringValue(cachedGit.workspace, node?.workspace) || '',
|
|
154
|
+
repoRoot: readStringValue(cachedGit.repoRoot, node?.repoRoot, node?.workspace) || null,
|
|
155
|
+
isGitRepo,
|
|
156
|
+
branch: readStringValue(cachedGit.branch) ?? null,
|
|
157
|
+
headCommit: readStringValue(cachedGit.headCommit) ?? null,
|
|
158
|
+
headMessage: readStringValue(cachedGit.headMessage) ?? null,
|
|
159
|
+
upstream: readStringValue(cachedGit.upstream) ?? null,
|
|
160
|
+
ahead: readNumberValue(cachedGit.ahead) ?? 0,
|
|
161
|
+
behind: readNumberValue(cachedGit.behind) ?? 0,
|
|
162
|
+
staged: readNumberValue(cachedGit.staged) ?? 0,
|
|
163
|
+
modified: readNumberValue(cachedGit.modified) ?? 0,
|
|
164
|
+
untracked: readNumberValue(cachedGit.untracked) ?? 0,
|
|
165
|
+
deleted: readNumberValue(cachedGit.deleted) ?? 0,
|
|
166
|
+
renamed: readNumberValue(cachedGit.renamed) ?? 0,
|
|
167
|
+
hasConflicts,
|
|
168
|
+
conflictFiles,
|
|
169
|
+
stashCount: readNumberValue(cachedGit.stashCount) ?? 0,
|
|
170
|
+
lastCheckedAt: readNumberValue(cachedGit.lastCheckedAt) ?? Date.now(),
|
|
171
|
+
...(submodules ? { submodules } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
177
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
178
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
179
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
180
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
181
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
182
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
183
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
184
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
185
|
+
const status = Object.keys(directStatus).length
|
|
186
|
+
? directStatus
|
|
187
|
+
: Object.keys(nestedStatus).length
|
|
188
|
+
? nestedStatus
|
|
189
|
+
: Object.keys(probeDirectStatus).length
|
|
190
|
+
? probeDirectStatus
|
|
191
|
+
: Object.keys(probeNestedStatus).length
|
|
192
|
+
? probeNestedStatus
|
|
193
|
+
: {};
|
|
194
|
+
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
195
|
+
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
196
|
+
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
197
|
+
? status.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
|
|
198
|
+
: [];
|
|
199
|
+
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
200
|
+
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
201
|
+
const submodules = readGitSubmodules(status.submodules);
|
|
202
|
+
return {
|
|
203
|
+
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
204
|
+
repoRoot: readStringValue(status.repoRoot, node?.repoRoot, node?.workspace) || null,
|
|
205
|
+
isGitRepo,
|
|
206
|
+
branch: readStringValue(status.branch) ?? null,
|
|
207
|
+
headCommit: readStringValue(status.headCommit) ?? null,
|
|
208
|
+
headMessage: readStringValue(status.headMessage) ?? null,
|
|
209
|
+
upstream: readStringValue(status.upstream) ?? null,
|
|
210
|
+
ahead: readNumberValue(status.ahead) ?? 0,
|
|
211
|
+
behind: readNumberValue(status.behind) ?? 0,
|
|
212
|
+
staged: readNumberValue(status.staged) ?? 0,
|
|
213
|
+
modified: readNumberValue(status.modified) ?? 0,
|
|
214
|
+
untracked: readNumberValue(status.untracked) ?? 0,
|
|
215
|
+
deleted: readNumberValue(status.deleted) ?? 0,
|
|
216
|
+
renamed: readNumberValue(status.renamed) ?? 0,
|
|
217
|
+
hasConflicts,
|
|
218
|
+
conflictFiles,
|
|
219
|
+
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
220
|
+
lastCheckedAt: Date.now(),
|
|
221
|
+
...(submodules ? { submodules } : {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
226
|
+
if (!git) return false;
|
|
227
|
+
return Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
231
|
+
const submodules = Array.isArray(git?.submodules) ? git.submodules : [];
|
|
232
|
+
let dirty = false;
|
|
233
|
+
let outOfSync = false;
|
|
234
|
+
for (const entry of submodules) {
|
|
235
|
+
const submodule = readObjectRecord(entry);
|
|
236
|
+
if (readBooleanValue(submodule.dirty) === true) dirty = true;
|
|
237
|
+
if (readBooleanValue(submodule.outOfSync) === true || !!readStringValue(submodule.error)) outOfSync = true;
|
|
238
|
+
}
|
|
239
|
+
return { dirty, outOfSync };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
243
|
+
if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
|
|
244
|
+
const branch = readStringValue(git.branch);
|
|
245
|
+
if (!branch) return 'degraded';
|
|
246
|
+
const submoduleDrift = getGitSubmoduleDriftState(git);
|
|
247
|
+
if (submoduleDrift.outOfSync) return 'degraded';
|
|
248
|
+
if (submoduleDrift.dirty || hasGitWorktreeChanges(git)) return 'dirty';
|
|
249
|
+
return 'online';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
253
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
254
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
255
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
256
|
+
? activeSession
|
|
257
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
258
|
+
const sessionId = readStringValue(fallbackSession.id, fallbackSession.sessionId, fallbackSession.session_id, node?.activeSessionId, node?.active_session_id, node?.sessionId, node?.session_id);
|
|
259
|
+
return sessionId ? [sessionId] : [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
263
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
264
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
265
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
266
|
+
? activeSession
|
|
267
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
268
|
+
const sessionId = readStringValue(
|
|
269
|
+
fallbackSession.id,
|
|
270
|
+
fallbackSession.sessionId,
|
|
271
|
+
fallbackSession.session_id,
|
|
272
|
+
node?.activeSessionId,
|
|
273
|
+
node?.active_session_id,
|
|
274
|
+
node?.sessionId,
|
|
275
|
+
node?.session_id,
|
|
276
|
+
);
|
|
277
|
+
if (!sessionId) return [];
|
|
278
|
+
return [{
|
|
279
|
+
sessionId,
|
|
280
|
+
providerType: readStringValue(
|
|
281
|
+
fallbackSession.providerType,
|
|
282
|
+
fallbackSession.provider_type,
|
|
283
|
+
fallbackSession.cliType,
|
|
284
|
+
fallbackSession.cli_type,
|
|
285
|
+
fallbackSession.provider,
|
|
286
|
+
node?.providerType,
|
|
287
|
+
node?.provider_type,
|
|
288
|
+
),
|
|
289
|
+
state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
|
|
290
|
+
lifecycle: readStringValue(fallbackSession.lifecycle),
|
|
291
|
+
title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
|
|
292
|
+
workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
|
|
293
|
+
lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
|
|
294
|
+
recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
|
|
295
|
+
isCached: true,
|
|
296
|
+
}];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function readLiveMeshSessionState(record: any): string | undefined {
|
|
300
|
+
return readStringValue(
|
|
301
|
+
record?.meta?.sessionStatus,
|
|
302
|
+
record?.meta?.status,
|
|
303
|
+
record?.meta?.providerStatus,
|
|
304
|
+
record?.status,
|
|
305
|
+
record?.state,
|
|
306
|
+
record?.lifecycle,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function toIsoTimestamp(value: unknown): string | null {
|
|
311
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
312
|
+
const stringValue = readStringValue(value);
|
|
313
|
+
return stringValue || null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
317
|
+
return {
|
|
318
|
+
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
319
|
+
providerType: readStringValue(record?.providerType),
|
|
320
|
+
state: readLiveMeshSessionState(record),
|
|
321
|
+
lifecycle: readStringValue(record?.lifecycle),
|
|
322
|
+
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
323
|
+
recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
|
|
324
|
+
workspace: readStringValue(record?.workspace) ?? null,
|
|
325
|
+
title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
|
|
326
|
+
lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
|
|
327
|
+
isCached: false,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function applyCachedInlineMeshNodeStatus(status: Record<string, unknown>, node: any): boolean {
|
|
332
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
333
|
+
const git = buildCachedInlineMeshGitStatus(node);
|
|
334
|
+
const error = readStringValue(cachedStatus.error, node?.error);
|
|
335
|
+
const health = readStringValue(cachedStatus.health, node?.health);
|
|
336
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
337
|
+
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
338
|
+
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
339
|
+
const activeSessions = readCachedInlineMeshActiveSessions(node);
|
|
340
|
+
const activeSessionDetails = readCachedInlineMeshActiveSessionDetails(node);
|
|
341
|
+
if (!git && !error && !health && !machineStatus && !lastSeenAt && !updatedAt && activeSessions.length === 0) return false;
|
|
342
|
+
if (git) status.git = git;
|
|
343
|
+
if (error) status.error = error;
|
|
344
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
345
|
+
if (lastSeenAt) status.lastSeenAt = lastSeenAt;
|
|
346
|
+
if (updatedAt) status.updatedAt = updatedAt;
|
|
347
|
+
if (activeSessions.length > 0) status.activeSessions = activeSessions;
|
|
348
|
+
if (activeSessionDetails.length > 0) status.activeSessionDetails = activeSessionDetails;
|
|
349
|
+
if (health) {
|
|
350
|
+
status.health = health;
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
if (git) {
|
|
354
|
+
status.health = deriveMeshNodeHealthFromGit(git);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
358
|
+
}
|
|
359
|
+
|
|
88
360
|
async function resolveProviderTypeFromPriority(args: {
|
|
89
361
|
nodeId: string;
|
|
90
362
|
providerPriority: string[];
|
|
@@ -502,6 +774,8 @@ export interface CommandRouterDeps {
|
|
|
502
774
|
statusVersion?: string;
|
|
503
775
|
/** Session host control plane */
|
|
504
776
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
777
|
+
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
778
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
505
779
|
}
|
|
506
780
|
|
|
507
781
|
export interface CommandRouterResult {
|
|
@@ -632,7 +906,16 @@ export class DaemonCommandRouter {
|
|
|
632
906
|
return this.inlineMeshCache.get(meshId);
|
|
633
907
|
}
|
|
634
908
|
|
|
635
|
-
private async getMeshForCommand(
|
|
909
|
+
private async getMeshForCommand(
|
|
910
|
+
meshId: string,
|
|
911
|
+
inlineMesh?: unknown,
|
|
912
|
+
options?: { preferInline?: boolean },
|
|
913
|
+
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
914
|
+
const preferInline = options?.preferInline === true;
|
|
915
|
+
if (preferInline) {
|
|
916
|
+
const cached = this.getCachedInlineMesh(meshId, inlineMesh);
|
|
917
|
+
if (cached) return { mesh: cached, inline: true };
|
|
918
|
+
}
|
|
636
919
|
try {
|
|
637
920
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
638
921
|
const mesh = getMesh(meshId);
|
|
@@ -2734,7 +3017,7 @@ export class DaemonCommandRouter {
|
|
|
2734
3017
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2735
3018
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2736
3019
|
try {
|
|
2737
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3020
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2738
3021
|
const mesh = meshRecord?.mesh;
|
|
2739
3022
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2740
3023
|
|
|
@@ -2745,90 +3028,112 @@ export class DaemonCommandRouter {
|
|
|
2745
3028
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2746
3029
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
2747
3030
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
3031
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
3032
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
3033
|
+
: [];
|
|
3034
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
2748
3035
|
|
|
3036
|
+
const localMachineId = loadConfig().machineId || '';
|
|
3037
|
+
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
3038
|
+
? readStringValue((mesh.nodes[0] as any)?.id, (mesh.nodes[0] as any)?.nodeId)
|
|
3039
|
+
: undefined;
|
|
3040
|
+
const refreshedAt = new Date().toISOString();
|
|
2749
3041
|
const nodeStatuses = [];
|
|
2750
|
-
for (const node of mesh.nodes || []) {
|
|
3042
|
+
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
3043
|
+
const nodeId = String(node.id || node.nodeId || '');
|
|
3044
|
+
const daemonId = readStringValue(node.daemonId);
|
|
3045
|
+
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
3046
|
+
const isSelfNode = Boolean(
|
|
3047
|
+
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
3048
|
+
) || Boolean(
|
|
3049
|
+
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
3050
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
2751
3051
|
const status: Record<string, unknown> = {
|
|
2752
|
-
nodeId
|
|
3052
|
+
nodeId,
|
|
2753
3053
|
machineLabel: node.machineLabel || node.id || node.nodeId,
|
|
2754
3054
|
workspace: node.workspace,
|
|
2755
3055
|
repoRoot: node.repoRoot,
|
|
2756
3056
|
isLocalWorktree: node.isLocalWorktree,
|
|
2757
3057
|
worktreeBranch: node.worktreeBranch,
|
|
2758
|
-
daemonId
|
|
3058
|
+
daemonId,
|
|
2759
3059
|
machineId: node.machineId,
|
|
3060
|
+
machineStatus: node.machineStatus,
|
|
2760
3061
|
health: 'unknown',
|
|
2761
3062
|
providers: node.providers || [],
|
|
3063
|
+
providerPriority,
|
|
2762
3064
|
activeSessions: [],
|
|
3065
|
+
activeSessionDetails: [],
|
|
3066
|
+
launchReady: false,
|
|
2763
3067
|
};
|
|
3068
|
+
if (isSelfNode) {
|
|
3069
|
+
status.connection = {
|
|
3070
|
+
perspective: 'selected_coordinator',
|
|
3071
|
+
source: 'mesh_peer_status',
|
|
3072
|
+
state: 'self',
|
|
3073
|
+
transport: 'local',
|
|
3074
|
+
reported: true,
|
|
3075
|
+
reason: 'Selected coordinator daemon',
|
|
3076
|
+
lastStateChangeAt: refreshedAt,
|
|
3077
|
+
};
|
|
3078
|
+
} else if (daemonId) {
|
|
3079
|
+
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
3080
|
+
status.connection = connection ?? {
|
|
3081
|
+
perspective: 'selected_coordinator',
|
|
3082
|
+
source: 'not_reported',
|
|
3083
|
+
state: 'unknown',
|
|
3084
|
+
transport: 'unknown',
|
|
3085
|
+
reported: false,
|
|
3086
|
+
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
3087
|
+
};
|
|
3088
|
+
} else {
|
|
3089
|
+
status.connection = {
|
|
3090
|
+
perspective: 'selected_coordinator',
|
|
3091
|
+
source: 'not_reported',
|
|
3092
|
+
state: 'unknown',
|
|
3093
|
+
transport: 'unknown',
|
|
3094
|
+
reported: false,
|
|
3095
|
+
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
const matchedLiveSessionRecords = liveMeshSessions
|
|
3099
|
+
.filter((record) => this.sessionMatchesMeshNode(record, node, nodeId));
|
|
3100
|
+
if (matchedLiveSessionRecords.length > 0) {
|
|
3101
|
+
const sessionIds = matchedLiveSessionRecords
|
|
3102
|
+
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
3103
|
+
.filter(Boolean);
|
|
3104
|
+
const providerTypes = matchedLiveSessionRecords
|
|
3105
|
+
.map((record: any) => readStringValue(record?.providerType))
|
|
3106
|
+
.filter(Boolean) as string[];
|
|
3107
|
+
status.activeSessions = sessionIds;
|
|
3108
|
+
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
3109
|
+
if (providerTypes.length > 0) {
|
|
3110
|
+
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
2764
3113
|
if (node.workspace && typeof node.workspace === 'string') {
|
|
3114
|
+
if (!fs.existsSync(node.workspace as string) && applyCachedInlineMeshNodeStatus(status, node)) {
|
|
3115
|
+
status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
|
|
3116
|
+
nodeStatuses.push(status);
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
2765
3119
|
try {
|
|
2766
|
-
const
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
timeout: 10_000,
|
|
2774
|
-
});
|
|
2775
|
-
return result.stdout.trim();
|
|
2776
|
-
};
|
|
2777
|
-
|
|
2778
|
-
const branch = await runGit(['branch', '--show-current']).catch(() => '');
|
|
2779
|
-
const porc = await runGit(['status', '--porcelain']).catch(() => '');
|
|
2780
|
-
const headCommit = await runGit(['rev-parse', '--short', 'HEAD']).catch(() => null);
|
|
2781
|
-
const headMessage = await runGit(['log', '-1', '--format=%s']).catch(() => null);
|
|
2782
|
-
const upstream = await runGit(['rev-parse', '--abbrev-ref', '@{upstream}']).catch(() => null);
|
|
2783
|
-
const aheadBehind = await runGit(['rev-list', '--left-right', '--count', '@{upstream}...HEAD']).catch(() => '');
|
|
2784
|
-
const stashCount = await runGit(['stash', 'list']).catch(() => '');
|
|
2785
|
-
|
|
2786
|
-
let ahead = 0, behind = 0;
|
|
2787
|
-
if (aheadBehind) {
|
|
2788
|
-
const parts = aheadBehind.split(/\s+/);
|
|
2789
|
-
if (parts.length >= 2) {
|
|
2790
|
-
behind = parseInt(parts[0], 10) || 0;
|
|
2791
|
-
ahead = parseInt(parts[1], 10) || 0;
|
|
2792
|
-
}
|
|
3120
|
+
const gitStatus = await getGitRepoStatus(node.workspace as string, { timeoutMs: 10_000, refreshUpstream: true });
|
|
3121
|
+
status.git = gitStatus;
|
|
3122
|
+
if (gitStatus.isGitRepo) {
|
|
3123
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
3124
|
+
} else {
|
|
3125
|
+
status.health = 'degraded';
|
|
3126
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
2793
3127
|
}
|
|
2794
|
-
|
|
2795
|
-
const dirty = porc.length > 0;
|
|
2796
|
-
const lines = porc ? porc.split('\n').filter(Boolean) : [];
|
|
2797
|
-
let staged = 0, modified = 0, untracked = 0, deleted = 0, renamed = 0;
|
|
2798
|
-
for (const line of lines) {
|
|
2799
|
-
const xy = line.slice(0, 2);
|
|
2800
|
-
if (xy[0] !== ' ' && xy[0] !== '?') staged++;
|
|
2801
|
-
if (xy[1] === 'M') modified++;
|
|
2802
|
-
if (xy[1] === 'D') deleted++;
|
|
2803
|
-
if (xy[0] === 'R' || xy[1] === 'R') renamed++;
|
|
2804
|
-
if (xy === '??') untracked++;
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
status.git = {
|
|
2808
|
-
workspace: node.workspace,
|
|
2809
|
-
repoRoot: node.workspace,
|
|
2810
|
-
isGitRepo: true,
|
|
2811
|
-
branch: branch || null,
|
|
2812
|
-
headCommit,
|
|
2813
|
-
headMessage,
|
|
2814
|
-
upstream,
|
|
2815
|
-
ahead,
|
|
2816
|
-
behind,
|
|
2817
|
-
staged,
|
|
2818
|
-
modified,
|
|
2819
|
-
untracked,
|
|
2820
|
-
deleted,
|
|
2821
|
-
renamed,
|
|
2822
|
-
hasConflicts: false,
|
|
2823
|
-
conflictFiles: [],
|
|
2824
|
-
stashCount: stashCount ? stashCount.split('\n').filter(Boolean).length : 0,
|
|
2825
|
-
lastCheckedAt: Date.now(),
|
|
2826
|
-
};
|
|
2827
|
-
status.health = branch ? (dirty ? 'dirty' : 'online') : 'degraded';
|
|
2828
3128
|
} catch {
|
|
2829
|
-
status
|
|
3129
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
3130
|
+
status.health = 'degraded';
|
|
3131
|
+
}
|
|
2830
3132
|
}
|
|
3133
|
+
} else {
|
|
3134
|
+
applyCachedInlineMeshNodeStatus(status, node);
|
|
2831
3135
|
}
|
|
3136
|
+
status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
|
|
2832
3137
|
nodeStatuses.push(status);
|
|
2833
3138
|
}
|
|
2834
3139
|
|
|
@@ -2838,6 +3143,7 @@ export class DaemonCommandRouter {
|
|
|
2838
3143
|
meshName: mesh.name,
|
|
2839
3144
|
repoIdentity: mesh.repoIdentity,
|
|
2840
3145
|
defaultBranch: mesh.defaultBranch,
|
|
3146
|
+
refreshedAt: new Date().toISOString(),
|
|
2841
3147
|
nodes: nodeStatuses,
|
|
2842
3148
|
queue: { tasks: queue, summary: queueSummary },
|
|
2843
3149
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
package/src/git/git-commands.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface GitPushResult extends GitRepoIdentity {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export interface GitCommandServices {
|
|
65
|
-
getStatus?: (params: { workspace: string }) => Promise<GitRepoStatus> | GitRepoStatus;
|
|
65
|
+
getStatus?: (params: { workspace: string; refreshUpstream?: boolean }) => Promise<GitRepoStatus> | GitRepoStatus;
|
|
66
66
|
getDiffSummary?: (params: { workspace: string; staged?: boolean }) => Promise<GitDiffSummary> | GitDiffSummary;
|
|
67
67
|
getDiffFile?: (params: { workspace: string; path: string; staged?: boolean }) => Promise<GitFileDiff> | GitFileDiff;
|
|
68
68
|
createSnapshot?: (params: {
|
|
@@ -171,7 +171,7 @@ const defaultSnapshotStore = createGitSnapshotStore({
|
|
|
171
171
|
|
|
172
172
|
export function createDefaultGitCommandServices(): GitCommandServices {
|
|
173
173
|
return {
|
|
174
|
-
getStatus: ({ workspace }) => getGitRepoStatus(workspace),
|
|
174
|
+
getStatus: ({ workspace, refreshUpstream }) => getGitRepoStatus(workspace, { refreshUpstream }),
|
|
175
175
|
getDiffSummary: ({ workspace }) => getGitDiffSummary(workspace),
|
|
176
176
|
getDiffFile: ({ workspace, path: filePath }) => getGitFileDiff(workspace, filePath),
|
|
177
177
|
createSnapshot: ({ workspace, reason, sessionId, turnId }) => defaultSnapshotStore.create({
|
|
@@ -290,7 +290,7 @@ export async function handleGitCommand(
|
|
|
290
290
|
switch (command) {
|
|
291
291
|
case 'git_status': {
|
|
292
292
|
if (!services.getStatus) return serviceNotImplemented(command);
|
|
293
|
-
const status = await runService(() => services.getStatus!({ workspace }));
|
|
293
|
+
const status = await runService(() => services.getStatus!({ workspace, refreshUpstream: optionalBoolean(args?.refreshUpstream) }));
|
|
294
294
|
return 'success' in status ? status : { success: true, status };
|
|
295
295
|
}
|
|
296
296
|
|