@ai-support-agent/cli 0.1.22-beta.4 → 0.1.22
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/auto-updater.js +1 -1
- package/dist/auto-updater.js.map +1 -1
- package/dist/cli/service/darwin-service.d.ts.map +1 -1
- package/dist/cli/service/darwin-service.js +11 -7
- package/dist/cli/service/darwin-service.js.map +1 -1
- package/dist/cli/service/win32-service.d.ts +59 -1
- package/dist/cli/service/win32-service.d.ts.map +1 -1
- package/dist/cli/service/win32-service.js +463 -95
- package/dist/cli/service/win32-service.js.map +1 -1
- package/dist/commands/chat-executor.js +1 -1
- package/dist/commands/chat-executor.js.map +1 -1
- package/dist/commands/e2e-test-executor.js +8 -8
- package/dist/commands/e2e-test-executor.js.map +1 -1
- package/dist/commands/stop-agent.js +1 -1
- package/dist/commands/stop-agent.js.map +1 -1
- package/dist/docker/docker-utils.d.ts.map +1 -1
- package/dist/docker/docker-utils.js +34 -6
- package/dist/docker/docker-utils.js.map +1 -1
- package/dist/docker/dockerfile-generator.d.ts.map +1 -1
- package/dist/docker/dockerfile-generator.js +8 -0
- package/dist/docker/dockerfile-generator.js.map +1 -1
- package/dist/docker/project-image-builder.d.ts.map +1 -1
- package/dist/docker/project-image-builder.js +2 -1
- package/dist/docker/project-image-builder.js.map +1 -1
- package/dist/locales/en.json +1 -0
- package/dist/locales/ja.json +1 -0
- package/dist/mcp/tools/browser.d.ts.map +1 -1
- package/dist/mcp/tools/browser.js +2 -1
- package/dist/mcp/tools/browser.js.map +1 -1
- package/dist/mcp/tools/e2e-test-step.d.ts.map +1 -1
- package/dist/mcp/tools/e2e-test-step.js +2 -1
- package/dist/mcp/tools/e2e-test-step.js.map +1 -1
- package/dist/project-agent.js +1 -1
- package/dist/project-agent.js.map +1 -1
- package/dist/project-worker.d.ts.map +1 -1
- package/dist/project-worker.js +2 -1
- package/dist/project-worker.js.map +1 -1
- package/dist/retry-strategy.d.ts.map +1 -1
- package/dist/retry-strategy.js +2 -1
- package/dist/retry-strategy.js.map +1 -1
- package/dist/utils/path-utils.d.ts +4 -0
- package/dist/utils/path-utils.d.ts.map +1 -1
- package/dist/utils/path-utils.js +14 -0
- package/dist/utils/path-utils.js.map +1 -1
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +27 -1
- package/dist/utils.js.map +1 -1
- package/dist/vscode/vscode-server.js +1 -1
- package/dist/vscode/vscode-server.js.map +1 -1
- package/dist/ws-reconnect.js +1 -1
- package/dist/ws-reconnect.js.map +1 -1
- package/package.json +1 -1
|
@@ -33,32 +33,88 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.Win32ServiceStrategy = void 0;
|
|
36
|
+
exports.Win32ServiceStrategy = exports.getNodePath = exports.getCliEntryPoint = void 0;
|
|
37
|
+
exports.getProjectTaskName = getProjectTaskName;
|
|
38
|
+
exports.getAllProjectTasks = getAllProjectTasks;
|
|
39
|
+
exports.generateProjectTaskXml = generateProjectTaskXml;
|
|
40
|
+
exports.generateWin32WrapperScript = generateWin32WrapperScript;
|
|
41
|
+
exports.writeAndRegisterProjectTask = writeAndRegisterProjectTask;
|
|
37
42
|
exports.generateTaskXml = generateTaskXml;
|
|
38
43
|
const child_process_1 = require("child_process");
|
|
39
44
|
const fs = __importStar(require("fs"));
|
|
40
45
|
const os = __importStar(require("os"));
|
|
41
46
|
const path = __importStar(require("path"));
|
|
42
47
|
const constants_1 = require("../../constants");
|
|
48
|
+
const config_manager_1 = require("../../config-manager");
|
|
49
|
+
const docker_utils_1 = require("../../docker/docker-utils");
|
|
43
50
|
const i18n_1 = require("../../i18n");
|
|
44
51
|
const logger_1 = require("../../logger");
|
|
45
52
|
const utils_1 = require("../../utils");
|
|
53
|
+
const path_utils_1 = require("../../utils/path-utils");
|
|
46
54
|
const escape_xml_1 = require("./escape-xml");
|
|
47
55
|
const node_paths_1 = require("./node-paths");
|
|
48
|
-
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
Object.defineProperty(exports, "getCliEntryPoint", { enumerable: true, get: function () { return node_paths_1.getCliEntryPoint; } });
|
|
57
|
+
Object.defineProperty(exports, "getNodePath", { enumerable: true, get: function () { return node_paths_1.getNodePath; } });
|
|
58
|
+
const wrapper_helpers_1 = require("./wrapper-helpers");
|
|
59
|
+
const TASK_PREFIX = 'AISupportAgent';
|
|
60
|
+
const getLogDir = path_utils_1.getWin32LogDir;
|
|
61
|
+
/** Sanitize a code the same way buildContainerName does (lowercase, [a-z0-9-]). */
|
|
62
|
+
function sanitize(s) {
|
|
63
|
+
return s.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
52
64
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Per-project task helpers
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Returns the scheduled-task name for a given project.
|
|
70
|
+
* Format: AISupportAgent-{tenantCode}-{projectCode} (sanitized).
|
|
71
|
+
*
|
|
72
|
+
* The Windows Task Scheduler accepts backslashes as folder separators in task
|
|
73
|
+
* names, so any backslash in a code would silently create a nested task folder.
|
|
74
|
+
* sanitize() already strips it (along with other non `[a-z0-9-]` chars).
|
|
75
|
+
*/
|
|
76
|
+
function getProjectTaskName(tenantCode, projectCode) {
|
|
77
|
+
return `${TASK_PREFIX}-${sanitize(tenantCode)}-${sanitize(projectCode)}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Lists all per-project scheduled tasks registered by this CLI.
|
|
81
|
+
*
|
|
82
|
+
* Queries Task Scheduler for tasks whose name starts with `${TASK_PREFIX}-`.
|
|
83
|
+
* Returns an empty list when schtasks is unavailable or no task matches.
|
|
84
|
+
*/
|
|
85
|
+
function getAllProjectTasks() {
|
|
86
|
+
const prefix = `${TASK_PREFIX}-`;
|
|
87
|
+
const results = [];
|
|
88
|
+
try {
|
|
89
|
+
// /FO CSV /NH → bare rows; first column is "\TaskName" (leading backslash
|
|
90
|
+
// = root folder). Strip the leading backslash and surrounding quotes.
|
|
91
|
+
const output = (0, child_process_1.execSync)('schtasks /Query /FO CSV /NH', { stdio: 'pipe' }).toString();
|
|
92
|
+
for (const line of output.split(/\r?\n/)) {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
if (!trimmed)
|
|
95
|
+
continue;
|
|
96
|
+
const firstCol = trimmed.split('","')[0].replace(/^"/, '').replace(/"$/, '');
|
|
97
|
+
const name = firstCol.replace(/^\\/, '');
|
|
98
|
+
if (name.startsWith(prefix) && !results.some((r) => r.taskName === name)) {
|
|
99
|
+
results.push({ taskName: name });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
58
102
|
}
|
|
59
|
-
|
|
60
|
-
|
|
103
|
+
catch {
|
|
104
|
+
// schtasks unavailable / no tasks — return empty list
|
|
61
105
|
}
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Task XML + wrapper generation
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
/**
|
|
112
|
+
* Generate the scheduled-task XML that runs a per-project wrapper script.
|
|
113
|
+
* The action invokes cmd.exe on the generated run.cmd so the wrapper can set
|
|
114
|
+
* env vars and run `docker run` without leaking secrets onto the task's
|
|
115
|
+
* command line (which is world-readable via `schtasks /Query /XML`).
|
|
116
|
+
*/
|
|
117
|
+
function generateProjectTaskXml(opts) {
|
|
62
118
|
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
63
119
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
64
120
|
<Triggers>
|
|
@@ -78,140 +134,452 @@ function generateTaskXml(options) {
|
|
|
78
134
|
</Settings>
|
|
79
135
|
<Actions>
|
|
80
136
|
<Exec>
|
|
81
|
-
<Command
|
|
82
|
-
<Arguments
|
|
137
|
+
<Command>cmd.exe</Command>
|
|
138
|
+
<Arguments>/c ${(0, escape_xml_1.escapeXml)(`"${opts.wrapperScriptPath}"`)}</Arguments>
|
|
83
139
|
</Exec>
|
|
84
140
|
</Actions>
|
|
85
141
|
</Task>`;
|
|
86
142
|
}
|
|
143
|
+
/** Convert localhost/127.0.0.1 to host.docker.internal for container use */
|
|
144
|
+
function toContainerApiUrl(apiUrl) {
|
|
145
|
+
return apiUrl.replace(/^(https?:\/\/)(localhost|127\.0\.0\.1)(:\d+)?/, (_, scheme, _host, port) => `${scheme}host.docker.internal${port ?? ''}`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Windows batch (cmd.exe) cannot reliably escape every metacharacter the way a
|
|
149
|
+
* POSIX shell can with single quotes. Rather than risk a broken or injectable
|
|
150
|
+
* `set "VAR=value"` line, reject values containing characters that have special
|
|
151
|
+
* meaning to cmd.exe so the user fixes the bad value instead of silently
|
|
152
|
+
* generating a broken wrapper. Tokens / URLs / API keys never legitimately
|
|
153
|
+
* contain these. (No fallback — see the project "no fallback" rule.)
|
|
154
|
+
*/
|
|
155
|
+
// Forbid cmd.exe metacharacters: % (var expansion), ! (delayed expansion),
|
|
156
|
+
// " (quote), CR/LF, ^ (escape/continuation), & (command separator), <>| (redir
|
|
157
|
+
// /pipe), and () (block grouping — harmless inside `set "..."` but rejected as
|
|
158
|
+
// defense in depth). A leading/trailing space is rejected separately below
|
|
159
|
+
// because `set " VAR=..."` would create a space-prefixed variable name that the
|
|
160
|
+
// `-e VAR` inherit line could never match.
|
|
161
|
+
const CMD_FORBIDDEN_RE = /[%!"\r\n^&<>|()]/;
|
|
162
|
+
function assertCmdSafe(value, field) {
|
|
163
|
+
if (CMD_FORBIDDEN_RE.test(value) || value !== value.trim()) {
|
|
164
|
+
throw new Error((0, i18n_1.t)('service.invalidWin32Value', { field }));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Generate a cmd.exe batch wrapper that runs docker for one project.
|
|
169
|
+
*
|
|
170
|
+
* Mirrors the Linux/Darwin wrappers: bind-mounts the per-project config dir and
|
|
171
|
+
* project dir, passes the token / api url / API keys as `-e` env vars, resolves
|
|
172
|
+
* the installed CLI version at runtime, auto-builds the image if missing, and
|
|
173
|
+
* removes any stale container before `docker run`.
|
|
174
|
+
*/
|
|
175
|
+
function generateWin32WrapperScript(opts) {
|
|
176
|
+
const containerHome = '/home/node';
|
|
177
|
+
const containerConfigDir = `${containerHome}/.ai-support-agent`;
|
|
178
|
+
const homeDir = os.homedir();
|
|
179
|
+
const containerApiUrl = toContainerApiUrl(opts.apiUrl);
|
|
180
|
+
// Reject codes whose characters would break container naming / the dir map.
|
|
181
|
+
(0, wrapper_helpers_1.assertProjectCodeIsSafe)(opts.projectCode);
|
|
182
|
+
(0, wrapper_helpers_1.assertProjectCodeIsSafe)(opts.tenantCode);
|
|
183
|
+
// Reject secret/url values that cmd.exe cannot safely carry.
|
|
184
|
+
assertCmdSafe(opts.token, 'token');
|
|
185
|
+
assertCmdSafe(containerApiUrl, 'apiUrl');
|
|
186
|
+
if (opts.anthropicApiKey)
|
|
187
|
+
assertCmdSafe(opts.anthropicApiKey, 'anthropicApiKey');
|
|
188
|
+
if (opts.claudeCodeOauthToken)
|
|
189
|
+
assertCmdSafe(opts.claudeCodeOauthToken, 'claudeCodeOauthToken');
|
|
190
|
+
const containerProjectDir = `/workspace/projects/${opts.projectCode}`;
|
|
191
|
+
// `||` (not `??`) so an empty projectDir falls back to the default; an empty
|
|
192
|
+
// host path would emit `-v :/workspace/...:rw` which docker rejects.
|
|
193
|
+
const hostProjectDir = opts.projectDir || path.dirname(opts.projectConfigHostDir);
|
|
194
|
+
const containerName = `ai-${sanitize(opts.tenantCode)}-${sanitize(opts.projectCode)}`;
|
|
195
|
+
// docker run argument lines. Host paths are wrapped in double quotes so a
|
|
196
|
+
// space in the Windows home dir (e.g. C:\Users\First Last) does not split the
|
|
197
|
+
// -v argument. The values were validated above to contain no `"`.
|
|
198
|
+
const mountLines = [
|
|
199
|
+
` -v "${homeDir}\\.claude:${containerHome}/.claude:rw" ^`,
|
|
200
|
+
` -v "${opts.projectConfigHostDir}:${containerConfigDir}:rw" ^`,
|
|
201
|
+
` -v "${homeDir}\\.claude.json:${containerHome}/.claude.json:rw" ^`,
|
|
202
|
+
` -v "${hostProjectDir}:${containerProjectDir}:rw" ^`,
|
|
203
|
+
];
|
|
204
|
+
const envLines = [
|
|
205
|
+
` -e AI_SUPPORT_AGENT_IN_DOCKER=1 ^`,
|
|
206
|
+
` -e HOME=${containerHome} ^`,
|
|
207
|
+
` -e AI_SUPPORT_AGENT_CONFIG_DIR=${containerConfigDir} ^`,
|
|
208
|
+
// `-e NAME` (no value) tells docker to inherit the value from the wrapper's
|
|
209
|
+
// own environment, which we set via `set` below. This keeps secrets off the
|
|
210
|
+
// `docker run` argument vector.
|
|
211
|
+
` -e AI_SUPPORT_AGENT_TOKEN ^`,
|
|
212
|
+
` -e AI_SUPPORT_AGENT_API_URL ^`,
|
|
213
|
+
` -e AI_SUPPORT_AGENT_PROJECT_DIR_MAP ^`,
|
|
214
|
+
];
|
|
215
|
+
if (opts.anthropicApiKey)
|
|
216
|
+
envLines.push(` -e ANTHROPIC_API_KEY ^`);
|
|
217
|
+
if (opts.claudeCodeOauthToken)
|
|
218
|
+
envLines.push(` -e CLAUDE_CODE_OAUTH_TOKEN ^`);
|
|
219
|
+
const containerArgs = [
|
|
220
|
+
'ai-support-agent', 'start', constants_1.CLI_FLAG_NO_DOCKER,
|
|
221
|
+
`--project ${opts.tenantCode}/${opts.projectCode}`,
|
|
222
|
+
];
|
|
223
|
+
if (opts.verbose)
|
|
224
|
+
containerArgs.push(constants_1.CLI_FLAG_VERBOSE);
|
|
225
|
+
const setLines = [
|
|
226
|
+
`set "AI_SUPPORT_AGENT_TOKEN=${opts.token}"`,
|
|
227
|
+
`set "AI_SUPPORT_AGENT_API_URL=${containerApiUrl}"`,
|
|
228
|
+
`set "AI_SUPPORT_AGENT_PROJECT_DIR_MAP=${opts.projectCode}=${containerProjectDir}"`,
|
|
229
|
+
];
|
|
230
|
+
if (opts.anthropicApiKey)
|
|
231
|
+
setLines.push(`set "ANTHROPIC_API_KEY=${opts.anthropicApiKey}"`);
|
|
232
|
+
if (opts.claudeCodeOauthToken)
|
|
233
|
+
setLines.push(`set "CLAUDE_CODE_OAUTH_TOKEN=${opts.claudeCodeOauthToken}"`);
|
|
234
|
+
const dockerRun = [
|
|
235
|
+
`docker run --rm -i --name "${containerName}" ^`,
|
|
236
|
+
mountLines.join('\r\n'),
|
|
237
|
+
envLines.join('\r\n'),
|
|
238
|
+
` "%IMAGE_TAG%" ^`,
|
|
239
|
+
` ${containerArgs.join(' ')}`,
|
|
240
|
+
].join('\r\n');
|
|
241
|
+
// NOTE: `setlocal` scopes the `set` secrets to this wrapper invocation so
|
|
242
|
+
// they never leak into the parent environment. EnableExtensions is needed for
|
|
243
|
+
// the `if` blocks. We deliberately do NOT enable delayed expansion so a `!`
|
|
244
|
+
// in a value cannot trigger expansion (and `!` is rejected above anyway).
|
|
245
|
+
const lines = [
|
|
246
|
+
`@echo off`,
|
|
247
|
+
`setlocal EnableExtensions`,
|
|
248
|
+
``,
|
|
249
|
+
`rem Resolve the installed CLI version at runtime so the image stays current after npm updates.`,
|
|
250
|
+
`rem usebackq lets the command be wrapped in backquotes so the inner node -p`,
|
|
251
|
+
`rem double quotes don't confuse for /f's parser, and a space in the npm root`,
|
|
252
|
+
`rem path (e.g. C:\\Program Files\\...) stays inside the JS string literal.`,
|
|
253
|
+
`for /f "usebackq delims=" %%R in (\`npm root -g 2^>nul\`) do set "_NPM_ROOT=%%R"`,
|
|
254
|
+
`if not defined _NPM_ROOT (`,
|
|
255
|
+
` echo ERROR: Could not determine npm global root 1>&2`,
|
|
256
|
+
` exit /b 1`,
|
|
257
|
+
`)`,
|
|
258
|
+
`for /f "usebackq delims=" %%V in (\`node -p "require('%_NPM_ROOT:\\=/%/@ai-support-agent/cli/package.json').version" 2^>nul\`) do set "_INSTALLED_VERSION=%%V"`,
|
|
259
|
+
`if not defined _INSTALLED_VERSION (`,
|
|
260
|
+
` echo ERROR: Could not determine installed version of @ai-support-agent/cli 1>&2`,
|
|
261
|
+
` exit /b 1`,
|
|
262
|
+
`)`,
|
|
263
|
+
`set "IMAGE_TAG=${opts.imageName}:%_INSTALLED_VERSION%"`,
|
|
264
|
+
``,
|
|
265
|
+
`rem Auto-build the image if the required version does not exist locally`,
|
|
266
|
+
`docker image inspect "%IMAGE_TAG%" >nul 2>&1`,
|
|
267
|
+
`if errorlevel 1 (`,
|
|
268
|
+
` echo Docker image %IMAGE_TAG% not found - building... 1>&2`,
|
|
269
|
+
` call ai-support-agent docker-build`,
|
|
270
|
+
` if errorlevel 1 (`,
|
|
271
|
+
` echo ERROR: docker-build failed - cannot start container 1>&2`,
|
|
272
|
+
` exit /b 1`,
|
|
273
|
+
` )`,
|
|
274
|
+
`)`,
|
|
275
|
+
``,
|
|
276
|
+
`rem Remove any stale container from a previous crash`,
|
|
277
|
+
`docker rm -f "${containerName}" >nul 2>&1`,
|
|
278
|
+
``,
|
|
279
|
+
...setLines,
|
|
280
|
+
``,
|
|
281
|
+
dockerRun,
|
|
282
|
+
``,
|
|
283
|
+
`endlocal`,
|
|
284
|
+
];
|
|
285
|
+
return lines.join('\r\n') + '\r\n';
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Service file writing
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
/**
|
|
291
|
+
* Write the run.cmd wrapper and register/refresh the scheduled task for one
|
|
292
|
+
* project. Idempotent: deletes any existing task with the same name first so
|
|
293
|
+
* token/config updates take effect.
|
|
294
|
+
*/
|
|
295
|
+
function writeAndRegisterProjectTask(project, options = {}) {
|
|
296
|
+
const { tenantCode, projectCode } = project;
|
|
297
|
+
(0, wrapper_helpers_1.assertProjectCodeIsSafe)(projectCode);
|
|
298
|
+
(0, wrapper_helpers_1.assertProjectCodeIsSafe)(tenantCode);
|
|
299
|
+
const projectKey = `${tenantCode}-${projectCode.toLowerCase()}`;
|
|
300
|
+
// mode 0o700 keeps the per-project metadata, services, and logs owner-only.
|
|
301
|
+
// The run.cmd wrapper written below contains the token in plaintext, so the
|
|
302
|
+
// service dir must not be world-readable. On Windows the POSIX mode bits are
|
|
303
|
+
// only partially honored, but we set them for parity with darwin/linux and so
|
|
304
|
+
// the wrapper is at least not group/other-writable on filesystems that map
|
|
305
|
+
// them (e.g. WSL/SMB shares).
|
|
306
|
+
const logDir = getLogDir();
|
|
307
|
+
const projectLogDir = (0, path_utils_1.getProjectLogDir)(logDir, projectKey);
|
|
308
|
+
if (!fs.existsSync(projectLogDir)) {
|
|
309
|
+
fs.mkdirSync(projectLogDir, { recursive: true, mode: 0o700 });
|
|
310
|
+
}
|
|
311
|
+
const servicesDir = (0, path_utils_1.getServicesDir)();
|
|
312
|
+
const projectServiceDir = (0, path_utils_1.getProjectServiceDir)(servicesDir, projectKey);
|
|
313
|
+
if (!fs.existsSync(projectServiceDir)) {
|
|
314
|
+
fs.mkdirSync(projectServiceDir, { recursive: true, mode: 0o700 });
|
|
315
|
+
}
|
|
316
|
+
const projectConfigHostDir = (0, path_utils_1.getProjectConfigHostDir)(tenantCode, projectCode);
|
|
317
|
+
if (!fs.existsSync(projectConfigHostDir)) {
|
|
318
|
+
fs.mkdirSync(projectConfigHostDir, { recursive: true, mode: 0o700 });
|
|
319
|
+
}
|
|
320
|
+
const validatedProjectDir = (0, wrapper_helpers_1.validateProjectDirForMount)(project.projectDir);
|
|
321
|
+
const wrapperScriptPath = (0, path_utils_1.getWin32WrapperScriptPath)(projectServiceDir);
|
|
322
|
+
const wrapperScript = generateWin32WrapperScript({
|
|
323
|
+
imageName: docker_utils_1.IMAGE_NAME,
|
|
324
|
+
tenantCode,
|
|
325
|
+
projectCode,
|
|
326
|
+
projectConfigHostDir,
|
|
327
|
+
projectDir: validatedProjectDir,
|
|
328
|
+
token: project.token,
|
|
329
|
+
apiUrl: project.apiUrl,
|
|
330
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
331
|
+
claudeCodeOauthToken: process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
332
|
+
verbose: options.verbose,
|
|
333
|
+
});
|
|
334
|
+
// The wrapper holds the token in plaintext — write it owner-only.
|
|
335
|
+
fs.writeFileSync(wrapperScriptPath, wrapperScript, { encoding: 'utf-8', mode: 0o700 });
|
|
336
|
+
const taskName = getProjectTaskName(tenantCode, projectCode);
|
|
337
|
+
const xml = generateProjectTaskXml({ wrapperScriptPath });
|
|
338
|
+
const tmpXmlPath = path.join(os.tmpdir(), `${taskName}-task.xml`);
|
|
339
|
+
try {
|
|
340
|
+
fs.writeFileSync(tmpXmlPath, xml, 'utf-8');
|
|
341
|
+
// /F overwrites an existing task so token/config updates take effect.
|
|
342
|
+
(0, child_process_1.execSync)(`schtasks /Create /TN "${taskName}" /XML "${tmpXmlPath}" /F`, { stdio: 'pipe' });
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
if (fs.existsSync(tmpXmlPath)) {
|
|
346
|
+
fs.unlinkSync(tmpXmlPath);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
87
350
|
class Win32ServiceStrategy {
|
|
88
351
|
install(options) {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
352
|
+
const config = (0, config_manager_1.loadConfig)();
|
|
353
|
+
const projects = config ? (0, config_manager_1.getProjectList)(config) : [];
|
|
354
|
+
if (projects.length === 0) {
|
|
355
|
+
logger_1.logger.error((0, i18n_1.t)('service.noProjectsConfigured'));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
91
358
|
const entryPoint = (0, node_paths_1.getCliEntryPoint)();
|
|
92
359
|
if (!fs.existsSync(entryPoint)) {
|
|
93
360
|
logger_1.logger.error((0, i18n_1.t)('service.entryPointNotFound', { path: entryPoint }));
|
|
94
361
|
return;
|
|
95
362
|
}
|
|
363
|
+
const logDir = getLogDir();
|
|
96
364
|
if (!fs.existsSync(logDir)) {
|
|
97
365
|
fs.mkdirSync(logDir, { recursive: true });
|
|
98
366
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
367
|
+
// Detect sanitize() collisions where two valid codes map to the same task
|
|
368
|
+
// name. Shared helper guarantees identical semantics with linux/darwin.
|
|
369
|
+
const { collisions } = (0, wrapper_helpers_1.detectInstallCollisions)(projects, getProjectTaskName);
|
|
370
|
+
const reportedCollisionLabels = new Set();
|
|
371
|
+
let installedCount = 0;
|
|
372
|
+
let failedCount = 0;
|
|
373
|
+
for (const project of projects) {
|
|
374
|
+
const { projectCode } = project;
|
|
375
|
+
const fqn = `${project.tenantCode}/${projectCode}`;
|
|
376
|
+
const collision = collisions.get(fqn);
|
|
377
|
+
if (collision) {
|
|
378
|
+
const messageKey = collision.isDuplicate
|
|
379
|
+
? 'service.projectDuplicateEntry'
|
|
380
|
+
: 'service.projectUnitNameCollision';
|
|
381
|
+
const dedupKey = `${collision.name}\x00${messageKey}`;
|
|
382
|
+
if (!reportedCollisionLabels.has(dedupKey)) {
|
|
383
|
+
logger_1.logger.error((0, i18n_1.t)(messageKey, {
|
|
384
|
+
projectCode,
|
|
385
|
+
unitName: collision.name,
|
|
386
|
+
others: collision.others.join(', '),
|
|
387
|
+
}));
|
|
388
|
+
reportedCollisionLabels.add(dedupKey);
|
|
389
|
+
}
|
|
390
|
+
failedCount += 1;
|
|
391
|
+
continue;
|
|
121
392
|
}
|
|
393
|
+
try {
|
|
394
|
+
writeAndRegisterProjectTask(project, { verbose: options.verbose });
|
|
395
|
+
logger_1.logger.success((0, i18n_1.t)('service.projectInstalled', { projectCode, path: getProjectTaskName(project.tenantCode, projectCode) }));
|
|
396
|
+
installedCount += 1;
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
const message = (0, utils_1.getErrorMessage)(error);
|
|
400
|
+
logger_1.logger.error((0, i18n_1.t)('service.projectInstallFailed', { projectCode, message }));
|
|
401
|
+
failedCount += 1;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (installedCount > 0) {
|
|
405
|
+
logger_1.logger.info((0, i18n_1.t)('service.loadHintMulti'));
|
|
406
|
+
logger_1.logger.info((0, i18n_1.t)('service.logDir', { path: logDir }));
|
|
407
|
+
logger_1.logger.info((0, i18n_1.t)('service.noLogRotation'));
|
|
408
|
+
}
|
|
409
|
+
if (failedCount > 0) {
|
|
410
|
+
logger_1.logger.warn((0, i18n_1.t)('service.partialInstallSummary', {
|
|
411
|
+
failed: String(failedCount),
|
|
412
|
+
total: String(projects.length),
|
|
413
|
+
succeeded: String(installedCount),
|
|
414
|
+
}));
|
|
122
415
|
}
|
|
123
|
-
logger_1.logger.success((0, i18n_1.t)('service.installed.win32', { taskName: TASK_NAME }));
|
|
124
|
-
logger_1.logger.info((0, i18n_1.t)('service.loadHint.win32', { taskName: TASK_NAME }));
|
|
125
|
-
logger_1.logger.info((0, i18n_1.t)('service.logDir', { path: logDir }));
|
|
126
|
-
logger_1.logger.info((0, i18n_1.t)('service.noLogRotation'));
|
|
127
416
|
}
|
|
128
417
|
uninstall() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
418
|
+
const projectTasks = getAllProjectTasks();
|
|
419
|
+
if (projectTasks.length === 0) {
|
|
133
420
|
logger_1.logger.warn((0, i18n_1.t)('service.notInstalled.win32'));
|
|
134
421
|
return;
|
|
135
422
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
423
|
+
let failed = false;
|
|
424
|
+
for (const { taskName } of projectTasks) {
|
|
425
|
+
try {
|
|
426
|
+
(0, child_process_1.execSync)(`schtasks /Delete /TN "${taskName}" /F`, { stdio: 'pipe' });
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
const message = (0, utils_1.getErrorMessage)(error);
|
|
430
|
+
logger_1.logger.error((0, i18n_1.t)('service.schtasksFailed', { message }));
|
|
431
|
+
failed = true;
|
|
432
|
+
}
|
|
139
433
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
logger_1.logger.error((0, i18n_1.t)('service.schtasksFailed', { message }));
|
|
143
|
-
return;
|
|
434
|
+
if (!failed) {
|
|
435
|
+
logger_1.logger.success((0, i18n_1.t)('service.uninstalled.win32'));
|
|
144
436
|
}
|
|
145
|
-
logger_1.logger.success((0, i18n_1.t)('service.uninstalled.win32'));
|
|
146
437
|
}
|
|
147
438
|
start() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
439
|
+
const projectTasks = getAllProjectTasks();
|
|
440
|
+
if (projectTasks.length === 0) {
|
|
152
441
|
logger_1.logger.error((0, i18n_1.t)('service.notInstalled.win32'));
|
|
153
442
|
return;
|
|
154
443
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
444
|
+
let failed = false;
|
|
445
|
+
for (const { taskName } of projectTasks) {
|
|
446
|
+
try {
|
|
447
|
+
(0, child_process_1.execSync)(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' });
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
const message = (0, utils_1.getErrorMessage)(error);
|
|
451
|
+
logger_1.logger.error((0, i18n_1.t)('service.startFailed', { message }));
|
|
452
|
+
failed = true;
|
|
453
|
+
}
|
|
158
454
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
logger_1.logger.error((0, i18n_1.t)('service.startFailed', { message }));
|
|
455
|
+
if (!failed) {
|
|
456
|
+
logger_1.logger.success((0, i18n_1.t)('service.started'));
|
|
162
457
|
}
|
|
163
458
|
}
|
|
164
459
|
stop() {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
460
|
+
const projectTasks = getAllProjectTasks();
|
|
461
|
+
if (projectTasks.length === 0) {
|
|
169
462
|
logger_1.logger.error((0, i18n_1.t)('service.notInstalled.win32'));
|
|
170
463
|
return;
|
|
171
464
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
465
|
+
let failed = false;
|
|
466
|
+
for (const { taskName } of projectTasks) {
|
|
467
|
+
try {
|
|
468
|
+
(0, child_process_1.execSync)(`schtasks /End /TN "${taskName}"`, { stdio: 'pipe' });
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
const message = (0, utils_1.getErrorMessage)(error);
|
|
472
|
+
logger_1.logger.error((0, i18n_1.t)('service.stopFailed', { message }));
|
|
473
|
+
failed = true;
|
|
474
|
+
}
|
|
175
475
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
logger_1.logger.error((0, i18n_1.t)('service.stopFailed', { message }));
|
|
476
|
+
if (!failed) {
|
|
477
|
+
logger_1.logger.success((0, i18n_1.t)('service.stopped'));
|
|
179
478
|
}
|
|
180
479
|
}
|
|
181
480
|
restart() {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
481
|
+
const projectTasks = getAllProjectTasks();
|
|
482
|
+
if (projectTasks.length === 0) {
|
|
186
483
|
logger_1.logger.error((0, i18n_1.t)('service.notInstalled.win32'));
|
|
187
484
|
return;
|
|
188
485
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
486
|
+
let failed = false;
|
|
487
|
+
for (const { taskName } of projectTasks) {
|
|
488
|
+
try {
|
|
489
|
+
try {
|
|
490
|
+
(0, child_process_1.execSync)(`schtasks /End /TN "${taskName}"`, { stdio: 'pipe' });
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
// Task may not be running — ignore
|
|
494
|
+
}
|
|
495
|
+
(0, child_process_1.execSync)(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' });
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
const message = (0, utils_1.getErrorMessage)(error);
|
|
499
|
+
logger_1.logger.error((0, i18n_1.t)('service.restartFailed', { message }));
|
|
500
|
+
failed = true;
|
|
501
|
+
}
|
|
194
502
|
}
|
|
195
|
-
|
|
196
|
-
(0, child_process_1.execSync)(`schtasks /Run /TN "${TASK_NAME}"`, { stdio: 'pipe' });
|
|
503
|
+
if (!failed) {
|
|
197
504
|
logger_1.logger.success((0, i18n_1.t)('service.restarted'));
|
|
198
505
|
}
|
|
199
|
-
catch (error) {
|
|
200
|
-
const message = (0, utils_1.getErrorMessage)(error);
|
|
201
|
-
logger_1.logger.error((0, i18n_1.t)('service.restartFailed', { message }));
|
|
202
|
-
}
|
|
203
506
|
}
|
|
204
507
|
status() {
|
|
508
|
+
const projectTasks = getAllProjectTasks();
|
|
205
509
|
const logDir = getLogDir();
|
|
206
|
-
|
|
207
|
-
const output = (0, child_process_1.execSync)(`schtasks /Query /TN "${TASK_NAME}" /FO CSV /NH`, { stdio: 'pipe' }).toString();
|
|
208
|
-
const running = output.includes('Running');
|
|
209
|
-
return { installed: true, running, logDir };
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
510
|
+
if (projectTasks.length === 0) {
|
|
212
511
|
return { installed: false, running: false };
|
|
213
512
|
}
|
|
513
|
+
// Build a reverse map from task name → original projectCode using config.
|
|
514
|
+
// sanitize() collapses `_` and other chars to `-`, so splitting the task
|
|
515
|
+
// name on `-` is lossy when tenant/project codes contain those characters
|
|
516
|
+
// (and the tenant/project boundary is ambiguous) — config is the source of
|
|
517
|
+
// truth. Mirrors the Linux strategy.
|
|
518
|
+
const config = (0, config_manager_1.loadConfig)();
|
|
519
|
+
const projectByTaskName = new Map();
|
|
520
|
+
if (config) {
|
|
521
|
+
for (const project of (0, config_manager_1.getProjectList)(config)) {
|
|
522
|
+
projectByTaskName.set(getProjectTaskName(project.tenantCode, project.projectCode), project.projectCode);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const projects = [];
|
|
526
|
+
let isAnyRunning = false;
|
|
527
|
+
for (const { taskName } of projectTasks) {
|
|
528
|
+
// Prefer the canonical projectCode from config. For orphaned tasks no
|
|
529
|
+
// longer in config, fall back to the task name with the brand prefix
|
|
530
|
+
// stripped so users still see something close to the original code.
|
|
531
|
+
const projectCode = projectByTaskName.get(taskName)
|
|
532
|
+
?? (taskName.startsWith(`${TASK_PREFIX}-`) ? taskName.slice(TASK_PREFIX.length + 1) : taskName);
|
|
533
|
+
let running = false;
|
|
534
|
+
try {
|
|
535
|
+
const output = (0, child_process_1.execSync)(`schtasks /Query /TN "${taskName}" /FO CSV /NH`, { stdio: 'pipe' }).toString();
|
|
536
|
+
if (output.includes('Running')) {
|
|
537
|
+
running = true;
|
|
538
|
+
isAnyRunning = true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// Not registered / query failed — running stays false
|
|
543
|
+
}
|
|
544
|
+
projects.push({ label: taskName, projectCode, running });
|
|
545
|
+
}
|
|
546
|
+
return { installed: true, running: isAnyRunning, logDir, projects };
|
|
214
547
|
}
|
|
215
548
|
}
|
|
216
549
|
exports.Win32ServiceStrategy = Win32ServiceStrategy;
|
|
550
|
+
// Backward-compat: keep generateTaskXml exported for any external callers.
|
|
551
|
+
// Generates a single-task XML (legacy single-project form) — no longer used by
|
|
552
|
+
// the multi-project install path above.
|
|
553
|
+
function generateTaskXml(options) {
|
|
554
|
+
const { nodePath, entryPoint, verbose, docker } = options;
|
|
555
|
+
const args = [entryPoint, 'start'];
|
|
556
|
+
if (!docker)
|
|
557
|
+
args.push(constants_1.CLI_FLAG_NO_DOCKER);
|
|
558
|
+
if (verbose)
|
|
559
|
+
args.push(constants_1.CLI_FLAG_VERBOSE);
|
|
560
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
561
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
562
|
+
<Triggers>
|
|
563
|
+
<LogonTrigger>
|
|
564
|
+
<Enabled>true</Enabled>
|
|
565
|
+
</LogonTrigger>
|
|
566
|
+
</Triggers>
|
|
567
|
+
<Settings>
|
|
568
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
569
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
570
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
571
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
572
|
+
<RestartOnFailure>
|
|
573
|
+
<Interval>PT1M</Interval>
|
|
574
|
+
<Count>999</Count>
|
|
575
|
+
</RestartOnFailure>
|
|
576
|
+
</Settings>
|
|
577
|
+
<Actions>
|
|
578
|
+
<Exec>
|
|
579
|
+
<Command>${(0, escape_xml_1.escapeXml)(nodePath)}</Command>
|
|
580
|
+
<Arguments>${(0, escape_xml_1.escapeXml)(args.join(' '))}</Arguments>
|
|
581
|
+
</Exec>
|
|
582
|
+
</Actions>
|
|
583
|
+
</Task>`;
|
|
584
|
+
}
|
|
217
585
|
//# sourceMappingURL=win32-service.js.map
|