@adhdev/mesh-shared 0.9.82-rc.267
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/git-normalize.d.ts +23 -0
- package/dist/git-summarize.d.ts +10 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +300 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +260 -0
- package/dist/index.mjs.map +1 -0
- package/dist/json.d.ts +21 -0
- package/dist/session-normalize.d.ts +7 -0
- package/dist/types.d.ts +110 -0
- package/package.json +48 -0
- package/src/git-normalize.ts +176 -0
- package/src/git-summarize.ts +48 -0
- package/src/index.ts +17 -0
- package/src/json.ts +58 -0
- package/src/session-normalize.ts +59 -0
- package/src/types.ts +126 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical git-status normalizers shared by the cloud (web-core) and standalone
|
|
3
|
+
* (daemon-core router) mesh paths. Previously each transport hand-maintained its
|
|
4
|
+
* own copy and they drifted (e.g. submodule drop rules, evidence checks); this is
|
|
5
|
+
* the one implementation both import.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { joinRepoPath, readBoolean, readNumber, readRecord, readString, type JsonRecord } from './json'
|
|
9
|
+
import type { GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './types'
|
|
10
|
+
|
|
11
|
+
export function scoreGitUpstreamFreshness(status: GitUpstreamFreshness | undefined): number {
|
|
12
|
+
switch (status) {
|
|
13
|
+
case 'fresh':
|
|
14
|
+
return 30
|
|
15
|
+
case 'no_upstream':
|
|
16
|
+
return 4
|
|
17
|
+
case 'unchecked':
|
|
18
|
+
case undefined:
|
|
19
|
+
return 0
|
|
20
|
+
case 'stale':
|
|
21
|
+
return -10
|
|
22
|
+
case 'unavailable':
|
|
23
|
+
return -15
|
|
24
|
+
default:
|
|
25
|
+
return 0
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmoduleStatus[] | undefined {
|
|
30
|
+
if (!Array.isArray(value)) return undefined
|
|
31
|
+
const submodules = value
|
|
32
|
+
.map(entry => {
|
|
33
|
+
const submodule = readRecord(entry)
|
|
34
|
+
const path = readString(submodule.path)
|
|
35
|
+
const commit = readString(submodule.commit)
|
|
36
|
+
const repoPath = readString(submodule.repoPath, submodule.repo_root)
|
|
37
|
+
?? joinRepoPath(parentRepoRoot, path)
|
|
38
|
+
// repoPath is only used for the submodule node's display workspace, which is
|
|
39
|
+
// allowed to be empty. The cloud P2P transit path can deliver submodule entries
|
|
40
|
+
// without repoPath (and a per-node git object without a derivable repoRoot), so
|
|
41
|
+
// dropping on missing repoPath would silently strip every submodule graph node.
|
|
42
|
+
// Keep any submodule that carries both path and commit.
|
|
43
|
+
if (!path || !commit) return null
|
|
44
|
+
const result: GitSubmoduleStatus = {
|
|
45
|
+
path,
|
|
46
|
+
commit,
|
|
47
|
+
dirty: readBoolean(submodule.dirty) ?? false,
|
|
48
|
+
outOfSync: readBoolean(submodule.outOfSync, submodule.out_of_sync) ?? false,
|
|
49
|
+
lastCheckedAt: readNumber(submodule.lastCheckedAt, submodule.last_checked_at) ?? Date.now(),
|
|
50
|
+
}
|
|
51
|
+
if (repoPath) result.repoPath = repoPath
|
|
52
|
+
const error = readString(submodule.error)
|
|
53
|
+
if (error) result.error = error
|
|
54
|
+
return result
|
|
55
|
+
})
|
|
56
|
+
.filter((entry): entry is GitSubmoduleStatus => entry !== null)
|
|
57
|
+
return submodules.length > 0 ? submodules : undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function hasGitStatusEvidence(status: JsonRecord): boolean {
|
|
61
|
+
// BUG FIX: a transit-reshaped git status that carries only a repoRoot/workspace
|
|
62
|
+
// (e.g. cloud P2P stripped the branch/upstream/counters but kept the path) must
|
|
63
|
+
// NOT be dropped — otherwise the node loses its git object and any submodules
|
|
64
|
+
// hanging off it. Treat a present repoRoot/repo_root/workspace as evidence too.
|
|
65
|
+
return readBoolean(status.isGitRepo) !== undefined
|
|
66
|
+
|| Boolean(readString(status.branch, status.upstream, status.upstreamStatus, status.upstream_status, status.headCommit))
|
|
67
|
+
|| Boolean(readString(status.repoRoot, status.repo_root, status.workspace))
|
|
68
|
+
|| readNumber(
|
|
69
|
+
status.ahead,
|
|
70
|
+
status.behind,
|
|
71
|
+
status.staged,
|
|
72
|
+
status.modified,
|
|
73
|
+
status.untracked,
|
|
74
|
+
status.deleted,
|
|
75
|
+
status.renamed,
|
|
76
|
+
status.lastCheckedAt,
|
|
77
|
+
status.last_checked_at,
|
|
78
|
+
) !== undefined
|
|
79
|
+
|| (Array.isArray(status.submodules) && status.submodules.length > 0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function normalizeGitStatus(
|
|
83
|
+
status: JsonRecord,
|
|
84
|
+
node: JsonRecord,
|
|
85
|
+
options?: { lastCheckedAt?: number },
|
|
86
|
+
): GitRepoStatus | undefined {
|
|
87
|
+
const explicitIsGitRepo = readBoolean(status.isGitRepo)
|
|
88
|
+
if (!Object.keys(status).length || !hasGitStatusEvidence(status)) return undefined
|
|
89
|
+
const isGitRepo = explicitIsGitRepo ?? true
|
|
90
|
+
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
91
|
+
? status.conflictFiles.filter((entry): entry is string => typeof entry === 'string')
|
|
92
|
+
: []
|
|
93
|
+
const conflictCount = readNumber(status.conflicts) ?? conflictFiles.length
|
|
94
|
+
const hasConflicts = readBoolean(status.hasConflicts) ?? conflictCount > 0
|
|
95
|
+
// node.workspace is in the fallback chain so a transit node carrying its path only
|
|
96
|
+
// on the node (not the inner git object) still yields a parentRepoRoot for submodules.
|
|
97
|
+
const repoRoot = readString(status.repoRoot, status.repo_root, node.repoRoot, node.repo_root, status.workspace, node.workspace) || undefined
|
|
98
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot)
|
|
99
|
+
const upstreamStatus = readString(status.upstreamStatus, status.upstream_status)
|
|
100
|
+
const upstreamFetchedAt = readNumber(status.upstreamFetchedAt, status.upstream_fetched_at)
|
|
101
|
+
const upstreamFetchError = readString(status.upstreamFetchError, status.upstream_fetch_error)
|
|
102
|
+
const error = readString(status.error)
|
|
103
|
+
const staged = readNumber(status.staged) ?? 0
|
|
104
|
+
const modified = readNumber(status.modified) ?? 0
|
|
105
|
+
const untracked = readNumber(status.untracked) ?? 0
|
|
106
|
+
const deleted = readNumber(status.deleted) ?? 0
|
|
107
|
+
const renamed = readNumber(status.renamed) ?? 0
|
|
108
|
+
return {
|
|
109
|
+
workspace: readString(status.workspace, node.workspace) || '',
|
|
110
|
+
repoRoot: repoRoot ?? null,
|
|
111
|
+
isGitRepo,
|
|
112
|
+
branch: readString(status.branch) ?? null,
|
|
113
|
+
headCommit: readString(status.headCommit) ?? null,
|
|
114
|
+
headMessage: readString(status.headMessage) ?? null,
|
|
115
|
+
upstream: readString(status.upstream) ?? null,
|
|
116
|
+
upstreamStatus: (upstreamStatus as GitUpstreamFreshness) ?? 'unchecked',
|
|
117
|
+
...(upstreamFetchedAt !== undefined ? { upstreamFetchedAt } : {}),
|
|
118
|
+
...(upstreamFetchError ? { upstreamFetchError } : {}),
|
|
119
|
+
ahead: readNumber(status.ahead) ?? 0,
|
|
120
|
+
behind: readNumber(status.behind) ?? 0,
|
|
121
|
+
staged,
|
|
122
|
+
modified,
|
|
123
|
+
untracked,
|
|
124
|
+
deleted,
|
|
125
|
+
renamed,
|
|
126
|
+
dirty: readBoolean(status.dirty, status.isDirty, status.is_dirty) ?? (staged + modified + untracked + deleted + renamed > 0 || hasConflicts),
|
|
127
|
+
hasConflicts,
|
|
128
|
+
conflictFiles,
|
|
129
|
+
stashCount: readNumber(status.stashCount, status.stash_count) ?? 0,
|
|
130
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumber(status.lastCheckedAt, status.last_checked_at) ?? Date.now(),
|
|
131
|
+
...(submodules ? { submodules } : {}),
|
|
132
|
+
...(error ? { error } : {}),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function scoreGitStatusCandidate(git: GitRepoStatus | undefined): number {
|
|
137
|
+
if (!git) return Number.NEGATIVE_INFINITY
|
|
138
|
+
let score = 0
|
|
139
|
+
if (git.isGitRepo === true) score += 50
|
|
140
|
+
if (git.isGitRepo === false) score -= 10
|
|
141
|
+
if (git.branch) score += 20
|
|
142
|
+
if (git.headCommit) score += 20
|
|
143
|
+
if (git.upstream) score += 10
|
|
144
|
+
score += scoreGitUpstreamFreshness(git.upstreamStatus)
|
|
145
|
+
if (typeof git.ahead === 'number') score += 2
|
|
146
|
+
if (typeof git.behind === 'number') score += 2
|
|
147
|
+
if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length
|
|
148
|
+
if (git.error) score -= 20
|
|
149
|
+
return score
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Pick the best git status out of the four transit envelope slots a mesh node can
|
|
154
|
+
* carry: lastGit.status, lastGit.result.status, lastProbe.git.status,
|
|
155
|
+
* lastProbe.git.result.status. Returns undefined when none carry git evidence.
|
|
156
|
+
*/
|
|
157
|
+
export function pickBestTransitGitStatus(node: JsonRecord, options?: { lastCheckedAt?: number }): GitRepoStatus | undefined {
|
|
158
|
+
const rawGit = readRecord(node.lastGit ?? node.last_git)
|
|
159
|
+
const gitResult = readRecord(rawGit.result)
|
|
160
|
+
const directStatus = readRecord(rawGit.status)
|
|
161
|
+
const nestedStatus = readRecord(gitResult.status)
|
|
162
|
+
const rawProbe = readRecord(node.lastProbe ?? node.last_probe)
|
|
163
|
+
const probeGit = readRecord(rawProbe.git)
|
|
164
|
+
const probeGitResult = readRecord(probeGit.result)
|
|
165
|
+
const probeDirectStatus = readRecord(probeGit.status)
|
|
166
|
+
const probeNestedStatus = readRecord(probeGitResult.status)
|
|
167
|
+
const lastCheckedAt = options?.lastCheckedAt
|
|
168
|
+
let best: { git: GitRepoStatus; score: number } | null = null
|
|
169
|
+
for (const status of [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus]) {
|
|
170
|
+
const normalized = normalizeGitStatus(status, node, { lastCheckedAt: lastCheckedAt ?? Date.now() })
|
|
171
|
+
if (!normalized) continue
|
|
172
|
+
const score = scoreGitStatusCandidate(normalized)
|
|
173
|
+
if (!best || score > best.score) best = { git: normalized, score }
|
|
174
|
+
}
|
|
175
|
+
return best?.git
|
|
176
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical compact git-shape summarizer used by debug/log surfaces on both the
|
|
3
|
+
* cloud (daemon-cloud mesh command summarizer) and standalone (daemon-core router
|
|
4
|
+
* RepoMeshStatusDebug) paths. Produces a small, log-safe projection of a git
|
|
5
|
+
* status record — commit SHAs are truncated to 12 chars.
|
|
6
|
+
*
|
|
7
|
+
* Transport-specific envelope unwrapping (result / result.status / top-level)
|
|
8
|
+
* stays in the caller; this takes an already-unwrapped status record.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readBoolean, readNumber, readRecord, readString } from './json'
|
|
12
|
+
|
|
13
|
+
export function summarizeGitShape(status: unknown): Record<string, unknown> | null {
|
|
14
|
+
const record = readRecord(status)
|
|
15
|
+
if (!Object.keys(record).length) return null
|
|
16
|
+
const submodules = Array.isArray(record.submodules)
|
|
17
|
+
? record.submodules.map((entry: unknown) => {
|
|
18
|
+
const sub = readRecord(entry)
|
|
19
|
+
return {
|
|
20
|
+
path: readString(sub.path) ?? null,
|
|
21
|
+
commit: readString(sub.commit)?.slice(0, 12) ?? null,
|
|
22
|
+
dirty: readBoolean(sub.dirty) ?? false,
|
|
23
|
+
outOfSync: readBoolean(sub.outOfSync, sub.out_of_sync) ?? false,
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
: []
|
|
27
|
+
return {
|
|
28
|
+
isGitRepo: readBoolean(record.isGitRepo),
|
|
29
|
+
workspace: readString(record.workspace) ?? null,
|
|
30
|
+
repoRoot: readString(record.repoRoot, record.repo_root) ?? null,
|
|
31
|
+
branch: readString(record.branch) ?? null,
|
|
32
|
+
upstream: readString(record.upstream) ?? null,
|
|
33
|
+
upstreamStatus: readString(record.upstreamStatus, record.upstream_status) ?? null,
|
|
34
|
+
headCommit: readString(record.headCommit, record.head_commit)?.slice(0, 12) ?? null,
|
|
35
|
+
ahead: readNumber(record.ahead) ?? null,
|
|
36
|
+
behind: readNumber(record.behind) ?? null,
|
|
37
|
+
dirtyCounts: {
|
|
38
|
+
staged: readNumber(record.staged) ?? 0,
|
|
39
|
+
modified: readNumber(record.modified) ?? 0,
|
|
40
|
+
untracked: readNumber(record.untracked) ?? 0,
|
|
41
|
+
deleted: readNumber(record.deleted) ?? 0,
|
|
42
|
+
renamed: readNumber(record.renamed) ?? 0,
|
|
43
|
+
},
|
|
44
|
+
lastCheckedAt: readNumber(record.lastCheckedAt, record.last_checked_at) ?? null,
|
|
45
|
+
submoduleCount: submodules.length,
|
|
46
|
+
submodules,
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @adhdev/mesh-shared — pure, dependency-free mesh/git status normalizers shared
|
|
3
|
+
* by daemon-core (standalone / local IPC) and web-core (cloud / P2P transit).
|
|
4
|
+
*
|
|
5
|
+
* This package exists to kill a recurring bug class: the cloud and standalone
|
|
6
|
+
* transports each used to carry their own hand-synced copy of these normalizers,
|
|
7
|
+
* and they drifted (cloud would strip/reshape fields, the web filter would drop
|
|
8
|
+
* entries the standalone path kept). Both cores now import this single source of
|
|
9
|
+
* truth. It MUST stay a pure leaf — types + pure functions on plain JS objects,
|
|
10
|
+
* no Node/DOM APIs, no git exec, no transport, and an empty dependency set.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export * from './json'
|
|
14
|
+
export * from './types'
|
|
15
|
+
export * from './git-normalize'
|
|
16
|
+
export * from './session-normalize'
|
|
17
|
+
export * from './git-summarize'
|
package/src/json.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure JSON-record reading primitives shared by the cloud (web-core / P2P transit)
|
|
3
|
+
* and standalone (daemon-core / local IPC) mesh normalizers.
|
|
4
|
+
*
|
|
5
|
+
* These operate only on plain JS values — no Node/DOM APIs, no transport, no git
|
|
6
|
+
* exec — so both cores can import them without violating the core↔core dependency
|
|
7
|
+
* ban. They are the single source of truth for the field-coercion rules that the
|
|
8
|
+
* two transports previously hand-synced (and drifted on).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type JsonRecord = Record<string, unknown>
|
|
12
|
+
|
|
13
|
+
export function readRecord(value: unknown): JsonRecord {
|
|
14
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readString(...values: unknown[]): string | undefined {
|
|
18
|
+
for (const value of values) {
|
|
19
|
+
if (typeof value !== 'string') continue
|
|
20
|
+
const trimmed = value.trim()
|
|
21
|
+
if (trimmed) return trimmed
|
|
22
|
+
}
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readNumber(...values: unknown[]): number | undefined {
|
|
27
|
+
for (const value of values) {
|
|
28
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
29
|
+
}
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readBoolean(...values: unknown[]): boolean | undefined {
|
|
34
|
+
for (const value of values) {
|
|
35
|
+
if (typeof value === 'boolean') return value
|
|
36
|
+
}
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readStringArray(value: unknown): string[] {
|
|
41
|
+
return Array.isArray(value)
|
|
42
|
+
? value.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
43
|
+
: []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Join a (possibly absent) repo root with a relative submodule path. Returns the
|
|
48
|
+
* path unchanged when it is already absolute, and undefined when nothing usable
|
|
49
|
+
* can be derived — callers must treat the result as optional.
|
|
50
|
+
*/
|
|
51
|
+
export function joinRepoPath(root: string | undefined, relativePath: string | undefined): string | undefined {
|
|
52
|
+
const normalizedRoot = typeof root === 'string' ? root.trim().replace(/[\\/]+$/, '') : ''
|
|
53
|
+
const normalizedPath = typeof relativePath === 'string' ? relativePath.trim() : ''
|
|
54
|
+
if (!normalizedPath) return undefined
|
|
55
|
+
if (/^(?:[A-Za-z]:[\\/]|\/)/.test(normalizedPath)) return normalizedPath
|
|
56
|
+
if (!normalizedRoot) return undefined
|
|
57
|
+
return `${normalizedRoot}/${normalizedPath.replace(/^[\\/]+/, '')}`
|
|
58
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical mesh-session record normalizer shared by the cloud (web-core) mesh
|
|
3
|
+
* paths. Parses an already-transit-shaped session record into a typed
|
|
4
|
+
* RepoMeshSessionStatus.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readBoolean, readRecord, readString } from './json'
|
|
8
|
+
import type { RepoMeshSessionStatus } from './types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a deterministic synthetic session id from a record that has no explicit
|
|
12
|
+
* id. Two transit refreshes of the same logical session produce the same id so
|
|
13
|
+
* downstream dedupe stays stable across refreshes (a random id would create a new
|
|
14
|
+
* node every poll). Derived from the stable identifying fields available on the
|
|
15
|
+
* record; prefixed "synthetic:" so callers can tell it apart from a real id.
|
|
16
|
+
*/
|
|
17
|
+
function deriveSyntheticSessionId(record: ReturnType<typeof readRecord>): string | undefined {
|
|
18
|
+
const parts = [
|
|
19
|
+
readString(record.workspace),
|
|
20
|
+
readString(record.providerType, record.provider),
|
|
21
|
+
readString(record.role),
|
|
22
|
+
readString(record.state, record.status),
|
|
23
|
+
readString(record.title),
|
|
24
|
+
readString(record.createdAt, record.created_at),
|
|
25
|
+
readString(record.startedAt, record.started_at),
|
|
26
|
+
].filter((part): part is string => Boolean(part))
|
|
27
|
+
if (parts.length === 0) return undefined
|
|
28
|
+
return `synthetic:${parts.join('|')}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeMeshSessionRecord(entry: unknown): RepoMeshSessionStatus | null {
|
|
32
|
+
const record = readRecord(entry)
|
|
33
|
+
// BUG FIX: cloud transit can reshape/strip the explicit id field. Fall back
|
|
34
|
+
// through sessionId → session_id → id, then to a DETERMINISTIC synthetic id
|
|
35
|
+
// derived from record content so the session survives the round trip instead
|
|
36
|
+
// of being dropped (and so dedupe stays stable across refreshes). Return null
|
|
37
|
+
// ONLY when the record carries no identifying fields at all.
|
|
38
|
+
const sessionId = readString(record.sessionId, record.session_id, record.id)
|
|
39
|
+
?? deriveSyntheticSessionId(record)
|
|
40
|
+
if (!sessionId) return null
|
|
41
|
+
return {
|
|
42
|
+
sessionId,
|
|
43
|
+
...(readString(record.providerType, record.provider) ? { providerType: readString(record.providerType, record.provider) } : {}),
|
|
44
|
+
...(readString(record.state, record.status) ? { state: readString(record.state, record.status) } : {}),
|
|
45
|
+
...(readString(record.chatStatus, record.chat_status) ? { chatStatus: readString(record.chatStatus, record.chat_status) } : {}),
|
|
46
|
+
...(readString(record.lifecycle) ? { lifecycle: readString(record.lifecycle) as RepoMeshSessionStatus['lifecycle'] } : {}),
|
|
47
|
+
...(readString(record.surfaceKind, record.surface_kind) ? { surfaceKind: readString(record.surfaceKind, record.surface_kind) as RepoMeshSessionStatus['surfaceKind'] } : {}),
|
|
48
|
+
...(readString(record.recoveryState, record.recovery_state) ? { recoveryState: readString(record.recoveryState, record.recovery_state) } : {}),
|
|
49
|
+
...(readString(record.workspace) ? { workspace: readString(record.workspace) } : {}),
|
|
50
|
+
...(readString(record.title) ? { title: readString(record.title) } : {}),
|
|
51
|
+
...(readString(record.role) ? { role: readString(record.role) } : {}),
|
|
52
|
+
...(readBoolean(record.isSelfCoordinator, record.is_self_coordinator) !== undefined ? { isSelfCoordinator: readBoolean(record.isSelfCoordinator, record.is_self_coordinator) } : {}),
|
|
53
|
+
...(readString(record.statusNote, record.status_note) ? { statusNote: readString(record.statusNote, record.status_note) } : {}),
|
|
54
|
+
...(readString(record.createdAt, record.created_at) ? { createdAt: readString(record.createdAt, record.created_at) } : {}),
|
|
55
|
+
...(readString(record.startedAt, record.started_at) ? { startedAt: readString(record.startedAt, record.started_at) } : {}),
|
|
56
|
+
...(readString(record.lastActivityAt, record.last_activity_at) ? { lastActivityAt: readString(record.lastActivityAt, record.last_activity_at) } : {}),
|
|
57
|
+
...(readBoolean(record.isCached, record.is_cached) !== undefined ? { isCached: readBoolean(record.isCached, record.is_cached) } : {}),
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical pure-shape mesh/git status types.
|
|
3
|
+
*
|
|
4
|
+
* These are the single source of truth for the data shapes that cross the
|
|
5
|
+
* cloud (P2P/transit) and standalone (local IPC) boundaries. daemon-core and
|
|
6
|
+
* web-core re-export these so neither side can drift its own copy. Pure type
|
|
7
|
+
* declarations only — no runtime, no dependency on either core.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type GitUpstreamFreshness = 'fresh' | 'unchecked' | 'stale' | 'no_upstream' | 'unavailable'
|
|
11
|
+
|
|
12
|
+
export type GitFailureReason =
|
|
13
|
+
| 'not_git_repo'
|
|
14
|
+
| 'git_not_installed'
|
|
15
|
+
| 'timeout'
|
|
16
|
+
| 'path_outside_repo'
|
|
17
|
+
| 'dirty_index_required'
|
|
18
|
+
| 'conflict'
|
|
19
|
+
| 'invalid_args'
|
|
20
|
+
| 'nothing_to_commit'
|
|
21
|
+
| 'git_command_failed'
|
|
22
|
+
|
|
23
|
+
export interface GitRepoIdentity {
|
|
24
|
+
workspace: string
|
|
25
|
+
repoRoot: string | null
|
|
26
|
+
isGitRepo: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GitSubmoduleStatus {
|
|
30
|
+
/** Submodule path relative to repo root */
|
|
31
|
+
path: string
|
|
32
|
+
/** Current commit SHA the submodule is at */
|
|
33
|
+
commit: string
|
|
34
|
+
/**
|
|
35
|
+
* Path to the submodule repo (absolute). Optional: cloud P2P transit can
|
|
36
|
+
* deliver submodule entries without a derivable repo path, and graph
|
|
37
|
+
* rendering only uses this for a display field that is allowed to be empty.
|
|
38
|
+
*/
|
|
39
|
+
repoPath?: string
|
|
40
|
+
/** Whether the submodule has uncommitted changes */
|
|
41
|
+
dirty: boolean
|
|
42
|
+
/** Whether the submodule commit differs from what the parent repo expects */
|
|
43
|
+
outOfSync: boolean
|
|
44
|
+
/** Last checked timestamp */
|
|
45
|
+
lastCheckedAt: number
|
|
46
|
+
/** Error message if submodule status could not be read */
|
|
47
|
+
error?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DaemonBuildBehind {
|
|
51
|
+
/** Full build commit baked into the running daemon. */
|
|
52
|
+
buildCommit: string
|
|
53
|
+
/** Short build commit. */
|
|
54
|
+
buildCommitShort: string
|
|
55
|
+
/** HEAD commit the build commit is behind (repo or submodule). */
|
|
56
|
+
head: string
|
|
57
|
+
/** Where the comparison matched: 'root' or the submodule path. */
|
|
58
|
+
scope: string
|
|
59
|
+
/**
|
|
60
|
+
* Whether any package changed between buildCommit..HEAD affects the daemon
|
|
61
|
+
* runtime (daemon-core, standalone, session-host, terminal-mux, ghostty,
|
|
62
|
+
* mcp-server). When false, only web/render packages changed — the daemon does
|
|
63
|
+
* NOT need a rebuild/restart; only the web deploy is pending. Conservative:
|
|
64
|
+
* when the changed-package set can't be determined it defaults to true.
|
|
65
|
+
*/
|
|
66
|
+
isDaemonAffecting: boolean
|
|
67
|
+
/** Distinct package names changed between buildCommit..HEAD (best-effort). */
|
|
68
|
+
affectedPackages?: string[]
|
|
69
|
+
warning: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface GitRepoStatus extends GitRepoIdentity {
|
|
73
|
+
branch: string | null
|
|
74
|
+
headCommit: string | null
|
|
75
|
+
headMessage: string | null
|
|
76
|
+
upstream: string | null
|
|
77
|
+
/** Whether ahead/behind was verified against a freshly fetched upstream ref. */
|
|
78
|
+
upstreamStatus: GitUpstreamFreshness
|
|
79
|
+
/** Timestamp for the fetch that refreshed upstream refs when upstreamStatus === 'fresh'. */
|
|
80
|
+
upstreamFetchedAt?: number
|
|
81
|
+
/** Error from the last refresh attempt when upstreamStatus === 'stale'. */
|
|
82
|
+
upstreamFetchError?: string
|
|
83
|
+
ahead: number
|
|
84
|
+
behind: number
|
|
85
|
+
staged: number
|
|
86
|
+
modified: number
|
|
87
|
+
untracked: number
|
|
88
|
+
deleted: number
|
|
89
|
+
renamed: number
|
|
90
|
+
/** Aggregate dirty flag including root worktree changes, conflicts, stash, and submodule drift. */
|
|
91
|
+
dirty: boolean
|
|
92
|
+
hasConflicts: boolean
|
|
93
|
+
conflictFiles: string[]
|
|
94
|
+
stashCount: number
|
|
95
|
+
lastCheckedAt: number
|
|
96
|
+
/** Submodule statuses when auto-discover is enabled */
|
|
97
|
+
submodules?: GitSubmoduleStatus[]
|
|
98
|
+
/**
|
|
99
|
+
* Set when the running daemon's build commit is a STRICT ancestor of this
|
|
100
|
+
* repo's HEAD (or a submodule HEAD) — i.e. the live daemon predates committed
|
|
101
|
+
* code in this workspace and is awaiting a deploy/restart to catch up.
|
|
102
|
+
* Omitted entirely when no staleness is provable.
|
|
103
|
+
*/
|
|
104
|
+
daemonBuildBehind?: DaemonBuildBehind
|
|
105
|
+
error?: string
|
|
106
|
+
reason?: GitFailureReason
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface RepoMeshSessionStatus {
|
|
110
|
+
sessionId: string
|
|
111
|
+
providerType?: string
|
|
112
|
+
state?: string
|
|
113
|
+
chatStatus?: string
|
|
114
|
+
lifecycle?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'interrupted'
|
|
115
|
+
surfaceKind?: 'live_runtime' | 'recovery_snapshot' | 'inactive_record'
|
|
116
|
+
recoveryState?: string | null
|
|
117
|
+
workspace?: string | null
|
|
118
|
+
title?: string | null
|
|
119
|
+
role?: string | null
|
|
120
|
+
isSelfCoordinator?: boolean
|
|
121
|
+
statusNote?: string | null
|
|
122
|
+
createdAt?: string | null
|
|
123
|
+
startedAt?: string | null
|
|
124
|
+
lastActivityAt?: string | null
|
|
125
|
+
isCached?: boolean
|
|
126
|
+
}
|