@adhdev/daemon-core 0.9.82-rc.19 → 0.9.82-rc.190
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 +1 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +22 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/commands/cli-manager.d.ts +2 -1
- package/dist/commands/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +26 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +72 -1
- package/dist/git/git-commands.d.ts +7 -1
- package/dist/git/git-types.d.ts +2 -0
- package/dist/index.d.ts +37 -10
- package/dist/index.js +32814 -16395
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32529 -16180
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +73 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +30 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-events.d.ts +143 -6
- package/dist/mesh/mesh-fast-forward.d.ts +41 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +58 -1
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-work-queue.d.ts +55 -6
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
- package/dist/providers/approval-utils.d.ts +13 -0
- package/dist/providers/cli-provider-instance.d.ts +39 -3
- package/dist/providers/contracts.d.ts +130 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/dispatcher.d.ts +27 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +12 -0
- package/dist/providers/provider-instance.d.ts +12 -0
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +60 -0
- package/dist/providers/spec/cli-adapter.d.ts +98 -0
- package/dist/providers/spec/driver.d.ts +168 -0
- package/dist/providers/spec/evaluator.d.ts +55 -0
- package/dist/providers/spec/loader.d.ts +14 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +549 -0
- package/dist/providers/spec/types.d.ts +237 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +48 -0
- package/dist/repo-mesh-types.d.ts +108 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -2
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +19 -10
- package/src/boot/process-hardening.ts +89 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +21 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1086 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +59 -11
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +1787 -60
- package/src/commands/cli-manager.ts +283 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +331 -122
- package/src/commands/router.ts +3600 -510
- package/src/config/chat-history.ts +95 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +280 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +37 -7
- package/src/git/git-status.ts +35 -6
- package/src/git/git-types.ts +2 -0
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +115 -9
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +490 -0
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +204 -30
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-events.ts +1234 -71
- package/src/mesh/mesh-fast-forward.ts +438 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +457 -104
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-work-queue.ts +267 -159
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +247 -0
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +763 -60
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +19 -5
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/dispatcher.ts +340 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +30 -0
- package/src/providers/provider-instance.ts +9 -0
- package/src/providers/provider-loader.ts +582 -50
- package/src/providers/provider-schema.ts +87 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +176 -0
- package/src/providers/spec/cli-adapter.ts +534 -0
- package/src/providers/spec/driver.ts +612 -0
- package/src/providers/spec/evaluator.ts +344 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +939 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +538 -0
- package/src/providers/spec/schema.json +235 -0
- package/src/providers/spec/types.ts +256 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +408 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +118 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -6
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +84 -25
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -1,24 +1,76 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
1
|
import { randomUUID } from 'crypto';
|
|
4
|
-
import {
|
|
2
|
+
import { requireMeshHostQueueOwner } from './mesh-host-ownership.js';
|
|
3
|
+
import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
|
|
4
|
+
import { BeadsDB } from './beads-db.js';
|
|
5
5
|
|
|
6
6
|
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
|
|
7
7
|
export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
|
|
8
8
|
export type MeshHistoricalTaskStatus = Extract<MeshTaskStatus, 'completed' | 'failed' | 'cancelled'>;
|
|
9
|
+
export type MeshTaskMode = 'code_change' | 'validation' | 'live_debug_readonly' | 'launch_app' | 'convergence';
|
|
9
10
|
|
|
10
11
|
export const ACTIVE_MESH_QUEUE_STATUSES: MeshActiveTaskStatus[] = ['pending', 'assigned'];
|
|
11
12
|
export const HISTORICAL_MESH_QUEUE_STATUSES: MeshHistoricalTaskStatus[] = ['completed', 'failed', 'cancelled'];
|
|
13
|
+
export const MESH_TASK_MODES: MeshTaskMode[] = ['code_change', 'validation', 'live_debug_readonly', 'launch_app', 'convergence'];
|
|
14
|
+
|
|
15
|
+
export interface MeshTaskModeValidationResult {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
taskMode?: MeshTaskMode;
|
|
18
|
+
violations: string[];
|
|
19
|
+
allowedOperations?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const LIVE_DEBUG_READONLY_FORBIDDEN: Array<{ label: string; pattern: RegExp }> = [
|
|
23
|
+
{ label: 'source_edit', pattern: /\b(edit|modify|patch|apply\s+patch|write\s+(?:to\s+)?(?:file|source)|overwrite|delete\s+file|remove\s+file|create\s+file|touch\s+file)\b/i },
|
|
24
|
+
{ label: 'git_mutation', pattern: /\b(?:git\s+(?:add|commit|push|reset|rebase|clean|checkout|switch|merge|tag|restore|rm|mv|stash|worktree\s+(?:add|remove|move))|push\b)/i },
|
|
25
|
+
{ label: 'checkpoint', pattern: /\b(checkpoint|mesh_checkpoint)\b/i },
|
|
26
|
+
{ label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release|npm\s+publish|yarn\s+publish|pnpm\s+publish)\b/i },
|
|
27
|
+
{ label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i|shred\b)\b/i },
|
|
28
|
+
{ label: 'package_install', pattern: /\b(npm\s+(?:install|i|add|link|uninstall|remove)|yarn\s+(?:add|remove|link)|pnpm\s+(?:add|remove|link)|pip\s+install|brew\s+install|apt\s+install|cargo\s+install)\b/i },
|
|
29
|
+
{ label: 'container_mutation', pattern: /\b(docker\s+(?:build|run|exec|push|tag|rmi|rm|create|start|stop|kill)|kubectl\s+(?:apply|delete|patch|replace|create|scale))\b/i },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
|
|
33
|
+
if (typeof value !== 'string') return undefined;
|
|
34
|
+
const normalized = value.trim() as MeshTaskMode;
|
|
35
|
+
return (MESH_TASK_MODES as string[]).includes(normalized) ? normalized : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateMeshTaskModeRequest(mode: unknown, message: string): MeshTaskModeValidationResult {
|
|
39
|
+
const taskMode = normalizeMeshTaskMode(mode);
|
|
40
|
+
if (!taskMode) {
|
|
41
|
+
return { valid: true, violations: [] };
|
|
42
|
+
}
|
|
43
|
+
if (taskMode !== 'live_debug_readonly') {
|
|
44
|
+
return { valid: true, taskMode, violations: [] };
|
|
45
|
+
}
|
|
46
|
+
const violations = LIVE_DEBUG_READONLY_FORBIDDEN
|
|
47
|
+
.filter(rule => rule.pattern.test(message || ''))
|
|
48
|
+
.map(rule => rule.label);
|
|
49
|
+
return {
|
|
50
|
+
valid: violations.length === 0,
|
|
51
|
+
taskMode,
|
|
52
|
+
violations,
|
|
53
|
+
allowedOperations: [
|
|
54
|
+
'process/log/window/port/session inspection',
|
|
55
|
+
'read-only filesystem listing/reading',
|
|
56
|
+
'status probes and keep-running handle reporting',
|
|
57
|
+
'diagnostic summaries without source edits, commits, checkpoints, pushes, deploys, resets, rebases, or destructive cleanups',
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
12
61
|
|
|
13
62
|
export interface MeshWorkQueueEntry {
|
|
14
63
|
id: string;
|
|
15
64
|
meshId: string;
|
|
16
65
|
message: string;
|
|
17
66
|
status: MeshTaskStatus;
|
|
67
|
+
taskMode?: MeshTaskMode;
|
|
18
68
|
/** If specified, only this node can claim the task (used by legacy mesh_send_task) */
|
|
19
69
|
targetNodeId?: string;
|
|
20
70
|
/** If specified, only this runtime session can claim the task */
|
|
21
71
|
targetSessionId?: string;
|
|
72
|
+
/** If specified, a node must expose all tags before it can claim the task. */
|
|
73
|
+
requiredTags?: string[];
|
|
22
74
|
/** The node that actually claimed and is executing the task */
|
|
23
75
|
assignedNodeId?: string;
|
|
24
76
|
/** The session currently executing the task */
|
|
@@ -45,25 +97,63 @@ export interface MeshWorkQueueEntry {
|
|
|
45
97
|
updatedAt: string;
|
|
46
98
|
}
|
|
47
99
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
100
|
+
export interface MeshQueueMutationOptions {
|
|
101
|
+
ownerRole?: RepoMeshDaemonRole;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function normalizeMeshCapabilityTags(value: unknown): string[] {
|
|
105
|
+
if (!Array.isArray(value)) return [];
|
|
106
|
+
const seen = new Set<string>();
|
|
107
|
+
return value
|
|
108
|
+
.map(tag => typeof tag === 'string' ? tag.trim() : '')
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.filter(tag => {
|
|
111
|
+
if (seen.has(tag)) return false;
|
|
112
|
+
seen.add(tag);
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function firstProviderPriority(policy: unknown): string | undefined {
|
|
118
|
+
const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
119
|
+
? (policy as Record<string, unknown>).providerPriority
|
|
120
|
+
: undefined;
|
|
121
|
+
if (!Array.isArray(raw)) return undefined;
|
|
122
|
+
return raw.find(type => typeof type === 'string' && type.trim())?.trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildMeshNodeCapabilityTags(
|
|
126
|
+
node: { capabilities?: unknown; policy?: unknown } | undefined,
|
|
127
|
+
providerType?: string,
|
|
128
|
+
): string[] {
|
|
129
|
+
const provider = typeof providerType === 'string' && providerType.trim()
|
|
130
|
+
? providerType.trim()
|
|
131
|
+
: firstProviderPriority(node?.policy);
|
|
132
|
+
return normalizeMeshCapabilityTags([
|
|
133
|
+
...(Array.isArray(node?.capabilities) ? node.capabilities : []),
|
|
134
|
+
`os=${process.platform}`,
|
|
135
|
+
`arch=${process.arch}`,
|
|
136
|
+
...(provider ? [`provider=${provider}`] : []),
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function nodeSatisfiesRequiredTags(requiredTags: unknown, capabilityTags: unknown): boolean {
|
|
141
|
+
const required = normalizeMeshCapabilityTags(requiredTags);
|
|
142
|
+
if (required.length === 0) return true;
|
|
143
|
+
const available = new Set(normalizeMeshCapabilityTags(capabilityTags));
|
|
144
|
+
return required.every(tag => available.has(tag));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function withQueueLock<T>(_meshId: string, fn: () => T): T {
|
|
148
|
+
return BeadsDB.getInstance().transaction(fn);
|
|
51
149
|
}
|
|
52
150
|
|
|
53
151
|
function readQueue(meshId: string): MeshWorkQueueEntry[] {
|
|
54
|
-
|
|
55
|
-
if (!existsSync(path)) return [];
|
|
56
|
-
try {
|
|
57
|
-
const content = readFileSync(path, 'utf-8');
|
|
58
|
-
return JSON.parse(content) as MeshWorkQueueEntry[];
|
|
59
|
-
} catch {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
152
|
+
return BeadsDB.getInstance().getQueueEntries(meshId);
|
|
62
153
|
}
|
|
63
154
|
|
|
64
155
|
function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
65
|
-
|
|
66
|
-
writeFileSync(path, JSON.stringify(queue, null, 2), 'utf-8');
|
|
156
|
+
BeadsDB.getInstance().replaceQueue(meshId, queue);
|
|
67
157
|
}
|
|
68
158
|
|
|
69
159
|
/**
|
|
@@ -72,21 +162,26 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
|
72
162
|
export function enqueueTask(
|
|
73
163
|
meshId: string,
|
|
74
164
|
message: string,
|
|
75
|
-
opts?: { targetNodeId?: string; targetSessionId?: string }
|
|
165
|
+
opts?: { targetNodeId?: string; targetSessionId?: string; taskMode?: MeshTaskMode | string; requiredTags?: string[] } & MeshQueueMutationOptions,
|
|
76
166
|
): MeshWorkQueueEntry {
|
|
77
|
-
|
|
167
|
+
requireMeshHostQueueOwner(opts);
|
|
168
|
+
const modeValidation = validateMeshTaskModeRequest(opts?.taskMode, message);
|
|
169
|
+
if (!modeValidation.valid) {
|
|
170
|
+
throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
|
|
171
|
+
}
|
|
78
172
|
const entry: MeshWorkQueueEntry = {
|
|
79
173
|
id: randomUUID(),
|
|
80
174
|
meshId,
|
|
81
175
|
message,
|
|
82
176
|
status: 'pending',
|
|
177
|
+
taskMode: modeValidation.taskMode,
|
|
83
178
|
targetNodeId: opts?.targetNodeId,
|
|
84
179
|
targetSessionId: opts?.targetSessionId,
|
|
180
|
+
requiredTags: normalizeMeshCapabilityTags(opts?.requiredTags),
|
|
85
181
|
createdAt: new Date().toISOString(),
|
|
86
182
|
updatedAt: new Date().toISOString(),
|
|
87
183
|
};
|
|
88
|
-
|
|
89
|
-
writeQueue(meshId, queue);
|
|
184
|
+
BeadsDB.getInstance().insertQueueEntry(entry);
|
|
90
185
|
return entry;
|
|
91
186
|
}
|
|
92
187
|
|
|
@@ -94,51 +189,18 @@ export function enqueueTask(
|
|
|
94
189
|
* Get all tasks in the queue, optionally filtered by status.
|
|
95
190
|
*/
|
|
96
191
|
export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }): MeshWorkQueueEntry[] {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return queue;
|
|
192
|
+
return BeadsDB.getInstance().getQueueEntries(meshId, opts?.status?.length ? opts.status : undefined);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getMeshQueueRevision(meshId: string): string {
|
|
196
|
+
return BeadsDB.getInstance().getQueueRevision(meshId);
|
|
103
197
|
}
|
|
104
198
|
|
|
105
199
|
/**
|
|
106
200
|
* Find the next pending task that this node is allowed to claim, and mark it as assigned.
|
|
107
201
|
*/
|
|
108
|
-
export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// A worker must finish or fail its current queued assignment before it can
|
|
112
|
-
// claim another one. maxParallelTasks limits total mesh concurrency; it is
|
|
113
|
-
// not permission for one node/session to accumulate multiple assigned items.
|
|
114
|
-
const hasActiveAssignment = queue.some(q => q.status === 'assigned' && (
|
|
115
|
-
q.assignedSessionId === sessionId || q.assignedNodeId === nodeId
|
|
116
|
-
));
|
|
117
|
-
if (hasActiveAssignment) return null;
|
|
118
|
-
|
|
119
|
-
// Find highest priority task:
|
|
120
|
-
// 1. Pending tasks explicitly targeted at this runtime session
|
|
121
|
-
// 2. Pending tasks explicitly targeted at this node (but not another session)
|
|
122
|
-
// 3. Pending tasks with no target node/session
|
|
123
|
-
let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetSessionId === sessionId);
|
|
124
|
-
if (targetIdx === -1) {
|
|
125
|
-
targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId && !q.targetSessionId);
|
|
126
|
-
}
|
|
127
|
-
if (targetIdx === -1) {
|
|
128
|
-
targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId && !q.targetSessionId);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (targetIdx === -1) return null;
|
|
132
|
-
|
|
133
|
-
const entry = queue[targetIdx];
|
|
134
|
-
entry.status = 'assigned';
|
|
135
|
-
entry.assignedNodeId = nodeId;
|
|
136
|
-
entry.assignedSessionId = sessionId;
|
|
137
|
-
entry.dispatchTimestamp = new Date().toISOString();
|
|
138
|
-
entry.updatedAt = new Date().toISOString();
|
|
139
|
-
|
|
140
|
-
writeQueue(meshId, queue);
|
|
141
|
-
return entry;
|
|
202
|
+
export function claimNextTask(meshId: string, nodeId: string, sessionId: string, capabilityTags?: string[]): MeshWorkQueueEntry | null {
|
|
203
|
+
return BeadsDB.getInstance().claimNextQueueTask(meshId, nodeId, sessionId, capabilityTags);
|
|
142
204
|
}
|
|
143
205
|
|
|
144
206
|
/**
|
|
@@ -149,15 +211,16 @@ export function updateTaskStatus(
|
|
|
149
211
|
meshId: string,
|
|
150
212
|
taskId: string,
|
|
151
213
|
status: MeshTaskStatus,
|
|
214
|
+
opts?: MeshQueueMutationOptions,
|
|
152
215
|
): MeshWorkQueueEntry | null {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
216
|
+
requireMeshHostQueueOwner(opts);
|
|
217
|
+
return withQueueLock(meshId, () => {
|
|
218
|
+
const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
|
|
219
|
+
if (!entry) return null;
|
|
220
|
+
entry.status = status;
|
|
221
|
+
BeadsDB.getInstance().updateQueueEntry(entry);
|
|
222
|
+
return entry;
|
|
223
|
+
});
|
|
161
224
|
}
|
|
162
225
|
|
|
163
226
|
export function recordTaskAutoLaunch(
|
|
@@ -165,17 +228,14 @@ export function recordTaskAutoLaunch(
|
|
|
165
228
|
taskId: string,
|
|
166
229
|
autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
|
|
167
230
|
): MeshWorkQueueEntry | null {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
};
|
|
176
|
-
queue[idx].updatedAt = now;
|
|
177
|
-
writeQueue(meshId, queue);
|
|
178
|
-
return queue[idx];
|
|
231
|
+
return withQueueLock(meshId, () => {
|
|
232
|
+
const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
|
|
233
|
+
if (!entry) return null;
|
|
234
|
+
const now = new Date().toISOString();
|
|
235
|
+
entry.autoLaunch = { ...autoLaunch, updatedAt: now };
|
|
236
|
+
BeadsDB.getInstance().updateQueueEntry(entry);
|
|
237
|
+
return entry;
|
|
238
|
+
});
|
|
179
239
|
}
|
|
180
240
|
|
|
181
241
|
/**
|
|
@@ -184,19 +244,19 @@ export function recordTaskAutoLaunch(
|
|
|
184
244
|
export function cancelTask(
|
|
185
245
|
meshId: string,
|
|
186
246
|
taskId: string,
|
|
187
|
-
opts?: { reason?: string },
|
|
247
|
+
opts?: { reason?: string } & MeshQueueMutationOptions,
|
|
188
248
|
): MeshWorkQueueEntry | null {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
249
|
+
requireMeshHostQueueOwner(opts);
|
|
250
|
+
return withQueueLock(meshId, () => {
|
|
251
|
+
const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
|
|
252
|
+
if (!entry) return null;
|
|
253
|
+
const now = new Date().toISOString();
|
|
254
|
+
entry.status = 'cancelled';
|
|
255
|
+
entry.cancelledAt = now;
|
|
256
|
+
if (opts?.reason) entry.cancelReason = opts.reason;
|
|
257
|
+
BeadsDB.getInstance().updateQueueEntry(entry);
|
|
258
|
+
return entry;
|
|
259
|
+
});
|
|
200
260
|
}
|
|
201
261
|
|
|
202
262
|
/**
|
|
@@ -212,29 +272,27 @@ export function requeueTask(
|
|
|
212
272
|
targetSessionId?: string;
|
|
213
273
|
clearTargetNode?: boolean;
|
|
214
274
|
clearTargetSession?: boolean;
|
|
215
|
-
},
|
|
275
|
+
} & MeshQueueMutationOptions,
|
|
216
276
|
): MeshWorkQueueEntry | null {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
writeQueue(meshId, queue);
|
|
237
|
-
return entry;
|
|
277
|
+
requireMeshHostQueueOwner(opts);
|
|
278
|
+
return withQueueLock(meshId, () => {
|
|
279
|
+
const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
|
|
280
|
+
if (!entry) return null;
|
|
281
|
+
entry.status = 'pending';
|
|
282
|
+
delete entry.assignedNodeId;
|
|
283
|
+
delete entry.assignedSessionId;
|
|
284
|
+
delete entry.cancelledAt;
|
|
285
|
+
delete entry.cancelReason;
|
|
286
|
+
if (opts?.clearTargetNode) delete entry.targetNodeId;
|
|
287
|
+
if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
|
|
288
|
+
if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
|
|
289
|
+
if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
|
|
290
|
+
entry.requeuedAt = new Date().toISOString();
|
|
291
|
+
entry.requeueCount = (entry.requeueCount || 0) + 1;
|
|
292
|
+
if (opts?.reason) entry.requeueReason = opts.reason;
|
|
293
|
+
BeadsDB.getInstance().updateQueueEntry(entry);
|
|
294
|
+
return entry;
|
|
295
|
+
});
|
|
238
296
|
}
|
|
239
297
|
|
|
240
298
|
/**
|
|
@@ -244,29 +302,16 @@ export function updateSessionTaskStatus(
|
|
|
244
302
|
meshId: string,
|
|
245
303
|
sessionId: string,
|
|
246
304
|
status: MeshTaskStatus,
|
|
305
|
+
opts?: { occurredAt?: string },
|
|
247
306
|
): MeshWorkQueueEntry | null {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (queue[i].assignedSessionId === sessionId && queue[i].status === 'assigned') {
|
|
257
|
-
const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
|
|
258
|
-
if (time > bestTime) {
|
|
259
|
-
bestTime = time;
|
|
260
|
-
bestIdx = i;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (bestIdx === -1) return null;
|
|
265
|
-
|
|
266
|
-
queue[bestIdx].status = status;
|
|
267
|
-
queue[bestIdx].updatedAt = new Date().toISOString();
|
|
268
|
-
writeQueue(meshId, queue);
|
|
269
|
-
return queue[bestIdx];
|
|
307
|
+
return withQueueLock(meshId, () => {
|
|
308
|
+
const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
|
|
309
|
+
const entry = BeadsDB.getInstance().findAssignedBySession(meshId, sessionId, occurredAtIso);
|
|
310
|
+
if (!entry) return null;
|
|
311
|
+
entry.status = status;
|
|
312
|
+
BeadsDB.getInstance().updateQueueEntry(entry);
|
|
313
|
+
return entry;
|
|
314
|
+
});
|
|
270
315
|
}
|
|
271
316
|
|
|
272
317
|
export interface MeshWorkQueueStats {
|
|
@@ -294,14 +339,16 @@ export interface MeshWorkQueueStats {
|
|
|
294
339
|
* Return aggregate queue statistics for the given mesh.
|
|
295
340
|
*/
|
|
296
341
|
export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
const
|
|
342
|
+
const rows = BeadsDB.getInstance().getQueueStatsByStatus(meshId);
|
|
343
|
+
const counts: Record<string, number> = {};
|
|
344
|
+
for (const r of rows) counts[r.status] = r.count;
|
|
345
|
+
const pending = counts['pending'] ?? 0;
|
|
346
|
+
const assigned = counts['assigned'] ?? 0;
|
|
347
|
+
const completed = counts['completed'] ?? 0;
|
|
348
|
+
const failed = counts['failed'] ?? 0;
|
|
349
|
+
const cancelled = counts['cancelled'] ?? 0;
|
|
303
350
|
return {
|
|
304
|
-
total:
|
|
351
|
+
total: pending + assigned + completed + failed + cancelled,
|
|
305
352
|
active: pending + assigned,
|
|
306
353
|
historical: completed + failed + cancelled,
|
|
307
354
|
pending,
|
|
@@ -309,22 +356,83 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
|
|
|
309
356
|
completed,
|
|
310
357
|
failed,
|
|
311
358
|
cancelled,
|
|
312
|
-
activeCounts: {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
},
|
|
316
|
-
historicalCounts: {
|
|
317
|
-
completed,
|
|
318
|
-
failed,
|
|
319
|
-
cancelled,
|
|
320
|
-
},
|
|
321
|
-
activeAssignments: queue
|
|
322
|
-
.filter(q => q.status === 'assigned')
|
|
323
|
-
.map(q => ({
|
|
324
|
-
id: q.id,
|
|
325
|
-
nodeId: q.assignedNodeId,
|
|
326
|
-
sessionId: q.assignedSessionId,
|
|
327
|
-
message: q.message,
|
|
328
|
-
})),
|
|
359
|
+
activeCounts: { pending, assigned },
|
|
360
|
+
historicalCounts: { completed, failed, cancelled },
|
|
361
|
+
activeAssignments: BeadsDB.getInstance().getActiveAssignmentDetails(meshId),
|
|
329
362
|
};
|
|
330
363
|
}
|
|
364
|
+
|
|
365
|
+
export function __replaceMeshQueueForTests(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
366
|
+
BeadsDB.getInstance().transaction(() => {
|
|
367
|
+
BeadsDB.getInstance().replaceQueue(meshId, queue);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function __clearMeshQueueForTests(meshId: string): void {
|
|
372
|
+
BeadsDB.getInstance().deleteQueue(meshId);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function __clearDirectDispatchesForTests(meshId: string): void {
|
|
376
|
+
BeadsDB.getInstance().deleteDirectDispatches(meshId);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function __resetBeadsDBForTests(): void {
|
|
380
|
+
BeadsDB.resetForTests();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Direct Dispatch Tracking ─────────────────────────────────────────────────
|
|
384
|
+
// Persists direct (non-queue) task dispatches so buildMeshActiveWork can read
|
|
385
|
+
// active work from BeadsDB instead of scanning ledger JSONL entries.
|
|
386
|
+
|
|
387
|
+
export type DirectDispatchRecord = ReturnType<BeadsDB['getActiveDirectDispatches']>[number];
|
|
388
|
+
|
|
389
|
+
export function insertDirectDispatch(
|
|
390
|
+
meshId: string,
|
|
391
|
+
data: {
|
|
392
|
+
taskId: string;
|
|
393
|
+
nodeId?: string;
|
|
394
|
+
sessionId?: string;
|
|
395
|
+
providerType?: string;
|
|
396
|
+
message: string;
|
|
397
|
+
taskMode?: string;
|
|
398
|
+
via: string;
|
|
399
|
+
dispatchedToIdleSession?: boolean;
|
|
400
|
+
dispatchedAt: string;
|
|
401
|
+
},
|
|
402
|
+
): void {
|
|
403
|
+
try {
|
|
404
|
+
BeadsDB.getInstance().insertDirectDispatch({ ...data, meshId });
|
|
405
|
+
} catch (e: any) {
|
|
406
|
+
process.stderr.write(`[adhdev-mesh] insertDirectDispatch failed for task ${data.taskId}: ${e?.message || e}\n`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[] {
|
|
411
|
+
try {
|
|
412
|
+
return BeadsDB.getInstance().getActiveDirectDispatches(meshId);
|
|
413
|
+
} catch {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function updateDirectDispatchStatus(
|
|
419
|
+
meshId: string,
|
|
420
|
+
sessionId: string,
|
|
421
|
+
status: 'acked' | 'completed' | 'failed' | 'stale',
|
|
422
|
+
): void {
|
|
423
|
+
try {
|
|
424
|
+
BeadsDB.getInstance().updateDirectDispatchStatus(meshId, sessionId, status);
|
|
425
|
+
} catch { /* best-effort */ }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function cleanupTerminalDirectDispatches(olderThanMs = 7 * 24 * 60 * 60_000): void {
|
|
429
|
+
try {
|
|
430
|
+
BeadsDB.getInstance().cleanupTerminalDirectDispatches(olderThanMs);
|
|
431
|
+
} catch { /* best-effort */ }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function markStaleDirectDispatches(meshId: string, olderThanMs = 60 * 60_000): void {
|
|
435
|
+
try {
|
|
436
|
+
BeadsDB.getInstance().markStaleDirectDispatches(meshId, olderThanMs);
|
|
437
|
+
} catch { /* best-effort */ }
|
|
438
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export type PreviewFreshnessStatus = 'fresh' | 'stale' | 'unknown' | 'not_configured';
|
|
6
|
+
|
|
7
|
+
export interface PreviewFreshness {
|
|
8
|
+
status: PreviewFreshnessStatus;
|
|
9
|
+
lastPreviewCommit: string | null;
|
|
10
|
+
currentMainCommit: string | null;
|
|
11
|
+
currentMainCommitSource: 'origin/main' | 'HEAD' | 'unknown';
|
|
12
|
+
recordPath: string;
|
|
13
|
+
lastDeployedAt?: string;
|
|
14
|
+
lastTarget?: string;
|
|
15
|
+
previewVersion?: string;
|
|
16
|
+
targets: Record<'npm' | 'server' | 'web', {
|
|
17
|
+
commit: string | null;
|
|
18
|
+
deployedAt?: string;
|
|
19
|
+
status: PreviewFreshnessStatus;
|
|
20
|
+
}>;
|
|
21
|
+
nextAction: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PREVIEW_DEPLOY_RECORD = '.adhdev/preview-deploy.json';
|
|
25
|
+
|
|
26
|
+
function runGit(repoRoot: string, args: readonly string[]): string {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync('git', args, {
|
|
29
|
+
cwd: repoRoot,
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
|
+
timeout: 5000,
|
|
33
|
+
}).trim();
|
|
34
|
+
} catch {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readRecord(repoRoot: string): Record<string, unknown> | null {
|
|
40
|
+
const path = resolve(repoRoot, PREVIEW_DEPLOY_RECORD);
|
|
41
|
+
if (!existsSync(path)) return null;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
44
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
45
|
+
? parsed as Record<string, unknown>
|
|
46
|
+
: null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeCommit(value: unknown): string | null {
|
|
53
|
+
return typeof value === 'string' && /^[0-9a-f]{7,40}$/i.test(value.trim())
|
|
54
|
+
? value.trim()
|
|
55
|
+
: null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readTargetFreshness(record: Record<string, unknown> | null, currentCommit: string | null): PreviewFreshness['targets'] {
|
|
59
|
+
const targets = record?.targets && typeof record.targets === 'object' && !Array.isArray(record.targets)
|
|
60
|
+
? record.targets as Record<string, unknown>
|
|
61
|
+
: {};
|
|
62
|
+
const result = {} as PreviewFreshness['targets'];
|
|
63
|
+
for (const targetName of ['npm', 'server', 'web'] as const) {
|
|
64
|
+
const targetRecord = targets[targetName] && typeof targets[targetName] === 'object' && !Array.isArray(targets[targetName])
|
|
65
|
+
? targets[targetName] as Record<string, unknown>
|
|
66
|
+
: {};
|
|
67
|
+
const commit = normalizeCommit(targetRecord.commit);
|
|
68
|
+
result[targetName] = {
|
|
69
|
+
commit,
|
|
70
|
+
deployedAt: typeof targetRecord.deployedAt === 'string' ? targetRecord.deployedAt : undefined,
|
|
71
|
+
status: commit && currentCommit ? (commit === currentCommit ? 'fresh' : 'stale') : 'unknown',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readCurrentMainCommit(repoRoot: string): Pick<PreviewFreshness, 'currentMainCommit' | 'currentMainCommitSource'> {
|
|
78
|
+
const originMain = runGit(repoRoot, ['rev-parse', '--verify', 'origin/main^{commit}']);
|
|
79
|
+
if (originMain) {
|
|
80
|
+
return { currentMainCommit: originMain, currentMainCommitSource: 'origin/main' };
|
|
81
|
+
}
|
|
82
|
+
const head = runGit(repoRoot, ['rev-parse', '--verify', 'HEAD']);
|
|
83
|
+
if (head) {
|
|
84
|
+
return { currentMainCommit: head, currentMainCommitSource: 'HEAD' };
|
|
85
|
+
}
|
|
86
|
+
return { currentMainCommit: null, currentMainCommitSource: 'unknown' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildPreviewFreshness(repoRoot: string): PreviewFreshness {
|
|
90
|
+
const current = readCurrentMainCommit(repoRoot);
|
|
91
|
+
const record = readRecord(repoRoot);
|
|
92
|
+
const lastPreviewCommit = normalizeCommit(record?.lastPreviewCommit);
|
|
93
|
+
const targets = readTargetFreshness(record, current.currentMainCommit);
|
|
94
|
+
let status: PreviewFreshnessStatus = 'unknown';
|
|
95
|
+
let nextAction = 'Run npm run deploy:preview from the current main commit, then smoke preview.';
|
|
96
|
+
|
|
97
|
+
if (lastPreviewCommit && current.currentMainCommit) {
|
|
98
|
+
status = lastPreviewCommit === current.currentMainCommit ? 'fresh' : 'stale';
|
|
99
|
+
nextAction = status === 'fresh'
|
|
100
|
+
? 'No preview deploy action needed.'
|
|
101
|
+
: 'Run npm run deploy:preview from origin/main, then smoke preview.';
|
|
102
|
+
} else if (!current.currentMainCommit) {
|
|
103
|
+
nextAction = 'Resolve the current main commit before judging preview freshness.';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
status,
|
|
108
|
+
lastPreviewCommit,
|
|
109
|
+
currentMainCommit: current.currentMainCommit,
|
|
110
|
+
currentMainCommitSource: current.currentMainCommitSource,
|
|
111
|
+
recordPath: PREVIEW_DEPLOY_RECORD,
|
|
112
|
+
lastDeployedAt: typeof record?.updatedAt === 'string' ? record.updatedAt : undefined,
|
|
113
|
+
lastTarget: typeof record?.target === 'string' ? record.target : undefined,
|
|
114
|
+
previewVersion: typeof record?.previewVersion === 'string' ? record.previewVersion : undefined,
|
|
115
|
+
targets,
|
|
116
|
+
nextAction,
|
|
117
|
+
};
|
|
118
|
+
}
|