@flame0510/project-aether 1.1.9
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/README.md +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildAgentImage.ts
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for obtaining the openclaw-agent-base Docker image.
|
|
5
|
+
* Used by /api/agents/download-image, /api/agents/image-status, and
|
|
6
|
+
* the rev4a serve CLI.
|
|
7
|
+
*
|
|
8
|
+
* Strategy (in order):
|
|
9
|
+
* 1. docker pull from ghcr.io/flame0510/rev4a/openclaw-agent-base:latest (public, no auth)
|
|
10
|
+
* 2. If pull fails (offline / no network) → fallback to local docker build
|
|
11
|
+
*
|
|
12
|
+
* Logs are written to /tmp/rev4a-download-<timestamp>.log so they survive
|
|
13
|
+
* page refreshes and modal closes.
|
|
14
|
+
*/
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { execSync as execSyncRaw } from 'child_process';
|
|
19
|
+
|
|
20
|
+
const REGISTRY_IMAGE = 'ghcr.io/flame0510/rev4a/openclaw-agent-base:latest';
|
|
21
|
+
const LOCAL_IMAGE = 'openclaw-agent-base:latest';
|
|
22
|
+
|
|
23
|
+
let isDownloading = false;
|
|
24
|
+
let downloadLogPath: string | null = null;
|
|
25
|
+
let downloadChildProcess: ReturnType<(typeof import('child_process'))['exec']> | null = null;
|
|
26
|
+
|
|
27
|
+
/** Check whether the docker CLI is available and functional. */
|
|
28
|
+
function dockerAvailable(): boolean {
|
|
29
|
+
try {
|
|
30
|
+
execSyncRaw('docker version --format "{{.Client.Version}}" 2>/dev/null', {
|
|
31
|
+
encoding: 'utf-8',
|
|
32
|
+
timeout: 3000,
|
|
33
|
+
});
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Returns the SHA256 hash of the current Dockerfile. */
|
|
41
|
+
export function getDockerfileHash(): string | null {
|
|
42
|
+
try {
|
|
43
|
+
const dockerfile = path.join(process.cwd(), 'agent-templates', 'base-image', 'Dockerfile');
|
|
44
|
+
if (!fs.existsSync(dockerfile)) return null;
|
|
45
|
+
const content = fs.readFileSync(dockerfile, 'utf-8');
|
|
46
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
/** Checks if a download is already running (in-process flag only). */
|
|
54
|
+
export function getIsDownloading(): boolean {
|
|
55
|
+
return isDownloading;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Acquire the lock atomically. Returns false if already locked. */
|
|
59
|
+
function tryAcquireLock(): boolean {
|
|
60
|
+
if (getIsDownloading()) return false;
|
|
61
|
+
isDownloading = true;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Release the lock and clear state. */
|
|
66
|
+
function releaseLock(): void {
|
|
67
|
+
isDownloading = false;
|
|
68
|
+
downloadLogPath = null;
|
|
69
|
+
downloadChildProcess = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
function appendLog(msg: string): void {
|
|
74
|
+
if (!downloadLogPath) return;
|
|
75
|
+
try {
|
|
76
|
+
fs.appendFileSync(downloadLogPath, msg + '\n', 'utf-8');
|
|
77
|
+
} catch { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Remove download log files older than 24 hours. */
|
|
81
|
+
function cleanupOldLogs(): void {
|
|
82
|
+
try {
|
|
83
|
+
const tmpDir = '/tmp';
|
|
84
|
+
const files = fs.readdirSync(tmpDir);
|
|
85
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
86
|
+
for (const f of files) {
|
|
87
|
+
if (!f.startsWith('rev4a-download-') || !f.endsWith('.log')) continue;
|
|
88
|
+
const fullPath = path.join(tmpDir, f);
|
|
89
|
+
const stat = fs.statSync(fullPath);
|
|
90
|
+
if (stat.mtimeMs < cutoff) {
|
|
91
|
+
fs.unlinkSync(fullPath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch { /* ignore */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface DownloadOptions {
|
|
98
|
+
force?: boolean;
|
|
99
|
+
onEvent?: (data: string) => void;
|
|
100
|
+
signal?: AbortSignal;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function localImageExists(): boolean {
|
|
104
|
+
if (!dockerAvailable()) return false;
|
|
105
|
+
try {
|
|
106
|
+
execSyncRaw(
|
|
107
|
+
`docker image inspect ${LOCAL_IMAGE} --format "{{.ID}}" 2>/dev/null`,
|
|
108
|
+
{ timeout: 3000 },
|
|
109
|
+
);
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getLocalImageId(): string | null {
|
|
117
|
+
if (!dockerAvailable()) return null;
|
|
118
|
+
try {
|
|
119
|
+
return execSyncRaw(
|
|
120
|
+
`docker image inspect ${LOCAL_IMAGE} --format "{{.ID}}" 2>/dev/null`,
|
|
121
|
+
{ encoding: 'utf-8', timeout: 3000 },
|
|
122
|
+
).trim() || null;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get the digest of the remote image (without pulling).
|
|
130
|
+
* Returns null if not reachable.
|
|
131
|
+
*/
|
|
132
|
+
function getRemoteDigest(): string | null {
|
|
133
|
+
if (!dockerAvailable()) return null;
|
|
134
|
+
try {
|
|
135
|
+
const out = execSyncRaw(
|
|
136
|
+
`docker manifest inspect ${REGISTRY_IMAGE} 2>/dev/null`,
|
|
137
|
+
{ encoding: 'utf-8', timeout: 15000 },
|
|
138
|
+
);
|
|
139
|
+
const manifest = JSON.parse(out);
|
|
140
|
+
return manifest?.config?.digest || null;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Pull the image from the public registry and tag it locally.
|
|
148
|
+
*/
|
|
149
|
+
async function pullFromRegistry(
|
|
150
|
+
opts: DownloadOptions,
|
|
151
|
+
emit: (data: string) => void,
|
|
152
|
+
): Promise<boolean> {
|
|
153
|
+
if (!dockerAvailable()) {
|
|
154
|
+
emit(JSON.stringify({ type: 'error', message: 'Docker is not available' }));
|
|
155
|
+
appendLog('Docker not available — cannot download image');
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
emit(JSON.stringify({ type: 'status', message: `Downloading ${REGISTRY_IMAGE}...` }));
|
|
160
|
+
appendLog(`Downloading from registry: ${REGISTRY_IMAGE}`);
|
|
161
|
+
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
const { exec } = require('child_process');
|
|
164
|
+
const child = exec(
|
|
165
|
+
`docker pull ${REGISTRY_IMAGE}`,
|
|
166
|
+
{ maxBuffer: 1024 * 1024 * 5, timeout: 300_000 },
|
|
167
|
+
(err: Error | null) => {
|
|
168
|
+
if (err) {
|
|
169
|
+
appendLog(`Download from registry failed: ${err.message}`);
|
|
170
|
+
emit(JSON.stringify({ type: 'log', message: 'Registry download failed, trying local build...' }));
|
|
171
|
+
resolve(false);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
execSyncRaw(`docker tag ${REGISTRY_IMAGE} ${LOCAL_IMAGE}`, { timeout: 10_000 });
|
|
177
|
+
} catch { /* ignore tag error */ }
|
|
178
|
+
appendLog('Download completed successfully');
|
|
179
|
+
emit(JSON.stringify({ type: 'complete', message: 'Image downloaded from registry' }));
|
|
180
|
+
resolve(true);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
downloadChildProcess = child;
|
|
185
|
+
|
|
186
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
187
|
+
appendLog(chunk.toString());
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
191
|
+
appendLog(chunk.toString());
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
child.on('error', (err: Error) => {
|
|
195
|
+
appendLog(`Pull process error: ${err.message}`);
|
|
196
|
+
releaseLock();
|
|
197
|
+
resolve(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Local docker build fallback. Returns a Promise.
|
|
204
|
+
*/
|
|
205
|
+
function localBuild(
|
|
206
|
+
opts: DownloadOptions,
|
|
207
|
+
emit: (data: string) => void,
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const dockerfileDir = path.join(process.cwd(), 'agent-templates', 'base-image');
|
|
211
|
+
const dockerfile = path.join(dockerfileDir, 'Dockerfile');
|
|
212
|
+
|
|
213
|
+
if (!fs.existsSync(dockerfile)) {
|
|
214
|
+
const msg = 'Dockerfile not found — cannot build locally';
|
|
215
|
+
emit(JSON.stringify({ type: 'error', message: msg }));
|
|
216
|
+
appendLog(msg);
|
|
217
|
+
releaseLock();
|
|
218
|
+
reject(new Error(msg));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const hash = getDockerfileHash();
|
|
223
|
+
const noCache = opts.force ? '--no-cache ' : '';
|
|
224
|
+
const cmd = hash
|
|
225
|
+
? `docker build ${noCache}-t ${LOCAL_IMAGE} --label "rev4a.dockerfile_hash=${hash}" --progress=plain .`
|
|
226
|
+
: `docker build ${noCache}-t ${LOCAL_IMAGE} --progress=plain .`;
|
|
227
|
+
|
|
228
|
+
appendLog('');
|
|
229
|
+
appendLog('=== Local build started (registry download failed) ===');
|
|
230
|
+
appendLog(`Dockerfile hash: ${hash || 'unknown'}`);
|
|
231
|
+
appendLog('');
|
|
232
|
+
|
|
233
|
+
emit(JSON.stringify({ type: 'status', message: 'Building agent base image locally...' }));
|
|
234
|
+
|
|
235
|
+
const { exec } = require('child_process');
|
|
236
|
+
const child = exec(cmd, {
|
|
237
|
+
cwd: dockerfileDir,
|
|
238
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
downloadChildProcess = child;
|
|
242
|
+
|
|
243
|
+
child.stdout?.on('data', (chunk: string) => {
|
|
244
|
+
const lines = chunk.toString().split('\n').filter(Boolean);
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
appendLog(line);
|
|
247
|
+
if (line.startsWith('#')) {
|
|
248
|
+
emit(JSON.stringify({ type: 'step', message: line }));
|
|
249
|
+
} else if (line.includes('Downloading') || line.includes('Extracting') || line.includes('Pulling')) {
|
|
250
|
+
emit(JSON.stringify({ type: 'progress', message: line }));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
child.stderr?.on('data', (chunk: string) => {
|
|
256
|
+
const text = chunk.toString().trim();
|
|
257
|
+
if (text) {
|
|
258
|
+
appendLog(text);
|
|
259
|
+
emit(JSON.stringify({ type: 'log', message: text }));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
child.on('close', (code: number) => {
|
|
264
|
+
if (code === 0) {
|
|
265
|
+
appendLog('=== Build completed successfully ===');
|
|
266
|
+
emit(JSON.stringify({ type: 'complete', message: 'Agent base image built successfully' }));
|
|
267
|
+
releaseLock();
|
|
268
|
+
resolve();
|
|
269
|
+
} else {
|
|
270
|
+
appendLog(`=== Build failed with exit code ${code} ===`);
|
|
271
|
+
releaseLock();
|
|
272
|
+
reject(new Error(`docker build exited with code ${code}`));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
child.on('error', (err: Error) => {
|
|
277
|
+
appendLog(`=== Build error: ${err.message} ===`);
|
|
278
|
+
releaseLock();
|
|
279
|
+
reject(err);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Main entry point: try pull first, fallback to local build.
|
|
286
|
+
*/
|
|
287
|
+
export async function downloadAgentImage(opts: DownloadOptions): Promise<void> {
|
|
288
|
+
const { force = false, onEvent, signal } = opts;
|
|
289
|
+
|
|
290
|
+
if (!dockerAvailable()) {
|
|
291
|
+
const msg = 'Docker is not available — please install Docker and try again';
|
|
292
|
+
onEvent?.(JSON.stringify({ type: 'error', message: msg }));
|
|
293
|
+
throw new Error(msg);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!tryAcquireLock()) {
|
|
297
|
+
throw new Error('A download is already in progress');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const timestamp = Date.now();
|
|
301
|
+
downloadLogPath = `/tmp/rev4a-download-${timestamp}.log`;
|
|
302
|
+
|
|
303
|
+
const emit = (data: string) => {
|
|
304
|
+
onEvent?.(data);
|
|
305
|
+
try {
|
|
306
|
+
const parsed = JSON.parse(data);
|
|
307
|
+
if (parsed.message) appendLog(parsed.message);
|
|
308
|
+
} catch { /* not JSON */ }
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// If not forced and image already exists, check if update is needed
|
|
312
|
+
if (!force && localImageExists()) {
|
|
313
|
+
const remoteDigest = getRemoteDigest();
|
|
314
|
+
if (!remoteDigest) {
|
|
315
|
+
emit(JSON.stringify({ type: 'complete', message: 'Image already downloaded (offline, cannot check for updates)' }));
|
|
316
|
+
releaseLock();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
emit(JSON.stringify({ type: 'complete', message: 'Image already downloaded' }));
|
|
321
|
+
releaseLock();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Force: remove local image first so we get a fresh pull
|
|
326
|
+
if (force) {
|
|
327
|
+
try {
|
|
328
|
+
execSyncRaw(`docker rmi -f ${LOCAL_IMAGE} ${REGISTRY_IMAGE} 2>/dev/null || true`, { timeout: 5000 });
|
|
329
|
+
} catch { /* ok */ }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
appendLog(opts.force ? '=== Image refresh started ===' : '=== Image download started ===');
|
|
333
|
+
|
|
334
|
+
// Try download from registry first
|
|
335
|
+
const downloaded = await pullFromRegistry(opts, emit);
|
|
336
|
+
|
|
337
|
+
if (downloaded) {
|
|
338
|
+
cleanupOldLogs();
|
|
339
|
+
releaseLock();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Fallback: local build
|
|
344
|
+
await localBuild(opts, emit);
|
|
345
|
+
cleanupOldLogs();
|
|
346
|
+
}
|