amalgm 0.1.51 → 0.1.53
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/lib/tunnel-events.js +48 -23
- package/package.json +2 -2
- package/runtime/lib/harnesses.js +12 -4
- package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
- package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
- package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
- package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
- package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
- package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
- package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
- package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
- package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
- package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
- package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +547 -0
- package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
- package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
- package/runtime/scripts/amalgm-mcp/config.js +33 -48
- package/runtime/scripts/amalgm-mcp/deps.js +1 -31
- package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
- package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
- package/runtime/scripts/amalgm-mcp/index.js +12 -14
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
- package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
- package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
- package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
- package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
- package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
- package/runtime/scripts/chat-core/adapters/claude.js +2 -1
- package/runtime/scripts/chat-core/auth.js +82 -12
- package/runtime/scripts/chat-core/contract.js +5 -1
- package/runtime/scripts/chat-core/engine.js +103 -62
- package/runtime/scripts/chat-core/event-schema.js +8 -0
- package/runtime/scripts/chat-core/events.js +5 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
- package/runtime/scripts/chat-core/parts.js +21 -6
- package/runtime/scripts/chat-core/sse.js +3 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
- package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
- package/runtime/scripts/chat-core/tool-shape.js +4 -4
- package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
- package/runtime/scripts/chat-core/tooling/native-binaries.js +34 -9
- package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
- package/runtime/scripts/local-gateway.js +34 -27
- package/runtime/scripts/platform-context.txt +76 -94
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
- package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
- package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
- package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /artifacts/* REST routes for the Next.js API and local tunnel runtime.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const { getConnectedRoutes, loadArtifacts } = require('./store');
|
|
6
|
-
const {
|
|
7
|
-
deleteArtifact,
|
|
8
|
-
connectDns,
|
|
9
|
-
disconnectDns,
|
|
10
|
-
redeployArtifact,
|
|
11
|
-
registerArtifact,
|
|
12
|
-
startArtifact,
|
|
13
|
-
stopArtifact,
|
|
14
|
-
} = require('./supervisor');
|
|
15
|
-
|
|
16
|
-
async function handleList(sendJson) {
|
|
17
|
-
sendJson(200, loadArtifacts());
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function handleRoutes(sendJson) {
|
|
21
|
-
sendJson(200, { routes: getConnectedRoutes() });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function handleRegister(body, sendJson) {
|
|
25
|
-
try {
|
|
26
|
-
const artifact = await registerArtifact(body || {});
|
|
27
|
-
sendJson(200, { artifact });
|
|
28
|
-
} catch (err) {
|
|
29
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function handleRedeploy(body, sendJson) {
|
|
34
|
-
try {
|
|
35
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
36
|
-
const artifact = await redeployArtifact(body.artifact_id, body);
|
|
37
|
-
sendJson(200, { artifact });
|
|
38
|
-
} catch (err) {
|
|
39
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function handleStart(body, sendJson) {
|
|
44
|
-
try {
|
|
45
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
46
|
-
const artifact = await startArtifact(body.artifact_id);
|
|
47
|
-
sendJson(200, { artifact });
|
|
48
|
-
} catch (err) {
|
|
49
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function handleStop(body, sendJson) {
|
|
54
|
-
try {
|
|
55
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
56
|
-
const artifact = await stopArtifact(body.artifact_id);
|
|
57
|
-
sendJson(200, { artifact });
|
|
58
|
-
} catch (err) {
|
|
59
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function handleDelete(body, sendJson) {
|
|
64
|
-
try {
|
|
65
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
66
|
-
const artifact = await deleteArtifact(body.artifact_id);
|
|
67
|
-
sendJson(200, { artifact });
|
|
68
|
-
} catch (err) {
|
|
69
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function handleConnectDns(body, sendJson) {
|
|
74
|
-
try {
|
|
75
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
76
|
-
const artifact = await connectDns(body.artifact_id);
|
|
77
|
-
sendJson(200, { artifact });
|
|
78
|
-
} catch (err) {
|
|
79
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function handleDisconnectDns(body, sendJson) {
|
|
84
|
-
try {
|
|
85
|
-
if (!body?.artifact_id) return sendJson(400, { error: 'artifact_id is required' });
|
|
86
|
-
const artifact = await disconnectDns(body.artifact_id);
|
|
87
|
-
sendJson(200, { artifact });
|
|
88
|
-
} catch (err) {
|
|
89
|
-
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
module.exports = {
|
|
94
|
-
handleConnectDns,
|
|
95
|
-
handleDelete,
|
|
96
|
-
handleDisconnectDns,
|
|
97
|
-
handleList,
|
|
98
|
-
handleRedeploy,
|
|
99
|
-
handleRegister,
|
|
100
|
-
handleRoutes,
|
|
101
|
-
handleStart,
|
|
102
|
-
handleStop,
|
|
103
|
-
};
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Artifact storage — ~/.amalgm/artifacts.json
|
|
3
|
-
*
|
|
4
|
-
* This is intentionally local-first. The laptop volume is the source of truth;
|
|
5
|
-
* the public gateway only gets ephemeral route advertisements while the
|
|
6
|
-
* computer is connected.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const crypto = require('crypto');
|
|
10
|
-
const {
|
|
11
|
-
ARTIFACTS_DIR,
|
|
12
|
-
ARTIFACTS_FILE,
|
|
13
|
-
ARTIFACTS_DOMAIN,
|
|
14
|
-
ARTIFACT_PORT_MIN,
|
|
15
|
-
ARTIFACT_PORT_MAX,
|
|
16
|
-
} = require('../config');
|
|
17
|
-
const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
|
|
18
|
-
const { appendStateEvent } = require('../state/events');
|
|
19
|
-
|
|
20
|
-
function ensureArtifactsDirs() {
|
|
21
|
-
ensureDir(ARTIFACTS_DIR);
|
|
22
|
-
if (!readJson(ARTIFACTS_FILE, null)) {
|
|
23
|
-
writeJsonAtomic(ARTIFACTS_FILE, { version: 1, artifacts: [] });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function publicUrlForRef(artifactRef) {
|
|
28
|
-
return `https://${artifactRef}.${ARTIFACTS_DOMAIN}/`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function genArtifactRef() {
|
|
32
|
-
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
33
|
-
const bytes = crypto.randomBytes(12);
|
|
34
|
-
let out = '';
|
|
35
|
-
for (let i = 0; i < 12; i += 1) {
|
|
36
|
-
out += chars[bytes[i] % chars.length];
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function normalizeArtifact(artifact) {
|
|
42
|
-
const artifactRef = artifact.artifactRef || artifact.artifact_ref || genArtifactRef();
|
|
43
|
-
const dnsConnected =
|
|
44
|
-
artifact.dnsConnected !== undefined
|
|
45
|
-
? artifact.dnsConnected !== false
|
|
46
|
-
: artifact.connected !== false;
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
...artifact,
|
|
50
|
-
kind: 'artifact',
|
|
51
|
-
artifactRef,
|
|
52
|
-
artifact_ref: artifactRef,
|
|
53
|
-
publicUrl: artifact.publicUrl || publicUrlForRef(artifactRef),
|
|
54
|
-
dnsConnected,
|
|
55
|
-
autostart: artifact.autostart !== false,
|
|
56
|
-
keepAlive: artifact.keepAlive !== false,
|
|
57
|
-
desiredState: artifact.desiredState || (artifact.status === 'stopped' ? 'stopped' : 'running'),
|
|
58
|
-
status: artifact.status || 'stopped',
|
|
59
|
-
pid: artifact.pid || null,
|
|
60
|
-
error: artifact.error || null,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function loadArtifacts() {
|
|
65
|
-
const data = readJson(ARTIFACTS_FILE, { version: 1, artifacts: [] });
|
|
66
|
-
const artifacts = Array.isArray(data.artifacts) ? data.artifacts.map(normalizeArtifact) : [];
|
|
67
|
-
return { version: 1, artifacts };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function publishArtifactsChange(data, source = 'artifacts') {
|
|
71
|
-
try {
|
|
72
|
-
appendStateEvent({
|
|
73
|
-
resource: 'artifacts',
|
|
74
|
-
op: 'replace',
|
|
75
|
-
value: Array.isArray(data?.artifacts) ? data.artifacts.map(normalizeArtifact) : [],
|
|
76
|
-
source,
|
|
77
|
-
});
|
|
78
|
-
} catch (error) {
|
|
79
|
-
console.warn('[Artifacts] Local Live Store publish failed:', error.message);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function saveArtifacts(data, options = {}) {
|
|
84
|
-
ensureDir(ARTIFACTS_DIR);
|
|
85
|
-
const normalizedData = {
|
|
86
|
-
version: 1,
|
|
87
|
-
artifacts: Array.isArray(data.artifacts) ? data.artifacts.map(normalizeArtifact) : [],
|
|
88
|
-
};
|
|
89
|
-
writeJsonAtomic(ARTIFACTS_FILE, normalizedData);
|
|
90
|
-
publishArtifactsChange(normalizedData, options.source || 'artifacts:save');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function updateArtifactMeta(artifactId, updates) {
|
|
94
|
-
const data = loadArtifacts();
|
|
95
|
-
const artifact = data.artifacts.find((item) => item.id === artifactId);
|
|
96
|
-
if (!artifact) return null;
|
|
97
|
-
Object.assign(artifact, updates, { updatedAt: new Date().toISOString() });
|
|
98
|
-
saveArtifacts(data);
|
|
99
|
-
return normalizeArtifact(artifact);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function getArtifact(artifactId) {
|
|
103
|
-
return loadArtifacts().artifacts.find((artifact) => artifact.id === artifactId) || null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function allocatePort(preferredPort) {
|
|
107
|
-
if (preferredPort !== undefined && preferredPort !== null) {
|
|
108
|
-
const parsed = Number(preferredPort);
|
|
109
|
-
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) return parsed;
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const used = new Set(loadArtifacts().artifacts.map((artifact) => Number(artifact.port)));
|
|
114
|
-
for (let port = ARTIFACT_PORT_MIN; port <= ARTIFACT_PORT_MAX; port += 1) {
|
|
115
|
-
if (!used.has(port)) return port;
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function allocateUniqueArtifactRef() {
|
|
121
|
-
const used = new Set(loadArtifacts().artifacts.map((artifact) => artifact.artifactRef));
|
|
122
|
-
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
123
|
-
const candidate = genArtifactRef();
|
|
124
|
-
if (!used.has(candidate)) return candidate;
|
|
125
|
-
}
|
|
126
|
-
throw new Error('Could not allocate artifact DNS ref');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function getConnectedRoutes() {
|
|
130
|
-
return loadArtifacts()
|
|
131
|
-
.artifacts
|
|
132
|
-
.filter((artifact) =>
|
|
133
|
-
artifact.dnsConnected
|
|
134
|
-
&& artifact.desiredState === 'running'
|
|
135
|
-
&& artifact.status !== 'stopped'
|
|
136
|
-
&& artifact.status !== 'error'
|
|
137
|
-
&& artifact.port
|
|
138
|
-
&& artifact.artifactRef,
|
|
139
|
-
)
|
|
140
|
-
.map((artifact) => ({
|
|
141
|
-
artifact_ref: artifact.artifactRef,
|
|
142
|
-
port: artifact.port,
|
|
143
|
-
name: artifact.name,
|
|
144
|
-
}));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
module.exports = {
|
|
148
|
-
allocatePort,
|
|
149
|
-
allocateUniqueArtifactRef,
|
|
150
|
-
ensureArtifactsDirs,
|
|
151
|
-
getArtifact,
|
|
152
|
-
getConnectedRoutes,
|
|
153
|
-
loadArtifacts,
|
|
154
|
-
publicUrlForRef,
|
|
155
|
-
saveArtifacts,
|
|
156
|
-
updateArtifactMeta,
|
|
157
|
-
};
|
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal artifact supervisor.
|
|
3
|
-
*
|
|
4
|
-
* Runs user-provided commands. It does not scaffold, wrap, or impose an app
|
|
5
|
-
* framework. The artifact process is expected to bind to PORT / {port}.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const { spawn } = require('child_process');
|
|
11
|
-
const { DEFAULT_CWD } = require('../config');
|
|
12
|
-
const activeMemory = require('../../chat-core/tooling/active-memory');
|
|
13
|
-
const { runtimePort } = require('../../../lib/runtime-manifest');
|
|
14
|
-
const { syncArtifactRoutesToGateway } = require('./advertise');
|
|
15
|
-
const {
|
|
16
|
-
allocatePort,
|
|
17
|
-
allocateUniqueArtifactRef,
|
|
18
|
-
getArtifact,
|
|
19
|
-
loadArtifacts,
|
|
20
|
-
publicUrlForRef,
|
|
21
|
-
saveArtifacts,
|
|
22
|
-
updateArtifactMeta,
|
|
23
|
-
} = require('./store');
|
|
24
|
-
|
|
25
|
-
const running = new Map();
|
|
26
|
-
const stopping = new Set();
|
|
27
|
-
const restartTimers = new Map();
|
|
28
|
-
|
|
29
|
-
const ARTIFACT_ENV_ALLOWLIST = [
|
|
30
|
-
'AMALGM_BIND_HOST',
|
|
31
|
-
'AMALGM_DIR',
|
|
32
|
-
'AMALGM_GATEWAY_PORT',
|
|
33
|
-
'AMALGM_MCP_PORT',
|
|
34
|
-
'AMALGM_RUNTIME_TOKEN',
|
|
35
|
-
'AMALGM_WORKSPACES_DIR',
|
|
36
|
-
'CHAT_SERVER_PORT',
|
|
37
|
-
'PATH',
|
|
38
|
-
'HOME',
|
|
39
|
-
'USER',
|
|
40
|
-
'LOGNAME',
|
|
41
|
-
'SHELL',
|
|
42
|
-
'TMPDIR',
|
|
43
|
-
'TMP',
|
|
44
|
-
'TEMP',
|
|
45
|
-
'LANG',
|
|
46
|
-
'LC_ALL',
|
|
47
|
-
'LC_CTYPE',
|
|
48
|
-
'TERM',
|
|
49
|
-
'NODE_ENV',
|
|
50
|
-
'COREPACK_HOME',
|
|
51
|
-
'PNPM_HOME',
|
|
52
|
-
'YARN_CACHE_FOLDER',
|
|
53
|
-
'BUN_INSTALL',
|
|
54
|
-
'PLAYWRIGHT_BROWSERS_PATH',
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
function isProcessAlive(pid) {
|
|
58
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
59
|
-
try {
|
|
60
|
-
process.kill(pid, 0);
|
|
61
|
-
return true;
|
|
62
|
-
} catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function syncRoutesBestEffort(reason) {
|
|
68
|
-
try {
|
|
69
|
-
await syncArtifactRoutesToGateway(reason);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
-
console.warn(`[AmalgmMCP] Artifact route sync failed (${reason}): ${message}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function replacePort(command, port) {
|
|
77
|
-
return String(command || '').replace(/\{port\}/g, String(port));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function buildEnv(artifact) {
|
|
81
|
-
const env = {};
|
|
82
|
-
for (const key of ARTIFACT_ENV_ALLOWLIST) {
|
|
83
|
-
if (process.env[key]) env[key] = process.env[key];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
...env,
|
|
88
|
-
PORT: String(artifact.port),
|
|
89
|
-
AMALGM_ARTIFACT_ID: artifact.id,
|
|
90
|
-
AMALGM_ARTIFACT_REF: artifact.artifactRef,
|
|
91
|
-
AMALGM_ARTIFACT_URL: artifact.publicUrl,
|
|
92
|
-
AMALGM_CHAT_SERVER_URL: `http://127.0.0.1:${runtimePort('chat-server')}`,
|
|
93
|
-
AMALGM_MCP_URL: `http://127.0.0.1:${runtimePort('amalgm-mcp')}`,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function runCommand(command, cwd, env, timeoutMs = 10 * 60 * 1000) {
|
|
98
|
-
return new Promise((resolve, reject) => {
|
|
99
|
-
const child = spawn(command, {
|
|
100
|
-
cwd,
|
|
101
|
-
env,
|
|
102
|
-
shell: true,
|
|
103
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
104
|
-
windowsHide: true,
|
|
105
|
-
});
|
|
106
|
-
let output = '';
|
|
107
|
-
const timer = setTimeout(() => {
|
|
108
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
109
|
-
reject(new Error(`Command timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
110
|
-
}, timeoutMs);
|
|
111
|
-
|
|
112
|
-
const append = (chunk) => {
|
|
113
|
-
output += chunk.toString();
|
|
114
|
-
if (output.length > 12_000) output = output.slice(-12_000);
|
|
115
|
-
};
|
|
116
|
-
child.stdout?.on('data', append);
|
|
117
|
-
child.stderr?.on('data', append);
|
|
118
|
-
child.on('error', (err) => {
|
|
119
|
-
clearTimeout(timer);
|
|
120
|
-
reject(err);
|
|
121
|
-
});
|
|
122
|
-
child.on('exit', (code) => {
|
|
123
|
-
clearTimeout(timer);
|
|
124
|
-
if (code === 0) {
|
|
125
|
-
resolve(output);
|
|
126
|
-
} else {
|
|
127
|
-
reject(new Error(output.trim() || `Command exited with code ${code}`));
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function runBuild(artifact) {
|
|
134
|
-
if (!artifact.buildCommand) return;
|
|
135
|
-
updateArtifactMeta(artifact.id, { status: 'building', error: null });
|
|
136
|
-
const command = replacePort(artifact.buildCommand, artifact.port);
|
|
137
|
-
await runCommand(command, artifact.cwd, buildEnv(artifact));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function startArtifactProcess(artifact) {
|
|
141
|
-
if (!artifact.startCommand) throw new Error('startCommand is required');
|
|
142
|
-
if (!fs.existsSync(artifact.cwd)) throw new Error(`Artifact cwd does not exist: ${artifact.cwd}`);
|
|
143
|
-
|
|
144
|
-
const command = replacePort(artifact.startCommand, artifact.port);
|
|
145
|
-
const child = spawn(command, {
|
|
146
|
-
cwd: artifact.cwd,
|
|
147
|
-
env: buildEnv(artifact),
|
|
148
|
-
shell: true,
|
|
149
|
-
detached: process.platform !== 'win32',
|
|
150
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
151
|
-
windowsHide: true,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
running.set(artifact.id, child);
|
|
155
|
-
child.unref();
|
|
156
|
-
child.stdout?.on('data', (chunk) => {
|
|
157
|
-
console.log(`[Artifact:${artifact.id}] ${chunk.toString().trim()}`);
|
|
158
|
-
});
|
|
159
|
-
child.stderr?.on('data', (chunk) => {
|
|
160
|
-
console.error(`[Artifact:${artifact.id}] ${chunk.toString().trim()}`);
|
|
161
|
-
});
|
|
162
|
-
child.on('exit', (code) => {
|
|
163
|
-
const activeChild = running.get(artifact.id);
|
|
164
|
-
if (activeChild && activeChild !== child) return;
|
|
165
|
-
running.delete(artifact.id);
|
|
166
|
-
const wasStopping = stopping.has(artifact.id);
|
|
167
|
-
stopping.delete(artifact.id);
|
|
168
|
-
if (wasStopping) return;
|
|
169
|
-
|
|
170
|
-
const current = getArtifact(artifact.id);
|
|
171
|
-
if (!current) return;
|
|
172
|
-
|
|
173
|
-
if (current.desiredState === 'running' && current.keepAlive !== false) {
|
|
174
|
-
updateArtifactMeta(artifact.id, {
|
|
175
|
-
status: 'restarting',
|
|
176
|
-
pid: null,
|
|
177
|
-
error: code === 0 ? null : `Process exited with code ${code}`,
|
|
178
|
-
});
|
|
179
|
-
void syncRoutesBestEffort(`exit:${artifact.id}`);
|
|
180
|
-
const timer = setTimeout(() => {
|
|
181
|
-
restartTimers.delete(artifact.id);
|
|
182
|
-
startArtifact(artifact.id).catch((err) => {
|
|
183
|
-
updateArtifactMeta(artifact.id, { status: 'error', error: err.message, pid: null });
|
|
184
|
-
});
|
|
185
|
-
}, 2000);
|
|
186
|
-
restartTimers.set(artifact.id, timer);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
updateArtifactMeta(artifact.id, {
|
|
191
|
-
status: code === 0 ? 'stopped' : 'error',
|
|
192
|
-
pid: null,
|
|
193
|
-
error: code === 0 ? null : `Process exited with code ${code}`,
|
|
194
|
-
});
|
|
195
|
-
void syncRoutesBestEffort(`exit:${artifact.id}`);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
return child;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function startArtifact(artifactId) {
|
|
202
|
-
const artifact = getArtifact(artifactId);
|
|
203
|
-
if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
|
|
204
|
-
const existingTimer = restartTimers.get(artifactId);
|
|
205
|
-
if (existingTimer) clearTimeout(existingTimer);
|
|
206
|
-
restartTimers.delete(artifactId);
|
|
207
|
-
|
|
208
|
-
if (running.has(artifactId)) {
|
|
209
|
-
const current = updateArtifactMeta(artifact.id, {
|
|
210
|
-
desiredState: 'running',
|
|
211
|
-
status: 'running',
|
|
212
|
-
error: null,
|
|
213
|
-
});
|
|
214
|
-
await syncRoutesBestEffort(`start:${artifactId}`);
|
|
215
|
-
return current;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (artifact.pid && isProcessAlive(artifact.pid)) {
|
|
219
|
-
const current = updateArtifactMeta(artifact.id, {
|
|
220
|
-
desiredState: 'running',
|
|
221
|
-
status: 'running',
|
|
222
|
-
error: null,
|
|
223
|
-
});
|
|
224
|
-
await syncRoutesBestEffort(`start:${artifactId}`);
|
|
225
|
-
return current;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (artifact.pid && !isProcessAlive(artifact.pid)) {
|
|
229
|
-
updateArtifactMeta(artifact.id, { pid: null });
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const child = startArtifactProcess(artifact);
|
|
233
|
-
const current = updateArtifactMeta(artifact.id, {
|
|
234
|
-
desiredState: 'running',
|
|
235
|
-
status: 'running',
|
|
236
|
-
pid: child.pid,
|
|
237
|
-
error: null,
|
|
238
|
-
lastStartedAt: new Date().toISOString(),
|
|
239
|
-
});
|
|
240
|
-
await syncRoutesBestEffort(`start:${artifactId}`);
|
|
241
|
-
return current;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function stopArtifact(artifactId, options = {}) {
|
|
245
|
-
const artifact = getArtifact(artifactId);
|
|
246
|
-
if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
|
|
247
|
-
|
|
248
|
-
const timer = restartTimers.get(artifactId);
|
|
249
|
-
if (timer) clearTimeout(timer);
|
|
250
|
-
restartTimers.delete(artifactId);
|
|
251
|
-
stopping.add(artifactId);
|
|
252
|
-
|
|
253
|
-
const child = running.get(artifactId);
|
|
254
|
-
if (child?.pid) {
|
|
255
|
-
try { process.kill(-child.pid, 'SIGTERM'); } catch {}
|
|
256
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
257
|
-
} else if (artifact.pid) {
|
|
258
|
-
try { process.kill(artifact.pid, 'SIGTERM'); } catch {}
|
|
259
|
-
}
|
|
260
|
-
running.delete(artifactId);
|
|
261
|
-
|
|
262
|
-
setTimeout(() => stopping.delete(artifactId), 1000);
|
|
263
|
-
const current = updateArtifactMeta(artifactId, {
|
|
264
|
-
desiredState: options.desiredState || 'stopped',
|
|
265
|
-
status: 'stopped',
|
|
266
|
-
pid: null,
|
|
267
|
-
});
|
|
268
|
-
await syncRoutesBestEffort(`stop:${artifactId}`);
|
|
269
|
-
return current;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async function deleteArtifact(artifactId) {
|
|
273
|
-
const artifact = getArtifact(artifactId);
|
|
274
|
-
if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
|
|
275
|
-
|
|
276
|
-
const timer = restartTimers.get(artifactId);
|
|
277
|
-
if (timer) clearTimeout(timer);
|
|
278
|
-
restartTimers.delete(artifactId);
|
|
279
|
-
|
|
280
|
-
await stopArtifact(artifactId, { desiredState: 'stopped' }).catch(() => {});
|
|
281
|
-
|
|
282
|
-
const data = loadArtifacts();
|
|
283
|
-
data.artifacts = data.artifacts.filter((item) => item.id !== artifactId);
|
|
284
|
-
saveArtifacts(data);
|
|
285
|
-
|
|
286
|
-
stopping.delete(artifactId);
|
|
287
|
-
running.delete(artifactId);
|
|
288
|
-
await syncRoutesBestEffort(`delete:${artifactId}`);
|
|
289
|
-
|
|
290
|
-
return artifact;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async function registerArtifact(input) {
|
|
294
|
-
const name = String(input.name || '').trim() || 'Artifact';
|
|
295
|
-
const cwd = path.resolve(input.cwd || input.root_dir || DEFAULT_CWD);
|
|
296
|
-
const startCommand = String(input.start_command || input.startCommand || '').trim();
|
|
297
|
-
if (!startCommand) throw new Error('start_command is required');
|
|
298
|
-
if (!fs.existsSync(cwd)) throw new Error(`cwd does not exist: ${cwd}`);
|
|
299
|
-
|
|
300
|
-
const port = allocatePort(input.port);
|
|
301
|
-
if (!port) throw new Error('No valid artifact port available');
|
|
302
|
-
|
|
303
|
-
const artifactRef = allocateUniqueArtifactRef();
|
|
304
|
-
const now = new Date().toISOString();
|
|
305
|
-
const artifact = {
|
|
306
|
-
id: input.id || cryptoRandomId(),
|
|
307
|
-
kind: 'artifact',
|
|
308
|
-
name,
|
|
309
|
-
description: input.description || '',
|
|
310
|
-
cwd,
|
|
311
|
-
port,
|
|
312
|
-
buildCommand: input.build_command || input.buildCommand || null,
|
|
313
|
-
startCommand,
|
|
314
|
-
artifactRef,
|
|
315
|
-
artifact_ref: artifactRef,
|
|
316
|
-
publicUrl: publicUrlForRef(artifactRef),
|
|
317
|
-
dnsConnected: input.connect_dns !== false,
|
|
318
|
-
autostart: input.autostart !== false,
|
|
319
|
-
keepAlive: input.keep_alive !== false,
|
|
320
|
-
desiredState: 'running',
|
|
321
|
-
status: 'registered',
|
|
322
|
-
pid: null,
|
|
323
|
-
createdAt: now,
|
|
324
|
-
updatedAt: now,
|
|
325
|
-
lastStartedAt: null,
|
|
326
|
-
lastDeployedAt: null,
|
|
327
|
-
error: null,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const data = loadArtifacts();
|
|
331
|
-
data.artifacts.push(artifact);
|
|
332
|
-
saveArtifacts(data);
|
|
333
|
-
activeMemory.ensureConstructMemory({
|
|
334
|
-
type: 'artifact',
|
|
335
|
-
id: artifact.id,
|
|
336
|
-
name: artifact.name,
|
|
337
|
-
projectPath: artifact.cwd,
|
|
338
|
-
}, { source: 'artifact:register' });
|
|
339
|
-
|
|
340
|
-
if (artifact.buildCommand) await runBuild(artifact);
|
|
341
|
-
await startArtifact(artifact.id);
|
|
342
|
-
const deployed = updateArtifactMeta(artifact.id, { lastDeployedAt: new Date().toISOString() });
|
|
343
|
-
activeMemory.ensureConstructMemory({
|
|
344
|
-
type: 'artifact',
|
|
345
|
-
id: deployed.id,
|
|
346
|
-
name: deployed.name,
|
|
347
|
-
projectPath: deployed.cwd,
|
|
348
|
-
}, { source: 'artifact:deploy' });
|
|
349
|
-
return deployed;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async function redeployArtifact(artifactId, updates = {}) {
|
|
353
|
-
let artifact = getArtifact(artifactId);
|
|
354
|
-
if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
|
|
355
|
-
|
|
356
|
-
const patch = {};
|
|
357
|
-
if (updates.cwd || updates.root_dir) patch.cwd = path.resolve(updates.cwd || updates.root_dir);
|
|
358
|
-
if (updates.port !== undefined) {
|
|
359
|
-
const parsed = Number(updates.port);
|
|
360
|
-
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
361
|
-
throw new Error('port must be between 1 and 65535');
|
|
362
|
-
}
|
|
363
|
-
patch.port = parsed;
|
|
364
|
-
}
|
|
365
|
-
if (updates.build_command !== undefined) patch.buildCommand = updates.build_command || null;
|
|
366
|
-
if (updates.start_command !== undefined) patch.startCommand = updates.start_command;
|
|
367
|
-
if (updates.keep_alive !== undefined) patch.keepAlive = updates.keep_alive !== false;
|
|
368
|
-
if (updates.autostart !== undefined) patch.autostart = updates.autostart !== false;
|
|
369
|
-
if (Object.keys(patch).length > 0) artifact = updateArtifactMeta(artifactId, patch);
|
|
370
|
-
activeMemory.ensureConstructMemory({
|
|
371
|
-
type: 'artifact',
|
|
372
|
-
id: artifact.id,
|
|
373
|
-
name: artifact.name,
|
|
374
|
-
projectPath: artifact.cwd,
|
|
375
|
-
}, { source: 'artifact:update' });
|
|
376
|
-
|
|
377
|
-
if (!artifact.startCommand) throw new Error('startCommand is required');
|
|
378
|
-
if (!fs.existsSync(artifact.cwd)) throw new Error(`cwd does not exist: ${artifact.cwd}`);
|
|
379
|
-
|
|
380
|
-
if (artifact.buildCommand) await runBuild(artifact);
|
|
381
|
-
await stopArtifact(artifactId, { desiredState: 'running' });
|
|
382
|
-
await startArtifact(artifactId);
|
|
383
|
-
const deployed = updateArtifactMeta(artifactId, { lastDeployedAt: new Date().toISOString() });
|
|
384
|
-
activeMemory.ensureConstructMemory({
|
|
385
|
-
type: 'artifact',
|
|
386
|
-
id: deployed.id,
|
|
387
|
-
name: deployed.name,
|
|
388
|
-
projectPath: deployed.cwd,
|
|
389
|
-
}, { source: 'artifact:deploy' });
|
|
390
|
-
return deployed;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function cryptoRandomId() {
|
|
394
|
-
return `artifact-${Math.random().toString(36).slice(2, 8)}${Date.now().toString(36).slice(-4)}`;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function connectDns(artifactId) {
|
|
398
|
-
const artifact = getArtifact(artifactId);
|
|
399
|
-
if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
|
|
400
|
-
const current = updateArtifactMeta(artifactId, { dnsConnected: true });
|
|
401
|
-
await syncRoutesBestEffort(`connect-dns:${artifactId}`);
|
|
402
|
-
return current;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
async function disconnectDns(artifactId) {
|
|
406
|
-
const artifact = getArtifact(artifactId);
|
|
407
|
-
if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
|
|
408
|
-
const current = updateArtifactMeta(artifactId, { dnsConnected: false });
|
|
409
|
-
await syncRoutesBestEffort(`disconnect-dns:${artifactId}`);
|
|
410
|
-
return current;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
async function startRegisteredArtifacts() {
|
|
414
|
-
const data = loadArtifacts();
|
|
415
|
-
await Promise.allSettled(data.artifacts.map(async (artifact) => {
|
|
416
|
-
if (artifact.pid && !isProcessAlive(artifact.pid)) {
|
|
417
|
-
updateArtifactMeta(artifact.id, { pid: null });
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (artifact.autostart !== false && artifact.desiredState === 'running') {
|
|
421
|
-
await startArtifact(artifact.id).catch((err) => {
|
|
422
|
-
updateArtifactMeta(artifact.id, { status: 'error', error: err.message, pid: null });
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
}));
|
|
426
|
-
await syncRoutesBestEffort('boot');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
module.exports = {
|
|
430
|
-
deleteArtifact,
|
|
431
|
-
connectDns,
|
|
432
|
-
disconnectDns,
|
|
433
|
-
redeployArtifact,
|
|
434
|
-
registerArtifact,
|
|
435
|
-
runBuild,
|
|
436
|
-
startArtifact,
|
|
437
|
-
startRegisteredArtifacts,
|
|
438
|
-
stopArtifact,
|
|
439
|
-
};
|