@ai-support-agent/cli 0.0.39 → 0.0.40-beta.0
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/agent-runner.d.ts +2 -6
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +6 -13
- package/dist/agent-runner.js.map +1 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +3 -7
- package/dist/api-client.js.map +1 -1
- package/dist/commands/chat-executor.d.ts +0 -14
- package/dist/commands/chat-executor.d.ts.map +1 -1
- package/dist/commands/chat-executor.js +0 -18
- package/dist/commands/chat-executor.js.map +1 -1
- package/dist/docker/docker-runner.d.ts +16 -70
- package/dist/docker/docker-runner.d.ts.map +1 -1
- package/dist/docker/docker-runner.js +50 -989
- package/dist/docker/docker-runner.js.map +1 -1
- package/dist/docker/docker-security.d.ts +11 -0
- package/dist/docker/docker-security.d.ts.map +1 -0
- package/dist/docker/docker-security.js +25 -0
- package/dist/docker/docker-security.js.map +1 -0
- package/dist/docker/docker-supervisor.d.ts +30 -0
- package/dist/docker/docker-supervisor.d.ts.map +1 -0
- package/dist/docker/docker-supervisor.js +408 -0
- package/dist/docker/docker-supervisor.js.map +1 -0
- package/dist/docker/docker-utils.d.ts +46 -0
- package/dist/docker/docker-utils.d.ts.map +1 -0
- package/dist/docker/docker-utils.js +147 -0
- package/dist/docker/docker-utils.js.map +1 -0
- package/dist/docker/dockerfile-generator.d.ts +13 -0
- package/dist/docker/dockerfile-generator.d.ts.map +1 -0
- package/dist/docker/dockerfile-generator.js +50 -0
- package/dist/docker/dockerfile-generator.js.map +1 -0
- package/dist/docker/dockerfile-sync.d.ts +13 -0
- package/dist/docker/dockerfile-sync.d.ts.map +1 -0
- package/dist/docker/dockerfile-sync.js +76 -0
- package/dist/docker/dockerfile-sync.js.map +1 -0
- package/dist/docker/project-config.d.ts +22 -0
- package/dist/docker/project-config.d.ts.map +1 -0
- package/dist/docker/project-config.js +80 -0
- package/dist/docker/project-config.js.map +1 -0
- package/dist/docker/project-image-builder.d.ts +12 -0
- package/dist/docker/project-image-builder.d.ts.map +1 -0
- package/dist/docker/project-image-builder.js +90 -0
- package/dist/docker/project-image-builder.js.map +1 -0
- package/dist/docker/update-handler.d.ts +14 -0
- package/dist/docker/update-handler.d.ts.map +1 -0
- package/dist/docker/update-handler.js +93 -0
- package/dist/docker/update-handler.js.map +1 -0
- package/dist/docker/version-manager.d.ts +21 -0
- package/dist/docker/version-manager.d.ts.map +1 -0
- package/dist/docker/version-manager.js +67 -0
- package/dist/docker/version-manager.js.map +1 -0
- package/dist/docker/volume-mount-builder.d.ts +38 -0
- package/dist/docker/volume-mount-builder.d.ts.map +1 -0
- package/dist/docker/volume-mount-builder.js +237 -0
- package/dist/docker/volume-mount-builder.js.map +1 -0
- package/dist/project-agent.d.ts.map +1 -1
- package/dist/project-agent.js +2 -6
- package/dist/project-agent.js.map +1 -1
- package/dist/utils/token-utils.d.ts +30 -0
- package/dist/utils/token-utils.d.ts.map +1 -0
- package/dist/utils/token-utils.js +44 -0
- package/dist/utils/token-utils.js.map +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +11 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Docker runner — main entry point for Docker-based execution
|
|
4
|
+
*
|
|
5
|
+
* Orchestrates Docker availability check, image build, and container supervision.
|
|
6
|
+
* Supports both legacy single-container mode and per-project supervisor mode.
|
|
7
|
+
*/
|
|
2
8
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
9
|
if (k2 === undefined) k2 = k;
|
|
4
10
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -33,375 +39,24 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
39
|
};
|
|
34
40
|
})();
|
|
35
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.checkDockerAvailable =
|
|
37
|
-
exports.imageExists = imageExists;
|
|
38
|
-
exports.buildImage = buildImage;
|
|
39
|
-
exports.validatePackageNames = validatePackageNames;
|
|
40
|
-
exports.generateProjectDockerfile = generateProjectDockerfile;
|
|
41
|
-
exports.buildProjectImage = buildProjectImage;
|
|
42
|
-
exports.buildContainerName = buildContainerName;
|
|
43
|
-
exports.removeStaleContainer = removeStaleContainer;
|
|
44
|
-
exports.buildVolumeMounts = buildVolumeMounts;
|
|
45
|
-
exports.buildEnvArgs = buildEnvArgs;
|
|
42
|
+
exports.DockerSupervisor = exports.migrateProjectConfigDir = exports.ensureImage = exports.resetInstalledVersionCache = exports.getInstalledVersion = exports.buildProjectVolumeMounts = exports.buildEnvArgs = exports.buildVolumeMounts = exports.buildProjectImage = exports.syncDockerfileToConfigDir = exports.generateProjectDockerfile = exports.validatePackageNames = exports.dockerLogin = exports.removeStaleContainer = exports.buildContainerName = exports.buildImage = exports.imageExists = exports.checkDockerAvailable = void 0;
|
|
46
43
|
exports.buildContainerArgs = buildContainerArgs;
|
|
47
|
-
exports.getInstalledVersion = getInstalledVersion;
|
|
48
|
-
exports.resetInstalledVersionCache = resetInstalledVersionCache;
|
|
49
|
-
exports.ensureImage = ensureImage;
|
|
50
|
-
exports.dockerLogin = dockerLogin;
|
|
51
|
-
exports.syncDockerfileToConfigDir = syncDockerfileToConfigDir;
|
|
52
|
-
exports.migrateProjectConfigDir = migrateProjectConfigDir;
|
|
53
44
|
exports.resetIsDockerRunning = resetIsDockerRunning;
|
|
54
45
|
exports.runInDocker = runInDocker;
|
|
55
46
|
const child_process_1 = require("child_process");
|
|
56
|
-
const fs = __importStar(require("fs"));
|
|
57
47
|
const os = __importStar(require("os"));
|
|
58
|
-
const path = __importStar(require("path"));
|
|
59
|
-
const dockerfile_path_1 = require("./dockerfile-path");
|
|
60
48
|
const constants_1 = require("../constants");
|
|
61
49
|
const config_manager_1 = require("../config-manager");
|
|
62
50
|
const pid_manager_1 = require("../pid-manager");
|
|
63
51
|
const i18n_1 = require("../i18n");
|
|
64
52
|
const logger_1 = require("../logger");
|
|
65
|
-
const security_1 = require("../security");
|
|
66
53
|
const claude_config_validator_1 = require("../utils/claude-config-validator");
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return (String(d.getFullYear()) +
|
|
74
|
-
pad(d.getMonth() + 1) +
|
|
75
|
-
pad(d.getDate()) +
|
|
76
|
-
pad(d.getHours()) +
|
|
77
|
-
pad(d.getMinutes()) +
|
|
78
|
-
pad(d.getSeconds()));
|
|
79
|
-
}
|
|
80
|
-
/** Convert a path.relative() result to POSIX format for container use */
|
|
81
|
-
function toPosixRelative(relativePath) {
|
|
82
|
-
return relativePath.split(path.sep).join('/');
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Returns true when running via ts-node (i.e. `npm run dev`).
|
|
86
|
-
* In this case, local dist/ should be mounted into containers instead of
|
|
87
|
-
* relying on the npm-installed package inside the image.
|
|
88
|
-
*/
|
|
89
|
-
function isRunningViaTsNode() {
|
|
90
|
-
const sym = Symbol.for('ts-node.register.instance');
|
|
91
|
-
return !!process[sym];
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Returns extra volume mount args to overlay the local dist/ into the container
|
|
95
|
-
* when running in dev mode (ts-node), so the container uses local source code.
|
|
96
|
-
* Returns an empty array when not in dev mode.
|
|
97
|
-
*/
|
|
98
|
-
function buildDevMounts() {
|
|
99
|
-
if (!isRunningViaTsNode())
|
|
100
|
-
return [];
|
|
101
|
-
// __dirname is agent/src/docker — walk up two levels to get agent/
|
|
102
|
-
const agentRoot = path.resolve(__dirname, '..', '..');
|
|
103
|
-
const distDir = path.join(agentRoot, 'dist');
|
|
104
|
-
const localesDir = path.join(agentRoot, 'src', 'locales');
|
|
105
|
-
const containerBase = '/usr/local/lib/node_modules/@ai-support-agent/cli';
|
|
106
|
-
return [
|
|
107
|
-
'-v', `${distDir}:${containerBase}/dist:ro`,
|
|
108
|
-
'-v', `${localesDir}:${containerBase}/dist/locales:ro`,
|
|
109
|
-
];
|
|
110
|
-
}
|
|
111
|
-
const IMAGE_NAME = 'ai-support-agent';
|
|
112
|
-
const PASSTHROUGH_ENV_VARS = [
|
|
113
|
-
'AI_SUPPORT_AGENT_TOKEN',
|
|
114
|
-
'AI_SUPPORT_AGENT_API_URL',
|
|
115
|
-
'AI_SUPPORT_AGENT_CONFIG_DIR',
|
|
116
|
-
'ANTHROPIC_API_KEY',
|
|
117
|
-
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
118
|
-
];
|
|
119
|
-
function checkDockerAvailable() {
|
|
120
|
-
try {
|
|
121
|
-
(0, child_process_1.execFileSync)('docker', ['info'], { stdio: 'ignore' });
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
function imageExists(version) {
|
|
129
|
-
try {
|
|
130
|
-
(0, child_process_1.execFileSync)('docker', ['image', 'inspect', `${IMAGE_NAME}:${version}`], { stdio: 'ignore' });
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function buildImage(version, customDockerfile) {
|
|
138
|
-
const { dockerfilePath, contextDir } = (0, dockerfile_path_1.resolveDockerfile)(customDockerfile);
|
|
139
|
-
logger_1.logger.info((0, i18n_1.t)('docker.building'));
|
|
140
|
-
if (customDockerfile) {
|
|
141
|
-
logger_1.logger.info((0, i18n_1.t)('docker.usingCustomDockerfile', { path: dockerfilePath }));
|
|
142
|
-
}
|
|
143
|
-
(0, child_process_1.execFileSync)('docker', ['build', '-t', `${IMAGE_NAME}:${version}`, '--pull=false', '--build-arg', `AGENT_VERSION=${version}`, '-f', dockerfilePath, contextDir], { stdio: 'inherit' });
|
|
144
|
-
logger_1.logger.success((0, i18n_1.t)('docker.buildComplete'));
|
|
145
|
-
}
|
|
146
|
-
/** Allowlist for apt package names: letters, digits, hyphens, dots, plus signs, colons */
|
|
147
|
-
const APT_PACKAGE_RE = /^[a-zA-Z0-9][a-zA-Z0-9+\-.:]*$/;
|
|
148
|
-
/** Allowlist for npm package names: scoped (@scope/name) or plain, with version (@x.y.z) */
|
|
149
|
-
const NPM_PACKAGE_RE = /^(@[a-zA-Z0-9_\-.]+\/)?[a-zA-Z0-9_\-.]+(@[a-zA-Z0-9._\-^~*]+)?$/;
|
|
150
|
-
/**
|
|
151
|
-
* Validate a list of package names against an allowlist regex.
|
|
152
|
-
* Throws if any name is invalid to prevent Dockerfile injection.
|
|
153
|
-
*/
|
|
154
|
-
function validatePackageNames(packages, type) {
|
|
155
|
-
const re = type === 'apt' ? APT_PACKAGE_RE : NPM_PACKAGE_RE;
|
|
156
|
-
for (const pkg of packages) {
|
|
157
|
-
if (!re.test(pkg)) {
|
|
158
|
-
throw new Error(`Invalid ${type} package name: "${pkg}"`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Generate a per-project Dockerfile that extends the base agent image.
|
|
164
|
-
*/
|
|
165
|
-
function generateProjectDockerfile(baseVersion, aptPackages, npmPackages, commands = [], timezone) {
|
|
166
|
-
validatePackageNames(aptPackages, 'apt');
|
|
167
|
-
validatePackageNames(npmPackages, 'npm');
|
|
168
|
-
const lines = [`FROM ${IMAGE_NAME}:${baseVersion}`];
|
|
169
|
-
if (timezone) {
|
|
170
|
-
// Override the TZ env var set by docker run, allowing per-project custom timezone
|
|
171
|
-
lines.push(`ENV TZ=${timezone}`);
|
|
172
|
-
}
|
|
173
|
-
if (aptPackages.length > 0) {
|
|
174
|
-
lines.push(`RUN apt-get update && apt-get install -y --no-install-recommends \\`, ` ${aptPackages.join(' \\\n ')} \\`, ` && rm -rf /var/lib/apt/lists/*`);
|
|
175
|
-
}
|
|
176
|
-
if (npmPackages.length > 0) {
|
|
177
|
-
lines.push(`RUN npm install -g ${npmPackages.join(' ')} && npm cache clean --force`);
|
|
178
|
-
}
|
|
179
|
-
for (const cmd of commands) {
|
|
180
|
-
if (/[\n\r|`;$()]/.test(cmd)) {
|
|
181
|
-
throw new Error(`Invalid command (contains forbidden character): "${cmd.substring(0, 50)}"`);
|
|
182
|
-
}
|
|
183
|
-
lines.push(`RUN ${cmd}`);
|
|
184
|
-
}
|
|
185
|
-
return lines.join('\n') + '\n';
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Build a per-project Docker image using the given Dockerfile.
|
|
189
|
-
* Streams stdout/stderr to both the host terminal and the API (for real-time log viewing).
|
|
190
|
-
*/
|
|
191
|
-
/** Maximum total log size kept in memory per session (2 MB). Older content is discarded. */
|
|
192
|
-
const MAX_SESSION_LOG_BYTES = 2 * 1024 * 1024;
|
|
193
|
-
/**
|
|
194
|
-
* docker build プロセスに渡す環境変数を必要最小限に絞る。
|
|
195
|
-
* process.env をそのまま渡すと API キー等の機密情報が漏洩するため、
|
|
196
|
-
* ビルドに必要な変数のみを明示的に選択する。
|
|
197
|
-
*/
|
|
198
|
-
function buildDockerEnv() {
|
|
199
|
-
const ALLOWED_KEYS = ['PATH', 'HOME', 'USER', 'TMPDIR', 'TMP', 'TEMP', 'LANG', 'LC_ALL'];
|
|
200
|
-
const env = {};
|
|
201
|
-
for (const key of ALLOWED_KEYS) {
|
|
202
|
-
if (process.env[key] !== undefined) {
|
|
203
|
-
env[key] = process.env[key];
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
env['BUILDKIT_PROGRESS'] = 'plain';
|
|
207
|
-
return env;
|
|
208
|
-
}
|
|
209
|
-
async function buildProjectImage(tenantCode, projectCode, baseVersion, dockerfilePath, apiClient, agentId) {
|
|
210
|
-
const imageTag = (0, dockerfile_path_1.getProjectImageTag)(tenantCode, projectCode, baseVersion);
|
|
211
|
-
const contextDir = (0, dockerfile_path_1.getDockerContextDir)();
|
|
212
|
-
const projectKey = `${tenantCode}#${projectCode}`;
|
|
213
|
-
const color = (0, logger_1.getProjectColor)(projectKey);
|
|
214
|
-
const reset = '\x1b[0m';
|
|
215
|
-
const prefix = `${color}[${projectKey}]${reset} `;
|
|
216
|
-
logger_1.logger.info(`[docker] Building project image: ${imageTag}`);
|
|
217
|
-
const sessionId = makeSessionId();
|
|
218
|
-
let seq = 0;
|
|
219
|
-
let fullLog = '';
|
|
220
|
-
let logTruncated = false;
|
|
221
|
-
let buf = '';
|
|
222
|
-
const flush = async () => {
|
|
223
|
-
if (!buf)
|
|
224
|
-
return;
|
|
225
|
-
const text = buf;
|
|
226
|
-
buf = '';
|
|
227
|
-
if (!logTruncated) {
|
|
228
|
-
if (fullLog.length + text.length <= MAX_SESSION_LOG_BYTES) {
|
|
229
|
-
fullLog += text;
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
const remaining = MAX_SESSION_LOG_BYTES - fullLog.length;
|
|
233
|
-
fullLog += remaining > 0 ? text.slice(0, remaining) : '';
|
|
234
|
-
logTruncated = true;
|
|
235
|
-
logger_1.logger.warn('[docker] Build log exceeded 2 MB limit; remaining output will not be saved to S3');
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (apiClient) {
|
|
239
|
-
await apiClient.submitLogChunk({ agentId: agentId ?? '', projectCode, logType: 'docker-build', sessionId, seq: ++seq, text })
|
|
240
|
-
.catch((e) => logger_1.logger.warn(`[docker] Failed to send log chunk: ${e}`));
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
let buildError;
|
|
244
|
-
await new Promise((resolve, reject) => {
|
|
245
|
-
const proc = (0, child_process_1.spawn)('docker', [
|
|
246
|
-
'build', '-t', imageTag, '--pull=false', '--progress=plain',
|
|
247
|
-
'--build-arg', `AGENT_VERSION=${baseVersion}`,
|
|
248
|
-
'-f', dockerfilePath, contextDir,
|
|
249
|
-
], { stdio: ['ignore', 'pipe', 'pipe'], env: buildDockerEnv() });
|
|
250
|
-
const writePrefixed = (0, logger_1.makeLinePrefixer)(prefix, (s) => process.stdout.write(s));
|
|
251
|
-
const onData = (d) => {
|
|
252
|
-
const text = d.toString();
|
|
253
|
-
writePrefixed(text);
|
|
254
|
-
buf += text;
|
|
255
|
-
if (buf.length > 4096) {
|
|
256
|
-
void flush();
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
proc.stdout?.on('data', onData);
|
|
260
|
-
proc.stderr?.on('data', onData);
|
|
261
|
-
proc.on('error', (err) => reject(err));
|
|
262
|
-
proc.on('close', (code) => {
|
|
263
|
-
if (code === 0)
|
|
264
|
-
resolve();
|
|
265
|
-
else
|
|
266
|
-
reject(new Error(`docker build exited with code ${code}`));
|
|
267
|
-
});
|
|
268
|
-
}).catch((e) => { buildError = e instanceof Error ? e : new Error(String(e)); });
|
|
269
|
-
await flush();
|
|
270
|
-
if (apiClient && fullLog) {
|
|
271
|
-
await apiClient.saveSessionLog({ agentId: agentId ?? '', projectCode, logType: 'docker-build', sessionId, content: fullLog })
|
|
272
|
-
.catch((e) => logger_1.logger.warn(`[docker] Failed to upload build log to S3: ${e}`));
|
|
273
|
-
}
|
|
274
|
-
if (buildError)
|
|
275
|
-
throw buildError;
|
|
276
|
-
logger_1.logger.success(`[docker] Project image built: ${imageTag}`);
|
|
277
|
-
}
|
|
278
|
-
/** Container-internal base path for project directories */
|
|
279
|
-
const CONTAINER_PROJECTS_BASE = '/workspace/projects';
|
|
280
|
-
/** Container-internal home directory */
|
|
281
|
-
const CONTAINER_HOME = '/home/node';
|
|
282
|
-
/**
|
|
283
|
-
* Build a deterministic container name for a project.
|
|
284
|
-
* Format: asa-{tenantCode}-{projectCode}-{agentId}
|
|
285
|
-
* All components are lowercased and non-alphanumeric chars (except hyphens) are replaced with hyphens.
|
|
286
|
-
*/
|
|
287
|
-
function buildContainerName(tenantCode, projectCode, agentId) {
|
|
288
|
-
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
289
|
-
const parts = ['ai', sanitize(tenantCode), sanitize(projectCode)];
|
|
290
|
-
if (agentId)
|
|
291
|
-
parts.push(sanitize(agentId));
|
|
292
|
-
return parts.join('-');
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Remove a stale container with the given name if it exists.
|
|
296
|
-
* This handles the case where a previous run crashed and left a container behind,
|
|
297
|
-
* which would prevent `docker run --name` from succeeding.
|
|
298
|
-
*/
|
|
299
|
-
function removeStaleContainer(containerName) {
|
|
300
|
-
try {
|
|
301
|
-
(0, child_process_1.execFileSync)('docker', ['rm', '-f', containerName], { stdio: 'ignore' });
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
// Container does not exist or docker rm failed — ignore
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
function buildVolumeMounts() {
|
|
308
|
-
const home = os.homedir();
|
|
309
|
-
const mounts = [];
|
|
310
|
-
const projectMappings = [];
|
|
311
|
-
// Claude Code OAuth tokens and config — mount to container home
|
|
312
|
-
const claudeDir = path.join(home, '.claude');
|
|
313
|
-
if (fs.existsSync(claudeDir)) {
|
|
314
|
-
mounts.push('-v', `${claudeDir}:${path.posix.join(CONTAINER_HOME, '.claude')}:rw`);
|
|
315
|
-
}
|
|
316
|
-
const claudeJson = path.join(home, '.claude.json');
|
|
317
|
-
if (fs.existsSync(claudeJson)) {
|
|
318
|
-
mounts.push('-v', `${claudeJson}:${path.posix.join(CONTAINER_HOME, '.claude.json')}:rw`);
|
|
319
|
-
}
|
|
320
|
-
// Agent config — mount to container home
|
|
321
|
-
const agentConfigDir = (0, config_manager_1.getConfigDir)();
|
|
322
|
-
if (fs.existsSync(agentConfigDir)) {
|
|
323
|
-
const relativeToHome = path.relative(home, agentConfigDir);
|
|
324
|
-
const isUnderHome = !relativeToHome.startsWith('..');
|
|
325
|
-
const containerConfigDir = isUnderHome
|
|
326
|
-
? path.posix.join(CONTAINER_HOME, toPosixRelative(relativeToHome))
|
|
327
|
-
: `/workspace/.config/ai-support-agent`;
|
|
328
|
-
mounts.push('-v', `${agentConfigDir}:${containerConfigDir}:rw`);
|
|
329
|
-
}
|
|
330
|
-
// AWS credentials — mount to container home
|
|
331
|
-
const awsDir = path.join(home, '.aws');
|
|
332
|
-
if (fs.existsSync(awsDir)) {
|
|
333
|
-
mounts.push('-v', `${awsDir}:${path.posix.join(CONTAINER_HOME, '.aws')}:ro`);
|
|
334
|
-
}
|
|
335
|
-
// Custom project directories — mount to /workspace/projects/{projectCode}
|
|
336
|
-
const config = (0, config_manager_1.loadConfig)();
|
|
337
|
-
if (config?.projects) {
|
|
338
|
-
const mounted = new Set();
|
|
339
|
-
const blockedPrefixes = [...security_1.BLOCKED_PATH_PREFIXES, ...(0, security_1.getSensitiveHomePaths)()];
|
|
340
|
-
for (const project of config.projects) {
|
|
341
|
-
if (project.projectDir && !mounted.has(project.projectDir) && fs.existsSync(project.projectDir)) {
|
|
342
|
-
let resolved;
|
|
343
|
-
try {
|
|
344
|
-
resolved = fs.realpathSync(project.projectDir);
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
logger_1.logger.warn(`[docker] Cannot resolve path, skipping: ${project.projectDir}`);
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
const isBlocked = blockedPrefixes.some((prefix) => {
|
|
351
|
-
const prefixWithoutSlash = prefix.replace(/\/$/, '');
|
|
352
|
-
return resolved === prefixWithoutSlash || resolved.startsWith(prefix);
|
|
353
|
-
});
|
|
354
|
-
if (isBlocked) {
|
|
355
|
-
logger_1.logger.warn(`[docker] Skipping blocked path for volume mount: ${project.projectDir}`);
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
const containerDir = `${CONTAINER_PROJECTS_BASE}/${project.projectCode}`;
|
|
359
|
-
mounts.push('-v', `${project.projectDir}:${containerDir}:rw`);
|
|
360
|
-
projectMappings.push({
|
|
361
|
-
hostDir: project.projectDir,
|
|
362
|
-
containerDir,
|
|
363
|
-
projectCode: project.projectCode,
|
|
364
|
-
});
|
|
365
|
-
mounted.add(project.projectDir);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return { mounts, projectMappings };
|
|
370
|
-
}
|
|
371
|
-
function buildEnvArgs(projectMappings) {
|
|
372
|
-
const args = [];
|
|
373
|
-
// Mark process as running inside a Docker container
|
|
374
|
-
args.push('-e', 'AI_SUPPORT_AGENT_IN_DOCKER=1');
|
|
375
|
-
// Set HOME to container-internal path (not host path)
|
|
376
|
-
args.push('-e', `HOME=${CONTAINER_HOME}`);
|
|
377
|
-
// Map config dir to container-internal path
|
|
378
|
-
const hostConfigDir = (0, config_manager_1.getConfigDir)();
|
|
379
|
-
const home = os.homedir();
|
|
380
|
-
const relativeToHome = path.relative(home, hostConfigDir);
|
|
381
|
-
const isUnderHome = !relativeToHome.startsWith('..');
|
|
382
|
-
const containerConfigDir = isUnderHome
|
|
383
|
-
? path.posix.join(CONTAINER_HOME, toPosixRelative(relativeToHome))
|
|
384
|
-
: `/workspace/.config/ai-support-agent`;
|
|
385
|
-
for (const key of PASSTHROUGH_ENV_VARS) {
|
|
386
|
-
if (process.env[key]) {
|
|
387
|
-
if (key === 'AI_SUPPORT_AGENT_CONFIG_DIR') {
|
|
388
|
-
args.push('-e', `${key}=${containerConfigDir}`);
|
|
389
|
-
}
|
|
390
|
-
else {
|
|
391
|
-
args.push('-e', `${key}=${process.env[key]}`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
// Pass project directory mappings so agent uses container-internal paths
|
|
396
|
-
if (projectMappings.length > 0) {
|
|
397
|
-
// Format: projectCode=containerDir;projectCode2=containerDir2
|
|
398
|
-
const mapping = projectMappings
|
|
399
|
-
.map((m) => `${m.projectCode}=${m.containerDir}`)
|
|
400
|
-
.join(';');
|
|
401
|
-
args.push('-e', `AI_SUPPORT_AGENT_PROJECT_DIR_MAP=${mapping}`);
|
|
402
|
-
}
|
|
403
|
-
return args;
|
|
404
|
-
}
|
|
54
|
+
const docker_utils_1 = require("./docker-utils");
|
|
55
|
+
const version_manager_1 = require("./version-manager");
|
|
56
|
+
const dockerfile_sync_1 = require("./dockerfile-sync");
|
|
57
|
+
const volume_mount_builder_1 = require("./volume-mount-builder");
|
|
58
|
+
const docker_supervisor_1 = require("./docker-supervisor");
|
|
59
|
+
const update_handler_1 = require("./update-handler");
|
|
405
60
|
function buildContainerArgs(opts) {
|
|
406
61
|
const args = ['ai-support-agent', 'start', '--no-docker'];
|
|
407
62
|
if (opts.token) {
|
|
@@ -430,626 +85,13 @@ function buildContainerArgs(opts) {
|
|
|
430
85
|
}
|
|
431
86
|
return args;
|
|
432
87
|
}
|
|
433
|
-
let cachedInstalledVersion = null;
|
|
434
|
-
/**
|
|
435
|
-
* Get the currently installed version of @ai-support-agent/cli from npm global.
|
|
436
|
-
* Falls back to AGENT_VERSION if npm query fails.
|
|
437
|
-
* Result is cached after the first call.
|
|
438
|
-
*
|
|
439
|
-
* NOTE: Must only be called from host-side code (i.e. runInDocker).
|
|
440
|
-
* Inside a Docker container the process runs with --no-docker, so
|
|
441
|
-
* ensureImage() / getInstalledVersion() are never reached.
|
|
442
|
-
*/
|
|
443
|
-
function getInstalledVersion() {
|
|
444
|
-
if (cachedInstalledVersion !== null)
|
|
445
|
-
return cachedInstalledVersion;
|
|
446
|
-
try {
|
|
447
|
-
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
448
|
-
const output = (0, child_process_1.execFileSync)(npmCmd, ['list', '-g', '--json', '--depth=0'], {
|
|
449
|
-
encoding: 'utf-8',
|
|
450
|
-
timeout: 10_000,
|
|
451
|
-
});
|
|
452
|
-
const parsed = JSON.parse(output);
|
|
453
|
-
const version = parsed.dependencies?.['@ai-support-agent/cli']?.version;
|
|
454
|
-
if (version && (0, version_1.isValidVersion)(version)) {
|
|
455
|
-
cachedInstalledVersion = version;
|
|
456
|
-
return version;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
catch {
|
|
460
|
-
// npm list failed — fall back to compile-time version
|
|
461
|
-
}
|
|
462
|
-
cachedInstalledVersion = constants_1.AGENT_VERSION;
|
|
463
|
-
return constants_1.AGENT_VERSION;
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Reset the cached installed version (for testing).
|
|
467
|
-
*/
|
|
468
|
-
function resetInstalledVersionCache() {
|
|
469
|
-
cachedInstalledVersion = null;
|
|
470
|
-
}
|
|
471
|
-
function ensureImage(customDockerfile) {
|
|
472
|
-
const installedVersion = getInstalledVersion();
|
|
473
|
-
// Use the installed version if it is newer than the compile-time version
|
|
474
|
-
const version = (0, version_1.isNewerVersion)(constants_1.AGENT_VERSION, installedVersion) ? installedVersion : constants_1.AGENT_VERSION;
|
|
475
|
-
if (!imageExists(version)) {
|
|
476
|
-
buildImage(version, customDockerfile);
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
logger_1.logger.info((0, i18n_1.t)('docker.imageFound', { version }));
|
|
480
|
-
}
|
|
481
|
-
return version;
|
|
482
|
-
}
|
|
483
|
-
function dockerLogin() {
|
|
484
|
-
logger_1.logger.info((0, i18n_1.t)('docker.loginStep1'));
|
|
485
|
-
console.log('');
|
|
486
|
-
console.log(' claude setup-token');
|
|
487
|
-
console.log('');
|
|
488
|
-
logger_1.logger.info((0, i18n_1.t)('docker.loginStep2'));
|
|
489
|
-
console.log('');
|
|
490
|
-
console.log(' export CLAUDE_CODE_OAUTH_TOKEN=<token>');
|
|
491
|
-
console.log(' ai-support-agent start');
|
|
492
|
-
console.log('');
|
|
493
|
-
logger_1.logger.info((0, i18n_1.t)('docker.loginStep3'));
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* Called on the host after a container exits with DOCKER_UPDATE_EXIT_CODE.
|
|
497
|
-
* Reads the new version from a project-specific config dir (written by the container),
|
|
498
|
-
* installs it on the host with npm, then re-execs the process so ensureImage()
|
|
499
|
-
* picks up the newly installed version and rebuilds the Docker image.
|
|
500
|
-
*/
|
|
501
|
-
async function installUpdateAndRestart(projectConfigDir) {
|
|
502
|
-
let newVersion;
|
|
503
|
-
// First try project-specific config dir, fall back to global config dir
|
|
504
|
-
const searchDirs = projectConfigDir
|
|
505
|
-
? [projectConfigDir, (0, config_manager_1.getConfigDir)()]
|
|
506
|
-
: [(0, config_manager_1.getConfigDir)()];
|
|
507
|
-
for (const dir of searchDirs) {
|
|
508
|
-
try {
|
|
509
|
-
const versionFile = path.join(dir, 'update-version.json');
|
|
510
|
-
const raw = fs.readFileSync(versionFile, 'utf-8');
|
|
511
|
-
const parsed = JSON.parse(raw);
|
|
512
|
-
if (parsed.version && (0, version_1.isValidVersion)(parsed.version)) {
|
|
513
|
-
newVersion = parsed.version;
|
|
514
|
-
}
|
|
515
|
-
fs.unlinkSync(versionFile);
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
catch {
|
|
519
|
-
// File does not exist in this dir — try next
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (newVersion) {
|
|
523
|
-
logger_1.logger.info(`[docker] Installing @ai-support-agent/cli@${newVersion} on host...`);
|
|
524
|
-
// Always use 'global' install method on the host side regardless of how the
|
|
525
|
-
// host process was originally launched (it may be detected as 'local' in
|
|
526
|
-
// service/systemd environments where the script path is under node_modules).
|
|
527
|
-
const result = await (0, update_checker_1.performUpdate)(newVersion, 'global');
|
|
528
|
-
if (!result.success) {
|
|
529
|
-
logger_1.logger.warn(`[docker] Host npm install failed: ${result.error ?? 'unknown'}. Proceeding with existing version.`);
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
// Invalidate the cached installed version so ensureImage() re-reads it
|
|
533
|
-
resetInstalledVersionCache(); // defined in this file
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
(0, update_checker_1.reExecProcess)();
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* Copy the bundled Dockerfile (and entrypoint.sh) to the config directory
|
|
540
|
-
* on first run so users can customise it.
|
|
541
|
-
* Does NOT overwrite an existing file — user edits are preserved.
|
|
542
|
-
* Exported for testing.
|
|
543
|
-
*/
|
|
544
|
-
function syncDockerfileToConfigDir() {
|
|
545
|
-
const configDir = (0, config_manager_1.getConfigDir)();
|
|
546
|
-
const destDockerfile = path.join(configDir, 'Dockerfile');
|
|
547
|
-
if (fs.existsSync(destDockerfile))
|
|
548
|
-
return; // preserve existing file
|
|
549
|
-
try {
|
|
550
|
-
const srcDockerfile = (0, dockerfile_path_1.getDockerfilePath)();
|
|
551
|
-
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
552
|
-
fs.copyFileSync(srcDockerfile, destDockerfile);
|
|
553
|
-
// Also copy entrypoint.sh which the bundled Dockerfile references via COPY
|
|
554
|
-
const srcEntrypoint = path.join((0, dockerfile_path_1.getDockerContextDir)(), 'docker', 'entrypoint.sh');
|
|
555
|
-
const destEntrypoint = path.join(configDir, 'docker', 'entrypoint.sh');
|
|
556
|
-
if (fs.existsSync(srcEntrypoint)) {
|
|
557
|
-
fs.mkdirSync(path.dirname(destEntrypoint), { recursive: true });
|
|
558
|
-
fs.copyFileSync(srcEntrypoint, destEntrypoint);
|
|
559
|
-
}
|
|
560
|
-
logger_1.logger.info((0, i18n_1.t)('docker.dockerfileSynced', { path: destDockerfile }));
|
|
561
|
-
}
|
|
562
|
-
catch (err) {
|
|
563
|
-
logger_1.logger.warn((0, i18n_1.t)('docker.dockerfileSyncFailed', { message: err instanceof Error ? err.message : String(err) }));
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
// ---------------------------------------------------------------------------
|
|
567
|
-
// Per-project volume mount helpers
|
|
568
|
-
// ---------------------------------------------------------------------------
|
|
569
|
-
/**
|
|
570
|
-
* Build volume mounts and env args for a single project container.
|
|
571
|
-
* Each project gets its own isolated config dir under ~/.ai-support-agent/projects/{tenantCode}/{projectCode}/.ai-support-agent/
|
|
572
|
-
*/
|
|
573
|
-
function buildProjectVolumeMounts(project, projectConfigHostDir) {
|
|
574
|
-
const home = os.homedir();
|
|
575
|
-
const mounts = [];
|
|
576
|
-
const envArgs = [];
|
|
577
|
-
// Claude Code OAuth tokens and config — mount to container home
|
|
578
|
-
const claudeDir = path.join(home, '.claude');
|
|
579
|
-
if (fs.existsSync(claudeDir)) {
|
|
580
|
-
mounts.push('-v', `${claudeDir}:${path.posix.join(CONTAINER_HOME, '.claude')}:rw`);
|
|
581
|
-
}
|
|
582
|
-
const claudeJson = path.join(home, '.claude.json');
|
|
583
|
-
if (fs.existsSync(claudeJson)) {
|
|
584
|
-
mounts.push('-v', `${claudeJson}:${path.posix.join(CONTAINER_HOME, '.claude.json')}:rw`);
|
|
585
|
-
}
|
|
586
|
-
// Per-project isolated config dir — mounts to /home/node/.ai-support-agent inside container
|
|
587
|
-
const containerConfigDir = path.posix.join(CONTAINER_HOME, '.ai-support-agent');
|
|
588
|
-
fs.mkdirSync(projectConfigHostDir, { recursive: true, mode: 0o700 });
|
|
589
|
-
mounts.push('-v', `${projectConfigHostDir}:${containerConfigDir}:rw`);
|
|
590
|
-
// Standard env vars for per-project container
|
|
591
|
-
envArgs.push('-e', 'AI_SUPPORT_AGENT_IN_DOCKER=1');
|
|
592
|
-
envArgs.push('-e', `HOME=${CONTAINER_HOME}`);
|
|
593
|
-
envArgs.push('-e', `AI_SUPPORT_AGENT_CONFIG_DIR=${containerConfigDir}`);
|
|
594
|
-
// Pass token and apiUrl per-project
|
|
595
|
-
if (project.token) {
|
|
596
|
-
envArgs.push('-e', `AI_SUPPORT_AGENT_TOKEN=${project.token}`);
|
|
597
|
-
}
|
|
598
|
-
if (project.apiUrl) {
|
|
599
|
-
// Replace localhost/127.0.0.1 with host.docker.internal so the container can reach the host
|
|
600
|
-
const containerApiUrl = project.apiUrl.replace(/^(https?:\/\/)(localhost|127\.0\.0\.1)(:\d+)?/, (_, scheme, _host, port) => `${scheme}host.docker.internal${port ?? ''}`);
|
|
601
|
-
envArgs.push('-e', `AI_SUPPORT_AGENT_API_URL=${containerApiUrl}`);
|
|
602
|
-
}
|
|
603
|
-
// Pass ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN if set
|
|
604
|
-
for (const key of ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN']) {
|
|
605
|
-
if (process.env[key]) {
|
|
606
|
-
envArgs.push('-e', `${key}=${process.env[key]}`);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
// Pass host timezone so container clocks match the host by default.
|
|
610
|
-
// The per-project Dockerfile can override this with ENV TZ= if a custom timezone is configured.
|
|
611
|
-
const hostTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
612
|
-
envArgs.push('-e', `TZ=${hostTz}`);
|
|
613
|
-
// Project dir mapping: single project only
|
|
614
|
-
const containerProjectDir = `${CONTAINER_PROJECTS_BASE}/${project.projectCode}`;
|
|
615
|
-
const blockedPrefixes = [...security_1.BLOCKED_PATH_PREFIXES, ...(0, security_1.getSensitiveHomePaths)()];
|
|
616
|
-
if (project.projectDir && fs.existsSync(project.projectDir)) {
|
|
617
|
-
let resolved;
|
|
618
|
-
try {
|
|
619
|
-
resolved = fs.realpathSync(project.projectDir);
|
|
620
|
-
const isBlocked = blockedPrefixes.some((prefix) => {
|
|
621
|
-
const prefixWithoutSlash = prefix.replace(/\/$/, '');
|
|
622
|
-
return resolved === prefixWithoutSlash || resolved.startsWith(prefix);
|
|
623
|
-
});
|
|
624
|
-
if (!isBlocked) {
|
|
625
|
-
mounts.push('-v', `${project.projectDir}:${containerProjectDir}:rw`);
|
|
626
|
-
envArgs.push('-e', `AI_SUPPORT_AGENT_PROJECT_DIR_MAP=${project.projectCode}=${containerProjectDir}`);
|
|
627
|
-
}
|
|
628
|
-
else {
|
|
629
|
-
logger_1.logger.warn(`[docker] Skipping blocked path for volume mount: ${project.projectDir}`);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
catch {
|
|
633
|
-
logger_1.logger.warn(`[docker] Cannot resolve path, skipping: ${project.projectDir}`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return { mounts, envArgs };
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Migrate per-project config directory from the legacy layout to the current layout.
|
|
640
|
-
*
|
|
641
|
-
* Legacy: ~/.ai-support-agent/projects/{projectCode}/
|
|
642
|
-
* Current: ~/.ai-support-agent/projects/{tenantCode}/{projectCode}/
|
|
643
|
-
*
|
|
644
|
-
* Older agent versions stored project data without a tenantCode path segment.
|
|
645
|
-
* This function moves the directory once so the current code finds it in the right place.
|
|
646
|
-
*/
|
|
647
|
-
function migrateProjectConfigDir(project) {
|
|
648
|
-
const configBase = path.join((0, config_manager_1.getConfigDir)(), 'projects');
|
|
649
|
-
const legacyDir = path.join(configBase, project.projectCode);
|
|
650
|
-
const newDir = path.join(configBase, project.tenantCode, project.projectCode);
|
|
651
|
-
if (!fs.existsSync(legacyDir))
|
|
652
|
-
return; // nothing to migrate
|
|
653
|
-
if (fs.existsSync(newDir))
|
|
654
|
-
return; // already migrated
|
|
655
|
-
try {
|
|
656
|
-
fs.mkdirSync(path.join(configBase, project.tenantCode), { recursive: true, mode: 0o700 });
|
|
657
|
-
fs.renameSync(legacyDir, newDir);
|
|
658
|
-
logger_1.logger.info(`[docker] Migrated project config dir: ${legacyDir} → ${newDir}`);
|
|
659
|
-
}
|
|
660
|
-
catch (err) {
|
|
661
|
-
logger_1.logger.warn(`[docker] Failed to migrate project config dir for ${project.projectCode}: ${err instanceof Error ? err.message : String(err)}`);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
|
-
* Get the host-side per-project config directory.
|
|
666
|
-
* Located at: ~/.ai-support-agent/projects/{tenantCode}/{projectCode}/.ai-support-agent/
|
|
667
|
-
*/
|
|
668
|
-
function getProjectConfigHostDir(project) {
|
|
669
|
-
return path.join((0, config_manager_1.getConfigDir)(), 'projects', project.tenantCode, project.projectCode, '.ai-support-agent');
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Manages one Docker container per project.
|
|
673
|
-
* Spawned from runInDocker() when multiple projects are configured.
|
|
674
|
-
*/
|
|
675
|
-
class DockerSupervisor {
|
|
676
|
-
handles = new Map();
|
|
677
|
-
updating = false;
|
|
678
|
-
opts;
|
|
679
|
-
version;
|
|
680
|
-
onAllStopped;
|
|
681
|
-
defaultAgentId;
|
|
682
|
-
/** Per-project agentId updated when the container registers with the API. */
|
|
683
|
-
projectAgentIds = new Map();
|
|
684
|
-
sigintHandler;
|
|
685
|
-
sigtermHandler;
|
|
686
|
-
constructor(version, opts) {
|
|
687
|
-
this.version = version;
|
|
688
|
-
this.opts = opts;
|
|
689
|
-
this.defaultAgentId = opts.agentId;
|
|
690
|
-
}
|
|
691
|
-
projectKey(project) {
|
|
692
|
-
return `${project.tenantCode}/${project.projectCode}`;
|
|
693
|
-
}
|
|
694
|
-
getProjectAgentId(project) {
|
|
695
|
-
return this.projectAgentIds.get(this.projectKey(project)) ?? this.defaultAgentId;
|
|
696
|
-
}
|
|
697
|
-
setProjectAgentId(project, agentId) {
|
|
698
|
-
this.projectAgentIds.set(this.projectKey(project), agentId);
|
|
699
|
-
}
|
|
700
|
-
createProjectApiClient(project) {
|
|
701
|
-
return new api_client_1.ApiClient(project.apiUrl, project.token);
|
|
702
|
-
}
|
|
703
|
-
start(projects, onStop) {
|
|
704
|
-
this.onAllStopped = onStop;
|
|
705
|
-
for (const project of projects) {
|
|
706
|
-
migrateProjectConfigDir(project);
|
|
707
|
-
this.spawnProject(project);
|
|
708
|
-
}
|
|
709
|
-
// Setup shutdown handlers
|
|
710
|
-
let shuttingDown = false;
|
|
711
|
-
const shutdown = () => {
|
|
712
|
-
if (shuttingDown)
|
|
713
|
-
return;
|
|
714
|
-
shuttingDown = true;
|
|
715
|
-
// Set updating flag so the close handler does not call process.exit again
|
|
716
|
-
this.updating = true;
|
|
717
|
-
if (this.sigintHandler)
|
|
718
|
-
process.removeListener('SIGINT', this.sigintHandler);
|
|
719
|
-
if (this.sigtermHandler)
|
|
720
|
-
process.removeListener('SIGTERM', this.sigtermHandler);
|
|
721
|
-
(0, pid_manager_1.removePidFile)();
|
|
722
|
-
logger_1.logger.info((0, i18n_1.t)('runner.shuttingDown'));
|
|
723
|
-
const closedPromises = [...this.handles.values()].map((h) => h.closedPromise);
|
|
724
|
-
this.stopAll();
|
|
725
|
-
onStop?.();
|
|
726
|
-
const shutdownTimer = setTimeout(() => {
|
|
727
|
-
logger_1.logger.warn('[docker] Shutdown timed out waiting for log flush; forcing exit');
|
|
728
|
-
process.exit(0);
|
|
729
|
-
}, this.opts.shutdownTimeoutMs ?? 10_000);
|
|
730
|
-
void Promise.all(closedPromises).then(() => {
|
|
731
|
-
clearTimeout(shutdownTimer);
|
|
732
|
-
process.exit(0);
|
|
733
|
-
});
|
|
734
|
-
};
|
|
735
|
-
this.sigintHandler = () => { logger_1.logger.info('[docker] SIGINT received, shutting down...'); shutdown(); };
|
|
736
|
-
this.sigtermHandler = () => { logger_1.logger.info('[docker] SIGTERM received, shutting down...'); shutdown(); };
|
|
737
|
-
process.on('SIGINT', this.sigintHandler);
|
|
738
|
-
process.on('SIGTERM', this.sigtermHandler);
|
|
739
|
-
// On macOS/Linux, Ctrl+C may not deliver SIGINT to Node.js when the terminal
|
|
740
|
-
// is in cooked mode and child processes share the process group. As a fallback,
|
|
741
|
-
// read \x03 (ETX) directly from stdin when it is a TTY in raw mode.
|
|
742
|
-
/* istanbul ignore next -- TTY-only path, not testable in CI */
|
|
743
|
-
if (process.stdin.isTTY) {
|
|
744
|
-
process.stdin.setRawMode(true);
|
|
745
|
-
process.stdin.resume();
|
|
746
|
-
process.stdin.on('data', (chunk) => {
|
|
747
|
-
if (chunk[0] === 0x03) { // Ctrl+C
|
|
748
|
-
shutdown();
|
|
749
|
-
}
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
getImageTag(project) {
|
|
754
|
-
const projectTag = (0, dockerfile_path_1.getProjectImageTag)(project.tenantCode, project.projectCode, this.version);
|
|
755
|
-
try {
|
|
756
|
-
(0, child_process_1.execFileSync)('docker', ['image', 'inspect', projectTag], { stdio: 'ignore' });
|
|
757
|
-
return projectTag;
|
|
758
|
-
}
|
|
759
|
-
catch /* istanbul ignore next */ {
|
|
760
|
-
return `${IMAGE_NAME}:${this.version}`;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
async rebuildAndRestart(project, projectConfigHostDir, forceIfDockerfileExists = false) {
|
|
764
|
-
const rebuildMarker = path.join(projectConfigHostDir, 'docker-rebuild-needed');
|
|
765
|
-
const projectDockerfile = path.join(projectConfigHostDir, 'Dockerfile');
|
|
766
|
-
const hasMarker = fs.existsSync(rebuildMarker);
|
|
767
|
-
const shouldBuild = hasMarker || (forceIfDockerfileExists && fs.existsSync(projectDockerfile));
|
|
768
|
-
if (shouldBuild) {
|
|
769
|
-
if (hasMarker)
|
|
770
|
-
fs.unlinkSync(rebuildMarker);
|
|
771
|
-
// Load the registered agentId before building so build logs are stored under
|
|
772
|
-
// the correct agentId (the one shown in the Web UI), not the host agentId.
|
|
773
|
-
const registeredAgentIdPath = path.join(projectConfigHostDir, 'docker-registered-agent-id');
|
|
774
|
-
if (fs.existsSync(registeredAgentIdPath)) {
|
|
775
|
-
const registeredId = fs.readFileSync(registeredAgentIdPath, 'utf-8').trim();
|
|
776
|
-
if (registeredId && registeredId !== this.getProjectAgentId(project)) {
|
|
777
|
-
this.setProjectAgentId(project, registeredId);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
// The container writes Dockerfile into its configDir root, which maps to projectConfigHostDir on host.
|
|
781
|
-
if (fs.existsSync(projectDockerfile)) {
|
|
782
|
-
try {
|
|
783
|
-
await buildProjectImage(project.tenantCode, project.projectCode, this.version, projectDockerfile, this.createProjectApiClient(project), this.getProjectAgentId(project));
|
|
784
|
-
// Copy the docker-customization-hash file written by the container so the
|
|
785
|
-
// next container startup knows the current customization was already built.
|
|
786
|
-
const srcHash = path.join(projectConfigHostDir, 'docker-customization-hash');
|
|
787
|
-
const dstHash = path.join(projectConfigHostDir, 'docker-built-hash');
|
|
788
|
-
if (fs.existsSync(srcHash)) {
|
|
789
|
-
fs.copyFileSync(srcHash, dstHash);
|
|
790
|
-
}
|
|
791
|
-
// Clear any previous build error so the container can report success
|
|
792
|
-
const buildErrorPath = path.join(projectConfigHostDir, 'docker-build-error');
|
|
793
|
-
/* istanbul ignore next */
|
|
794
|
-
if (fs.existsSync(buildErrorPath)) {
|
|
795
|
-
fs.unlinkSync(buildErrorPath);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
catch (err) {
|
|
799
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
800
|
-
logger_1.logger.error(`[docker] Image build failed: ${errorMsg}`);
|
|
801
|
-
logger_1.logger.warn(`[docker] Container ${this.projectKey(project)} will start with previous image due to build failure.`);
|
|
802
|
-
// Write the error message so the container can report it via heartbeat
|
|
803
|
-
// Truncate to 3000 chars to avoid DynamoDB item size limits
|
|
804
|
-
const buildErrorPath = path.join(projectConfigHostDir, 'docker-build-error');
|
|
805
|
-
const truncatedError = errorMsg.length > 3000 ? errorMsg.substring(0, 3000) + '...(truncated)' : errorMsg;
|
|
806
|
-
/* istanbul ignore next */
|
|
807
|
-
try {
|
|
808
|
-
fs.writeFileSync(buildErrorPath, truncatedError, 'utf-8');
|
|
809
|
-
}
|
|
810
|
-
catch (writeErr) {
|
|
811
|
-
logger_1.logger.warn(`[docker] Failed to write build error file: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}`);
|
|
812
|
-
}
|
|
813
|
-
// Write docker-built-hash even on failure so the next startup does not
|
|
814
|
-
// attempt the same failed build again. The container starts with the
|
|
815
|
-
// previous (base) image until the configuration is fixed and changed.
|
|
816
|
-
const srcHash = path.join(projectConfigHostDir, 'docker-customization-hash');
|
|
817
|
-
const dstHash = path.join(projectConfigHostDir, 'docker-built-hash');
|
|
818
|
-
if (fs.existsSync(srcHash)) {
|
|
819
|
-
fs.copyFileSync(srcHash, dstHash);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
this.spawnProject(project);
|
|
825
|
-
}
|
|
826
|
-
spawnProject(project) {
|
|
827
|
-
const key = this.projectKey(project);
|
|
828
|
-
const projectConfigHostDir = getProjectConfigHostDir(project);
|
|
829
|
-
// Pre-startup hash check: if docker-customization-hash !== docker-built-hash,
|
|
830
|
-
// rebuild before starting the container (handles the case where config changed while agent was stopped)
|
|
831
|
-
const customizationHashPath = path.join(projectConfigHostDir, 'docker-customization-hash');
|
|
832
|
-
const builtHashPath = path.join(projectConfigHostDir, 'docker-built-hash');
|
|
833
|
-
if (fs.existsSync(customizationHashPath) && fs.existsSync(builtHashPath)) {
|
|
834
|
-
const customizationHash = fs.readFileSync(customizationHashPath, 'utf-8').trim();
|
|
835
|
-
const builtHash = fs.readFileSync(builtHashPath, 'utf-8').trim();
|
|
836
|
-
if (customizationHash !== builtHash) {
|
|
837
|
-
logger_1.logger.info(`[docker] Pre-startup hash mismatch for ${key}, rebuilding before start...`);
|
|
838
|
-
void this.rebuildAndRestart(project, projectConfigHostDir, true);
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
// Load the server-assigned agentId written by the container after registration.
|
|
843
|
-
// This ensures logs are stored under the same agentId shown in the Web UI.
|
|
844
|
-
const registeredAgentIdPath = path.join(projectConfigHostDir, 'docker-registered-agent-id');
|
|
845
|
-
if (fs.existsSync(registeredAgentIdPath)) {
|
|
846
|
-
const registeredId = fs.readFileSync(registeredAgentIdPath, 'utf-8').trim();
|
|
847
|
-
if (registeredId && registeredId !== this.getProjectAgentId(project)) {
|
|
848
|
-
logger_1.logger.info(`[docker] Using registered agentId for ${key}: ${registeredId}`);
|
|
849
|
-
this.setProjectAgentId(project, registeredId);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
const { mounts, envArgs } = buildProjectVolumeMounts(project, projectConfigHostDir);
|
|
853
|
-
const containerArgs = [
|
|
854
|
-
'ai-support-agent', 'start', '--no-docker',
|
|
855
|
-
'--project', key,
|
|
856
|
-
];
|
|
857
|
-
if (this.opts.pollInterval !== undefined) {
|
|
858
|
-
containerArgs.push('--poll-interval', String(this.opts.pollInterval));
|
|
859
|
-
}
|
|
860
|
-
if (this.opts.heartbeatInterval !== undefined) {
|
|
861
|
-
containerArgs.push('--heartbeat-interval', String(this.opts.heartbeatInterval));
|
|
862
|
-
}
|
|
863
|
-
if (this.opts.verbose) {
|
|
864
|
-
containerArgs.push('--verbose');
|
|
865
|
-
}
|
|
866
|
-
if (this.opts.autoUpdate === false) {
|
|
867
|
-
containerArgs.push('--no-auto-update');
|
|
868
|
-
}
|
|
869
|
-
if (this.opts.updateChannel) {
|
|
870
|
-
containerArgs.push('--update-channel', this.opts.updateChannel);
|
|
871
|
-
}
|
|
872
|
-
// No -i/-t flags: the container does not need stdin. stdout/stderr are
|
|
873
|
-
// captured via pipe. Shutdown is handled exclusively via docker stop.
|
|
874
|
-
const imageTag = this.getImageTag(project);
|
|
875
|
-
const containerName = buildContainerName(project.tenantCode, project.projectCode, this.opts.agentId);
|
|
876
|
-
removeStaleContainer(containerName);
|
|
877
|
-
const cidFile = path.join(os.tmpdir(), `ai-support-agent-${project.tenantCode}-${project.projectCode}-${Date.now()}.cid`);
|
|
878
|
-
const dockerArgs = [
|
|
879
|
-
'run', '--rm', '--name', containerName, '--cidfile', cidFile,
|
|
880
|
-
...(process.getuid ? ['--user', `${process.getuid()}:${process.getgid()}`] : []),
|
|
881
|
-
...mounts,
|
|
882
|
-
...buildDevMounts(),
|
|
883
|
-
...envArgs,
|
|
884
|
-
imageTag,
|
|
885
|
-
...containerArgs,
|
|
886
|
-
];
|
|
887
|
-
const projectColor = (0, logger_1.getProjectColor)(key);
|
|
888
|
-
const colorReset = '\x1b[0m';
|
|
889
|
-
const logPrefix = `${projectColor}[${key}]${colorReset} `;
|
|
890
|
-
logger_1.logger.info(`[docker] Starting container for project: ${key}`);
|
|
891
|
-
const child = (0, child_process_1.spawn)('docker', dockerArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
892
|
-
let resolveClosed;
|
|
893
|
-
const closedPromise = new Promise((resolve) => { resolveClosed = resolve; });
|
|
894
|
-
const handle = {
|
|
895
|
-
project,
|
|
896
|
-
child,
|
|
897
|
-
version: this.version,
|
|
898
|
-
closeHandled: false,
|
|
899
|
-
closedPromise,
|
|
900
|
-
resolveClosed,
|
|
901
|
-
cidFile,
|
|
902
|
-
};
|
|
903
|
-
this.handles.set(key, handle);
|
|
904
|
-
// Stream container stdout/stderr to host terminal and API for real-time log viewing
|
|
905
|
-
// Use project-specific ApiClient so the token matches the projectCode in the request
|
|
906
|
-
const projectApiClient = this.createProjectApiClient(project);
|
|
907
|
-
if (projectApiClient) {
|
|
908
|
-
const sessionId = makeSessionId();
|
|
909
|
-
let seq = 0;
|
|
910
|
-
let fullLog = '';
|
|
911
|
-
let logTruncated = false;
|
|
912
|
-
let buf = '';
|
|
913
|
-
const apiClient = projectApiClient;
|
|
914
|
-
// Use a getter so that if the container writes docker-registered-agent-id after startup,
|
|
915
|
-
// subsequent log chunks are stored under the correct server-assigned agentId.
|
|
916
|
-
const supervisor = this;
|
|
917
|
-
const getAgentId = () => supervisor.getProjectAgentId(project) ?? '';
|
|
918
|
-
// Watch for the registered agentId file written by the container after registration
|
|
919
|
-
const noopWatcher = { close: () => undefined };
|
|
920
|
-
let registeredIdWatcher = noopWatcher;
|
|
921
|
-
try {
|
|
922
|
-
registeredIdWatcher = fs.watch(projectConfigHostDir, (eventType, filename) => {
|
|
923
|
-
if (filename === 'docker-registered-agent-id' && (eventType === 'rename' || eventType === 'change')) {
|
|
924
|
-
try {
|
|
925
|
-
const newId = fs.readFileSync(registeredAgentIdPath, 'utf-8').trim();
|
|
926
|
-
const currentId = supervisor.getProjectAgentId(project);
|
|
927
|
-
if (newId && newId !== currentId) {
|
|
928
|
-
logger_1.logger.info(`[docker] Container registered with agentId: ${newId} (was: ${currentId})`);
|
|
929
|
-
supervisor.setProjectAgentId(project, newId);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
catch {
|
|
933
|
-
// File may not exist yet if rename event fires before write completes
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
catch {
|
|
939
|
-
// Directory watch may fail if projectConfigHostDir doesn't exist yet — ignore
|
|
940
|
-
}
|
|
941
|
-
const flush = async () => {
|
|
942
|
-
if (!buf)
|
|
943
|
-
return;
|
|
944
|
-
const text = buf;
|
|
945
|
-
buf = '';
|
|
946
|
-
if (!logTruncated) {
|
|
947
|
-
if (fullLog.length + text.length <= MAX_SESSION_LOG_BYTES) {
|
|
948
|
-
fullLog += text;
|
|
949
|
-
}
|
|
950
|
-
else {
|
|
951
|
-
const remaining = MAX_SESSION_LOG_BYTES - fullLog.length;
|
|
952
|
-
fullLog += remaining > 0 ? text.slice(0, remaining) : '';
|
|
953
|
-
logTruncated = true;
|
|
954
|
-
logger_1.logger.warn(`[docker] Container log for ${key} exceeded 2 MB limit; remaining output will not be saved to S3`);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
await apiClient.submitLogChunk({ agentId: getAgentId(), projectCode: project.projectCode, logType: 'container', sessionId, seq: ++seq, text })
|
|
958
|
-
.catch((e) => logger_1.logger.warn(`[docker] log chunk failed: ${e}`));
|
|
959
|
-
};
|
|
960
|
-
const flushTimer = setInterval(() => { void flush(); }, 1_000).unref();
|
|
961
|
-
const writeStdout = (0, logger_1.makeLinePrefixer)(logPrefix, (s) => process.stdout.write(s));
|
|
962
|
-
const writeStderr = (0, logger_1.makeLinePrefixer)(logPrefix, (s) => process.stderr.write(s));
|
|
963
|
-
child.stdout?.on('data', (d) => { const t = d.toString(); writeStdout(t); buf += t; });
|
|
964
|
-
child.stderr?.on('data', (d) => { const t = d.toString(); writeStderr(t); buf += t; });
|
|
965
|
-
child.on('close', () => {
|
|
966
|
-
registeredIdWatcher.close();
|
|
967
|
-
clearInterval(flushTimer);
|
|
968
|
-
void flush().then(() => {
|
|
969
|
-
if (fullLog) {
|
|
970
|
-
void apiClient.saveSessionLog({ agentId: getAgentId(), projectCode: project.projectCode, logType: 'container', sessionId, content: fullLog })
|
|
971
|
-
.catch((e) => logger_1.logger.warn(`[docker] S3 upload failed: ${e}`))
|
|
972
|
-
.finally(() => { handle.resolveClosed(); });
|
|
973
|
-
}
|
|
974
|
-
else {
|
|
975
|
-
handle.resolveClosed();
|
|
976
|
-
}
|
|
977
|
-
}).catch(/* istanbul ignore next */ () => { handle.resolveClosed(); }).catch(/* istanbul ignore next */ () => { handle.resolveClosed(); });
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
child.on('error', (err) => {
|
|
981
|
-
logger_1.logger.error(`[docker] Container error for ${key}: ${err.message}`);
|
|
982
|
-
});
|
|
983
|
-
child.on('close', (code) => {
|
|
984
|
-
if (handle.closeHandled)
|
|
985
|
-
return;
|
|
986
|
-
handle.closeHandled = true;
|
|
987
|
-
this.handles.delete(key);
|
|
988
|
-
// Clean up cidfile
|
|
989
|
-
try {
|
|
990
|
-
fs.unlinkSync(handle.cidFile);
|
|
991
|
-
}
|
|
992
|
-
catch { /* ignore */ }
|
|
993
|
-
if (code === constants_1.DOCKER_UPDATE_EXIT_CODE && !this.updating) {
|
|
994
|
-
this.updating = true;
|
|
995
|
-
logger_1.logger.info(`[docker] Container ${key} exited for update. Stopping all containers and rebuilding...`);
|
|
996
|
-
this.stopAll();
|
|
997
|
-
void installUpdateAndRestart(projectConfigHostDir).catch((err) => {
|
|
998
|
-
logger_1.logger.error(`[docker] Update failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
999
|
-
process.exit(1);
|
|
1000
|
-
});
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
if (code === constants_1.DOCKER_RESTART_EXIT_CODE && !this.updating) {
|
|
1004
|
-
logger_1.logger.info(`[docker] Container ${key} requested restart. Rebuilding image if needed...`);
|
|
1005
|
-
void this.rebuildAndRestart(project, projectConfigHostDir, true).catch((err) => {
|
|
1006
|
-
logger_1.logger.error(`[docker] Restart failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1007
|
-
});
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
if (this.handles.size === 0 && !this.updating) {
|
|
1011
|
-
// All containers have exited cleanly
|
|
1012
|
-
this.onAllStopped?.();
|
|
1013
|
-
process.exit(code ?? 0);
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
stopAll() {
|
|
1018
|
-
for (const [key, handle] of this.handles) {
|
|
1019
|
-
if (!handle.closeHandled) {
|
|
1020
|
-
logger_1.logger.info(`[docker] Stopping container for project: ${key}`);
|
|
1021
|
-
// Try docker stop via cidfile (more reliable than SIGTERM to docker run process).
|
|
1022
|
-
// Use spawn (non-blocking) so we don't stall the Node.js event loop during shutdown.
|
|
1023
|
-
let stopped = false;
|
|
1024
|
-
try {
|
|
1025
|
-
/* istanbul ignore next */
|
|
1026
|
-
if (fs.existsSync(handle.cidFile)) {
|
|
1027
|
-
const containerId = fs.readFileSync(handle.cidFile, 'utf-8').trim();
|
|
1028
|
-
/* istanbul ignore next */
|
|
1029
|
-
if (containerId) {
|
|
1030
|
-
(0, child_process_1.spawn)('docker', ['stop', '--time', '5', containerId], { stdio: 'ignore' });
|
|
1031
|
-
stopped = true;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
catch /* istanbul ignore next */ {
|
|
1036
|
-
// Ignore errors — fall through to child.kill
|
|
1037
|
-
}
|
|
1038
|
-
if (!stopped) {
|
|
1039
|
-
handle.child.kill('SIGTERM');
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
88
|
let isDockerRunning = false;
|
|
1046
89
|
/** Reset the running flag (for testing). */
|
|
1047
90
|
function resetIsDockerRunning() {
|
|
1048
91
|
isDockerRunning = false;
|
|
1049
92
|
}
|
|
1050
93
|
function runInDocker(opts) {
|
|
1051
|
-
// Guard against multiple concurrent invocations
|
|
1052
|
-
// server-triggered update firing at the same time).
|
|
94
|
+
// Guard against multiple concurrent invocations
|
|
1053
95
|
if (isDockerRunning) {
|
|
1054
96
|
logger_1.logger.warn('[docker] runInDocker called while already running — ignoring duplicate call');
|
|
1055
97
|
return;
|
|
@@ -1061,7 +103,7 @@ function runInDocker(opts) {
|
|
|
1061
103
|
process.exit(1);
|
|
1062
104
|
return;
|
|
1063
105
|
}
|
|
1064
|
-
if (!checkDockerAvailable()) {
|
|
106
|
+
if (!(0, docker_utils_1.checkDockerAvailable)()) {
|
|
1065
107
|
logger_1.logger.error((0, i18n_1.t)('docker.notAvailable'));
|
|
1066
108
|
process.exit(1);
|
|
1067
109
|
return;
|
|
@@ -1070,16 +112,15 @@ function runInDocker(opts) {
|
|
|
1070
112
|
// Sync bundled Dockerfile to config dir on first run (unless disabled)
|
|
1071
113
|
const shouldSync = opts.dockerfileSync !== false && config?.dockerfileSync !== false;
|
|
1072
114
|
if (shouldSync) {
|
|
1073
|
-
syncDockerfileToConfigDir();
|
|
115
|
+
(0, dockerfile_sync_1.syncDockerfileToConfigDir)();
|
|
1074
116
|
}
|
|
1075
117
|
// Resolve Dockerfile: CLI flag > config.dockerfilePath > configDir/Dockerfile > bundled default
|
|
1076
118
|
const customDockerfile = opts.dockerfile ?? config?.dockerfilePath;
|
|
1077
|
-
const version = ensureImage(customDockerfile);
|
|
119
|
+
const version = (0, version_manager_1.ensureImage)(customDockerfile);
|
|
1078
120
|
// Determine projects to run (skip entries without tenantCode)
|
|
1079
121
|
const allProjects = config ? (0, config_manager_1.getProjectList)(config) : [];
|
|
1080
122
|
let projects;
|
|
1081
123
|
if (opts.project) {
|
|
1082
|
-
// Single-project mode (e.g. for testing or external invocation)
|
|
1083
124
|
const slashIdx = opts.project.indexOf('/');
|
|
1084
125
|
if (slashIdx < 0) {
|
|
1085
126
|
logger_1.logger.error(`[docker] --project must be in "tenantCode/projectCode" format: ${opts.project}`);
|
|
@@ -1091,7 +132,6 @@ function runInDocker(opts) {
|
|
|
1091
132
|
projects = allProjects.filter((p) => p.tenantCode === tenantCode && p.projectCode === projectCode);
|
|
1092
133
|
if (projects.length === 0) {
|
|
1093
134
|
if (opts.token || process.env.AI_SUPPORT_AGENT_TOKEN) {
|
|
1094
|
-
// Fallback: create a temporary project from env/CLI args
|
|
1095
135
|
projects = [{
|
|
1096
136
|
tenantCode,
|
|
1097
137
|
projectCode,
|
|
@@ -1107,16 +147,13 @@ function runInDocker(opts) {
|
|
|
1107
147
|
}
|
|
1108
148
|
}
|
|
1109
149
|
else if (allProjects.length > 0) {
|
|
1110
|
-
// Multi-project supervisor mode: one container per project
|
|
1111
150
|
projects = allProjects;
|
|
1112
151
|
}
|
|
1113
152
|
else {
|
|
1114
|
-
// No config: run single container with legacy volume mount approach
|
|
1115
153
|
projects = [];
|
|
1116
154
|
}
|
|
1117
155
|
(0, claude_config_validator_1.ensureClaudeJsonIntegrity)();
|
|
1118
156
|
if (projects.length > 0) {
|
|
1119
|
-
// Per-project container mode (new architecture)
|
|
1120
157
|
logger_1.logger.info(`[docker] Starting ${projects.length} project container(s)...`);
|
|
1121
158
|
const agentId = config?.agentId ?? os.hostname();
|
|
1122
159
|
const enrichedOpts = {
|
|
@@ -1124,15 +161,14 @@ function runInDocker(opts) {
|
|
|
1124
161
|
agentId: opts.agentId ?? agentId,
|
|
1125
162
|
};
|
|
1126
163
|
(0, pid_manager_1.writePidFile)();
|
|
1127
|
-
const supervisor = new DockerSupervisor(version, enrichedOpts);
|
|
164
|
+
const supervisor = new docker_supervisor_1.DockerSupervisor(version, enrichedOpts);
|
|
1128
165
|
supervisor.start(projects, () => { isDockerRunning = false; });
|
|
1129
166
|
return;
|
|
1130
167
|
}
|
|
1131
168
|
// Legacy fallback: single container handling all projects via shared volume mount
|
|
1132
|
-
// (used when no projects are registered in config — e.g. token/apiUrl passed via CLI)
|
|
1133
169
|
logger_1.logger.info((0, i18n_1.t)('docker.starting'));
|
|
1134
|
-
const { mounts, projectMappings } = buildVolumeMounts();
|
|
1135
|
-
const envArgs = buildEnvArgs(projectMappings);
|
|
170
|
+
const { mounts, projectMappings } = (0, volume_mount_builder_1.buildVolumeMounts)();
|
|
171
|
+
const envArgs = (0, volume_mount_builder_1.buildEnvArgs)(projectMappings);
|
|
1136
172
|
const containerArgs = buildContainerArgs(opts);
|
|
1137
173
|
const interactive = process.stdin.isTTY ? ['-it'] : ['-i'];
|
|
1138
174
|
const dockerArgs = [
|
|
@@ -1140,7 +176,7 @@ function runInDocker(opts) {
|
|
|
1140
176
|
...(process.getuid ? ['--user', `${process.getuid()}:${process.getgid()}`] : []),
|
|
1141
177
|
...mounts,
|
|
1142
178
|
...envArgs,
|
|
1143
|
-
`${IMAGE_NAME}:${version}`,
|
|
179
|
+
`${docker_utils_1.IMAGE_NAME}:${version}`,
|
|
1144
180
|
...containerArgs,
|
|
1145
181
|
];
|
|
1146
182
|
const child = (0, child_process_1.spawn)('docker', dockerArgs, {
|
|
@@ -1158,19 +194,44 @@ function runInDocker(opts) {
|
|
|
1158
194
|
});
|
|
1159
195
|
let closeHandled = false;
|
|
1160
196
|
child.on('close', (code) => {
|
|
1161
|
-
// Guard against duplicate close events
|
|
1162
197
|
if (closeHandled)
|
|
1163
198
|
return;
|
|
1164
199
|
closeHandled = true;
|
|
1165
200
|
isDockerRunning = false;
|
|
1166
|
-
// DOCKER_UPDATE_EXIT_CODE signals "update installed, rebuild image and restart".
|
|
1167
|
-
// Any other exit (including 0 from SIGINT) exits the host process as-is.
|
|
1168
201
|
if (code === constants_1.DOCKER_UPDATE_EXIT_CODE) {
|
|
1169
202
|
logger_1.logger.info('[docker] Container exited for update. Rebuilding image and restarting...');
|
|
1170
|
-
void installUpdateAndRestart();
|
|
203
|
+
void (0, update_handler_1.installUpdateAndRestart)();
|
|
1171
204
|
return;
|
|
1172
205
|
}
|
|
1173
206
|
process.exit(code ?? 0);
|
|
1174
207
|
});
|
|
1175
208
|
}
|
|
209
|
+
// Re-exports for backward compatibility
|
|
210
|
+
var docker_utils_2 = require("./docker-utils");
|
|
211
|
+
Object.defineProperty(exports, "checkDockerAvailable", { enumerable: true, get: function () { return docker_utils_2.checkDockerAvailable; } });
|
|
212
|
+
Object.defineProperty(exports, "imageExists", { enumerable: true, get: function () { return docker_utils_2.imageExists; } });
|
|
213
|
+
Object.defineProperty(exports, "buildImage", { enumerable: true, get: function () { return docker_utils_2.buildImage; } });
|
|
214
|
+
Object.defineProperty(exports, "buildContainerName", { enumerable: true, get: function () { return docker_utils_2.buildContainerName; } });
|
|
215
|
+
Object.defineProperty(exports, "removeStaleContainer", { enumerable: true, get: function () { return docker_utils_2.removeStaleContainer; } });
|
|
216
|
+
Object.defineProperty(exports, "dockerLogin", { enumerable: true, get: function () { return docker_utils_2.dockerLogin; } });
|
|
217
|
+
var docker_security_1 = require("./docker-security");
|
|
218
|
+
Object.defineProperty(exports, "validatePackageNames", { enumerable: true, get: function () { return docker_security_1.validatePackageNames; } });
|
|
219
|
+
var dockerfile_generator_1 = require("./dockerfile-generator");
|
|
220
|
+
Object.defineProperty(exports, "generateProjectDockerfile", { enumerable: true, get: function () { return dockerfile_generator_1.generateProjectDockerfile; } });
|
|
221
|
+
var dockerfile_sync_2 = require("./dockerfile-sync");
|
|
222
|
+
Object.defineProperty(exports, "syncDockerfileToConfigDir", { enumerable: true, get: function () { return dockerfile_sync_2.syncDockerfileToConfigDir; } });
|
|
223
|
+
var project_image_builder_1 = require("./project-image-builder");
|
|
224
|
+
Object.defineProperty(exports, "buildProjectImage", { enumerable: true, get: function () { return project_image_builder_1.buildProjectImage; } });
|
|
225
|
+
var volume_mount_builder_2 = require("./volume-mount-builder");
|
|
226
|
+
Object.defineProperty(exports, "buildVolumeMounts", { enumerable: true, get: function () { return volume_mount_builder_2.buildVolumeMounts; } });
|
|
227
|
+
Object.defineProperty(exports, "buildEnvArgs", { enumerable: true, get: function () { return volume_mount_builder_2.buildEnvArgs; } });
|
|
228
|
+
Object.defineProperty(exports, "buildProjectVolumeMounts", { enumerable: true, get: function () { return volume_mount_builder_2.buildProjectVolumeMounts; } });
|
|
229
|
+
var version_manager_2 = require("./version-manager");
|
|
230
|
+
Object.defineProperty(exports, "getInstalledVersion", { enumerable: true, get: function () { return version_manager_2.getInstalledVersion; } });
|
|
231
|
+
Object.defineProperty(exports, "resetInstalledVersionCache", { enumerable: true, get: function () { return version_manager_2.resetInstalledVersionCache; } });
|
|
232
|
+
Object.defineProperty(exports, "ensureImage", { enumerable: true, get: function () { return version_manager_2.ensureImage; } });
|
|
233
|
+
var project_config_1 = require("./project-config");
|
|
234
|
+
Object.defineProperty(exports, "migrateProjectConfigDir", { enumerable: true, get: function () { return project_config_1.migrateProjectConfigDir; } });
|
|
235
|
+
var docker_supervisor_2 = require("./docker-supervisor");
|
|
236
|
+
Object.defineProperty(exports, "DockerSupervisor", { enumerable: true, get: function () { return docker_supervisor_2.DockerSupervisor; } });
|
|
1176
237
|
//# sourceMappingURL=docker-runner.js.map
|