dotcms 0.2.0 → 26.9.3-1-next.2657
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/index.js +681 -320
- package/package.json +23 -22
package/index.js
CHANGED
|
@@ -45,9 +45,6 @@ var confirmExclude = (files) => ask(
|
|
|
45
45
|
`Add ${files.length === 1 ? "this file" : `these ${files.length} files`} to .gitignore? They contain an access token.`
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
-
// libs/sdk/cli/src/commands/agent/setup.ts
|
|
49
|
-
import { parse as parseToml } from "smol-toml";
|
|
50
|
-
|
|
51
48
|
// libs/sdk/cli/src/commands/agent/connect.ts
|
|
52
49
|
import * as childProcess from "node:child_process";
|
|
53
50
|
|
|
@@ -60,25 +57,49 @@ var SERVER_ENV = {
|
|
|
60
57
|
};
|
|
61
58
|
var SKILLS_SOURCE = "dotCMS/agent-toolkit";
|
|
62
59
|
|
|
60
|
+
// libs/sdk/cli/src/shared/env.ts
|
|
61
|
+
var ENV_KEYS = {
|
|
62
|
+
url: "DOTCMS_URL",
|
|
63
|
+
password: "DOTCMS_PASSWORD",
|
|
64
|
+
authToken: "DOTCMS_AUTH_TOKEN",
|
|
65
|
+
codexHome: "CODEX_HOME"
|
|
66
|
+
};
|
|
67
|
+
function readEnv(key) {
|
|
68
|
+
const value = process.env[key];
|
|
69
|
+
return value && value.trim() !== "" ? value : void 0;
|
|
70
|
+
}
|
|
71
|
+
function envWithoutSecrets(extra = {}) {
|
|
72
|
+
const clean = { ...process.env };
|
|
73
|
+
delete clean[ENV_KEYS.authToken];
|
|
74
|
+
delete clean[ENV_KEYS.password];
|
|
75
|
+
return { ...clean, ...extra };
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
// libs/sdk/cli/src/commands/agent/connect.ts
|
|
64
79
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
65
|
-
function classify(stderr
|
|
80
|
+
function classify(stderr) {
|
|
66
81
|
if (/404|E404|not found|ETARGET|ENOTFOUND|registry/i.test(stderr))
|
|
67
82
|
return "fetch-failed";
|
|
68
83
|
if (/Unsupported engine|requires Node|SyntaxError|Unexpected token/i.test(stderr)) {
|
|
69
84
|
return "runtime-unsupported";
|
|
70
85
|
}
|
|
71
|
-
return
|
|
86
|
+
return "exited";
|
|
72
87
|
}
|
|
73
88
|
async function confirmConnection(args) {
|
|
74
89
|
const timeoutMs = args.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
75
90
|
const child = childProcess.spawn("npx", ["-y", MCP_SERVER_PACKAGE], {
|
|
76
91
|
stdio: ["pipe", "pipe", "pipe"],
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
// `npx` is `npx.cmd` on Windows and Node refuses `.cmd` without a shell since the
|
|
93
|
+
// CVE-2024-27980 fix — so FR-024a failed on every Windows run, reporting a broken
|
|
94
|
+
// server for a configuration that was written correctly.
|
|
95
|
+
shell: process.platform === "win32",
|
|
96
|
+
// This child needs the URL and the token; it has no use for the developer's
|
|
97
|
+
// DOTCMS_PASSWORD, and it is an unpinned `@latest` package by design — so the smaller
|
|
98
|
+
// the environment it sees, the better.
|
|
99
|
+
env: envWithoutSecrets({
|
|
79
100
|
[SERVER_ENV.url]: args.url,
|
|
80
101
|
[SERVER_ENV.token]: args.token
|
|
81
|
-
}
|
|
102
|
+
})
|
|
82
103
|
});
|
|
83
104
|
let stderr = "";
|
|
84
105
|
child.stderr?.on("data", (chunk) => {
|
|
@@ -92,23 +113,48 @@ async function confirmConnection(args) {
|
|
|
92
113
|
settled = true;
|
|
93
114
|
clearTimeout(timer);
|
|
94
115
|
try {
|
|
95
|
-
child.
|
|
116
|
+
child.stdin?.end();
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
if (process.platform === "win32" && child.pid) {
|
|
121
|
+
childProcess.spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"]);
|
|
122
|
+
} else {
|
|
123
|
+
child.kill();
|
|
124
|
+
}
|
|
96
125
|
} catch {
|
|
97
126
|
}
|
|
98
127
|
resolve2(result);
|
|
99
128
|
};
|
|
100
129
|
const timer = setTimeout(
|
|
101
|
-
() => finish({
|
|
130
|
+
() => finish({
|
|
131
|
+
ok: false,
|
|
132
|
+
cause: "timeout",
|
|
133
|
+
detail: `No response within ${timeoutMs}ms.`
|
|
134
|
+
}),
|
|
102
135
|
timeoutMs
|
|
103
136
|
);
|
|
104
137
|
let buffer = "";
|
|
105
138
|
child.stdout?.on("data", (chunk) => {
|
|
106
139
|
buffer += String(chunk);
|
|
107
|
-
|
|
140
|
+
const frames = buffer.split("\n");
|
|
141
|
+
buffer = frames.pop() ?? "";
|
|
142
|
+
for (const line of frames) {
|
|
108
143
|
if (!line.trim())
|
|
109
144
|
continue;
|
|
110
145
|
try {
|
|
111
146
|
const message = JSON.parse(line);
|
|
147
|
+
if (message.id === 1) {
|
|
148
|
+
child.stdin?.write(
|
|
149
|
+
`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}
|
|
150
|
+
`
|
|
151
|
+
);
|
|
152
|
+
child.stdin?.write(
|
|
153
|
+
`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" })}
|
|
154
|
+
`
|
|
155
|
+
);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
112
158
|
const tools = message.result?.tools;
|
|
113
159
|
if (Array.isArray(tools))
|
|
114
160
|
finish({ ok: true, toolCount: tools.length });
|
|
@@ -124,7 +170,7 @@ async function confirmConnection(args) {
|
|
|
124
170
|
"exit",
|
|
125
171
|
(code) => finish({
|
|
126
172
|
ok: false,
|
|
127
|
-
cause: classify(stderr
|
|
173
|
+
cause: classify(stderr),
|
|
128
174
|
detail: stderr.trim().split("\n").slice(-1)[0] || `Server exited with code ${code}.`
|
|
129
175
|
})
|
|
130
176
|
);
|
|
@@ -133,100 +179,32 @@ async function confirmConnection(args) {
|
|
|
133
179
|
jsonrpc: "2.0",
|
|
134
180
|
id: 1,
|
|
135
181
|
method: "initialize",
|
|
136
|
-
params: {
|
|
182
|
+
params: {
|
|
183
|
+
protocolVersion: "2024-11-05",
|
|
184
|
+
capabilities: {},
|
|
185
|
+
clientInfo: { name: "dotcms", version: "0" }
|
|
186
|
+
}
|
|
137
187
|
})}
|
|
138
188
|
`
|
|
139
189
|
);
|
|
140
|
-
child.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" })}
|
|
141
|
-
`);
|
|
142
190
|
});
|
|
143
191
|
}
|
|
144
192
|
|
|
145
193
|
// libs/sdk/cli/src/commands/agent/gitignore.ts
|
|
194
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
146
195
|
import { existsSync } from "node:fs";
|
|
147
|
-
import * as fs from "node:fs/promises";
|
|
148
|
-
import * as path from "node:path";
|
|
149
|
-
var CONVENTIONALLY_COMMITTED = /* @__PURE__ */ new Set([".mcp.json"]);
|
|
150
|
-
function findRepositoryRoot(from) {
|
|
151
|
-
let dir = path.resolve(from);
|
|
152
|
-
for (; ; ) {
|
|
153
|
-
if (existsSync(path.join(dir, ".git")))
|
|
154
|
-
return dir;
|
|
155
|
-
const parent = path.dirname(dir);
|
|
156
|
-
if (parent === dir)
|
|
157
|
-
return null;
|
|
158
|
-
dir = parent;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
async function protectFromVersionControl(args) {
|
|
162
|
-
const warnings = [];
|
|
163
|
-
const root = findRepositoryRoot(args.cwd);
|
|
164
|
-
for (const file of args.files) {
|
|
165
|
-
if (CONVENTIONALLY_COMMITTED.has(path.basename(file))) {
|
|
166
|
-
warnings.push(
|
|
167
|
-
`${path.basename(file)} is normally committed to version control \u2014 it now holds a token, so committing it would publish that token.`
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (!root) {
|
|
172
|
-
warnings.push(
|
|
173
|
-
"This directory is not under version control, so these files are unprotected \u2014 nothing here can exclude them for you."
|
|
174
|
-
);
|
|
175
|
-
return { files: args.files, inRepository: false, excluded: false, warnings };
|
|
176
|
-
}
|
|
177
|
-
const proceed = args.confirmExclude ? await args.confirmExclude(args.files) : false;
|
|
178
|
-
if (!proceed) {
|
|
179
|
-
return { files: args.files, inRepository: true, excluded: false, warnings };
|
|
180
|
-
}
|
|
181
|
-
const gitignorePath = path.join(root, ".gitignore");
|
|
182
|
-
let current = "";
|
|
183
|
-
try {
|
|
184
|
-
current = await fs.readFile(gitignorePath, "utf8");
|
|
185
|
-
} catch {
|
|
186
|
-
}
|
|
187
|
-
const already = new Set(current.split("\n").map((line) => line.trim()));
|
|
188
|
-
const toAdd = args.files.map((file) => path.relative(root, file).split(path.sep).join("/")).filter((entry) => !already.has(entry));
|
|
189
|
-
if (toAdd.length) {
|
|
190
|
-
const prefix = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
191
|
-
const block = `${prefix}
|
|
192
|
-
# dotCMS agent configuration \u2014 contains an access token
|
|
193
|
-
${toAdd.join("\n")}
|
|
194
|
-
`;
|
|
195
|
-
await fs.writeFile(gitignorePath, current + block, "utf8");
|
|
196
|
-
}
|
|
197
|
-
return { files: args.files, inRepository: true, excluded: true, warnings };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// libs/sdk/cli/src/commands/agent/skills.ts
|
|
201
|
-
import * as childProcess2 from "node:child_process";
|
|
202
|
-
function buildSkillsArgs(agentIds, global) {
|
|
203
|
-
return [
|
|
204
|
-
"-y",
|
|
205
|
-
"skills",
|
|
206
|
-
"add",
|
|
207
|
-
SKILLS_SOURCE,
|
|
208
|
-
...agentIds.flatMap((id) => ["-a", id]),
|
|
209
|
-
...global ? ["-g"] : [],
|
|
210
|
-
"-y"
|
|
211
|
-
];
|
|
212
|
-
}
|
|
213
|
-
async function installSkills(args) {
|
|
214
|
-
const argv = buildSkillsArgs(args.agentIds, args.global);
|
|
215
|
-
const command = `npx ${argv.join(" ")}`;
|
|
216
|
-
try {
|
|
217
|
-
const result = childProcess2.spawnSync("npx", argv, { stdio: "inherit" });
|
|
218
|
-
if (result.status === 0)
|
|
219
|
-
return { ok: true, command };
|
|
220
|
-
return { ok: false, command, reason: `skills exited with code ${result.status}` };
|
|
221
|
-
} catch (error) {
|
|
222
|
-
return { ok: false, command, reason: error.message };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// libs/sdk/cli/src/shared/config-file.ts
|
|
227
196
|
import * as fs2 from "node:fs/promises";
|
|
228
197
|
import * as path2 from "node:path";
|
|
229
198
|
|
|
199
|
+
// libs/sdk/cli/src/shared/config-file.ts
|
|
200
|
+
import {
|
|
201
|
+
findNodeAtLocation,
|
|
202
|
+
parse as parseJsonc,
|
|
203
|
+
parseTree
|
|
204
|
+
} from "jsonc-parser";
|
|
205
|
+
import * as fs from "node:fs/promises";
|
|
206
|
+
import * as path from "node:path";
|
|
207
|
+
|
|
230
208
|
// libs/sdk/cli/src/shared/errors.ts
|
|
231
209
|
var CliError = class extends Error {
|
|
232
210
|
constructor(message) {
|
|
@@ -236,22 +214,52 @@ var CliError = class extends Error {
|
|
|
236
214
|
};
|
|
237
215
|
var UsageError = class extends CliError {
|
|
238
216
|
};
|
|
217
|
+
function withoutCredentials(value) {
|
|
218
|
+
try {
|
|
219
|
+
const u = new URL(value);
|
|
220
|
+
u.username = "";
|
|
221
|
+
u.password = "";
|
|
222
|
+
const path5 = u.pathname === "/" && !/\/$/.test(value) ? "" : u.pathname;
|
|
223
|
+
return `${u.protocol}//${u.host}${path5}${u.search}`;
|
|
224
|
+
} catch {
|
|
225
|
+
return value.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/]*@/i, "$1");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
239
228
|
var InvalidUrlError = class extends CliError {
|
|
240
229
|
constructor(value) {
|
|
241
|
-
const
|
|
230
|
+
const safe = withoutCredentials(value);
|
|
231
|
+
const host = safe.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/^\/+/, "");
|
|
242
232
|
super(
|
|
243
|
-
`"${
|
|
233
|
+
`"${safe}" is not a valid instance address. Pass it with an http:// or https:// scheme, e.g. https://${host || "demo.dotcms.com"}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var CredentialInUrlError = class extends CliError {
|
|
238
|
+
constructor(url) {
|
|
239
|
+
const safe = withoutCredentials(url);
|
|
240
|
+
super(
|
|
241
|
+
`The instance address must not carry a username or password. Use ${safe} on its own \u2014 setup will ask you to sign in, or pass --authToken.`
|
|
244
242
|
);
|
|
245
243
|
}
|
|
246
244
|
};
|
|
247
245
|
var InstanceUnreachableError = class extends CliError {
|
|
248
246
|
constructor(url, reason) {
|
|
249
|
-
super(
|
|
247
|
+
super(
|
|
248
|
+
`Could not reach ${url} \u2014 ${reason}. Check the address and that the instance is running.`
|
|
249
|
+
);
|
|
250
250
|
}
|
|
251
251
|
};
|
|
252
252
|
var NotADotCmsInstanceError = class extends CliError {
|
|
253
253
|
constructor(url) {
|
|
254
|
-
|
|
254
|
+
let hint = " Check the address.";
|
|
255
|
+
try {
|
|
256
|
+
const parsed = new URL(url);
|
|
257
|
+
if (parsed.pathname !== "/" && parsed.pathname !== "") {
|
|
258
|
+
hint = ` That address includes a path; the instance itself is probably ${parsed.origin}.`;
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
super(`${url} is not a valid dotCMS instance.${hint}`);
|
|
255
263
|
}
|
|
256
264
|
};
|
|
257
265
|
var CredentialsRejectedError = class extends CliError {
|
|
@@ -284,20 +292,31 @@ var ConflictingAuthError = class extends UsageError {
|
|
|
284
292
|
};
|
|
285
293
|
var MissingInputError = class extends UsageError {
|
|
286
294
|
constructor(what) {
|
|
287
|
-
super(
|
|
295
|
+
super(
|
|
296
|
+
`${what} is required and there is no terminal to prompt on. Pass it as an option or set its environment variable.`
|
|
297
|
+
);
|
|
288
298
|
}
|
|
289
299
|
};
|
|
290
300
|
var NoConfigPathError = class extends CliError {
|
|
291
301
|
constructor(displayName, scope) {
|
|
292
302
|
super(
|
|
293
|
-
`${displayName} has no configuration file at ${scope} scope. Re-run
|
|
303
|
+
`${displayName} has no configuration file at ${scope} scope. Re-run ${scope === "folder" ? "with -g/--global" : "without -g/--global"}, or drop it from --agent.`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var UnreadableConfigError = class extends CliError {
|
|
308
|
+
constructor(file, reason) {
|
|
309
|
+
super(
|
|
310
|
+
`${file} could not be read \u2014 ${reason}. Nothing was changed. Check the file's permissions, or re-run with --skip-mcp.`
|
|
294
311
|
);
|
|
295
312
|
}
|
|
296
313
|
};
|
|
297
314
|
var MalformedConfigError = class extends CliError {
|
|
298
|
-
|
|
315
|
+
/** The format is passed, not sniffed from the extension: both callers know exactly which
|
|
316
|
+
* parser just refused the file. */
|
|
317
|
+
constructor(file, format = "JSON") {
|
|
299
318
|
super(
|
|
300
|
-
`${file} is not valid ${
|
|
319
|
+
`${file} is not valid ${format} \u2014 fix it or re-run with --skip-mcp. It has been left untouched.`
|
|
301
320
|
);
|
|
302
321
|
}
|
|
303
322
|
};
|
|
@@ -306,105 +325,243 @@ var MalformedConfigError = class extends CliError {
|
|
|
306
325
|
var CAN_RESTRICT = process.platform !== "win32";
|
|
307
326
|
var FILE_MODE = 384;
|
|
308
327
|
var DIR_MODE = 448;
|
|
309
|
-
async function
|
|
310
|
-
let raw;
|
|
328
|
+
async function readFileIfPresent(file) {
|
|
311
329
|
try {
|
|
312
|
-
|
|
330
|
+
return await fs.readFile(file, "utf8");
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (error.code === "ENOENT")
|
|
333
|
+
return null;
|
|
334
|
+
throw new UnreadableConfigError(file, error.message);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async function writeFileAtomic(file, contents) {
|
|
338
|
+
const temp = `${file}.dotcms-${process.pid}.tmp`;
|
|
339
|
+
try {
|
|
340
|
+
await fs.writeFile(temp, contents, { encoding: "utf8", mode: FILE_MODE });
|
|
313
341
|
} catch {
|
|
314
|
-
|
|
342
|
+
await fs.writeFile(file, contents, { encoding: "utf8", mode: FILE_MODE });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await fs.rename(temp, file);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
await fs.rm(temp, { force: true });
|
|
349
|
+
throw error;
|
|
315
350
|
}
|
|
351
|
+
}
|
|
352
|
+
var PARSE_OPTIONS = { allowTrailingComma: true, allowEmptyContent: true };
|
|
353
|
+
function parseOrThrow(raw, file) {
|
|
316
354
|
if (raw.trim() === "")
|
|
317
355
|
return {};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
356
|
+
const errors = [];
|
|
357
|
+
const doc = parseJsonc(raw, errors, PARSE_OPTIONS);
|
|
358
|
+
if (errors.length > 0 || doc === void 0)
|
|
359
|
+
throw new MalformedConfigError(file);
|
|
360
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
321
361
|
throw new MalformedConfigError(file);
|
|
322
362
|
}
|
|
363
|
+
return doc;
|
|
364
|
+
}
|
|
365
|
+
async function readJsonDocument(file) {
|
|
366
|
+
const raw = await readFileIfPresent(file);
|
|
367
|
+
return raw === null ? null : parseOrThrow(raw, file);
|
|
368
|
+
}
|
|
369
|
+
function detectIndent(raw) {
|
|
370
|
+
const match = raw.match(/\n([ \t]+)\S/);
|
|
371
|
+
if (!match)
|
|
372
|
+
return { insertSpaces: true, tabSize: 2 };
|
|
373
|
+
const indent = match[1];
|
|
374
|
+
return indent.startsWith(" ") ? { insertSpaces: false, tabSize: 1 } : { insertSpaces: true, tabSize: indent.length };
|
|
323
375
|
}
|
|
324
376
|
async function hasEntry(args) {
|
|
325
|
-
|
|
326
|
-
if (args.parse) {
|
|
327
|
-
try {
|
|
328
|
-
const raw = await fs2.readFile(args.file, "utf8");
|
|
329
|
-
doc = raw.trim() === "" ? {} : args.parse(raw);
|
|
330
|
-
} catch {
|
|
331
|
-
return false;
|
|
332
|
-
}
|
|
333
|
-
} else {
|
|
334
|
-
doc = await readJsonDocument(args.file);
|
|
335
|
-
}
|
|
377
|
+
const doc = await readJsonDocument(args.file).catch(() => null);
|
|
336
378
|
const container = doc?.[args.containerKey] ?? {};
|
|
337
379
|
return Object.prototype.hasOwnProperty.call(container, args.entryKey);
|
|
338
380
|
}
|
|
339
381
|
async function restrictFile(file, canRestrict = CAN_RESTRICT) {
|
|
340
382
|
if (!canRestrict)
|
|
341
383
|
return false;
|
|
342
|
-
await
|
|
384
|
+
await fs.chmod(file, FILE_MODE);
|
|
343
385
|
return true;
|
|
344
386
|
}
|
|
345
387
|
async function ensureDir(dir) {
|
|
346
|
-
const created = await
|
|
388
|
+
const created = await fs.mkdir(dir, { recursive: true });
|
|
347
389
|
if (created && CAN_RESTRICT)
|
|
348
|
-
await
|
|
390
|
+
await fs.chmod(dir, DIR_MODE).catch(() => void 0);
|
|
391
|
+
}
|
|
392
|
+
function renderAt(value, unit, depth) {
|
|
393
|
+
const base = unit.repeat(depth);
|
|
394
|
+
return JSON.stringify(value, null, unit).split("\n").map((line, i) => i === 0 ? line : base + line).join("\n");
|
|
395
|
+
}
|
|
396
|
+
function setProperty(raw, object, key, value, unit, depth) {
|
|
397
|
+
const rendered = renderAt(value, unit, depth);
|
|
398
|
+
const properties = object.children ?? [];
|
|
399
|
+
const existing = properties.find((p) => p.children?.[0]?.value === key);
|
|
400
|
+
if (existing?.children?.[1]) {
|
|
401
|
+
const node = existing.children[1];
|
|
402
|
+
return raw.slice(0, node.offset) + rendered + raw.slice(node.offset + node.length);
|
|
403
|
+
}
|
|
404
|
+
const insertion = `"${key}": ${rendered}`;
|
|
405
|
+
const last = properties[properties.length - 1];
|
|
406
|
+
if (last) {
|
|
407
|
+
const end = last.offset + last.length;
|
|
408
|
+
return `${raw.slice(0, end)},
|
|
409
|
+
${unit.repeat(depth)}${insertion}${raw.slice(end)}`;
|
|
410
|
+
}
|
|
411
|
+
const close = raw.lastIndexOf("}", object.offset + object.length);
|
|
412
|
+
return `${raw.slice(0, object.offset + 1)}
|
|
413
|
+
${unit.repeat(depth)}${insertion}
|
|
414
|
+
${unit.repeat(depth - 1)}${raw.slice(close)}`;
|
|
349
415
|
}
|
|
350
416
|
async function writeMerged(args) {
|
|
351
|
-
const
|
|
417
|
+
const raw = await readFileIfPresent(args.file);
|
|
418
|
+
const existing = raw === null ? {} : parseOrThrow(raw, args.file);
|
|
352
419
|
const container = existing[args.containerKey] ?? {};
|
|
353
420
|
const replacedExisting = Object.prototype.hasOwnProperty.call(container, args.entryKey);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
421
|
+
let next;
|
|
422
|
+
const tree = raw === null ? void 0 : parseTree(raw, [], PARSE_OPTIONS);
|
|
423
|
+
if (raw === null || raw.trim() === "" || tree?.type !== "object") {
|
|
424
|
+
next = `${JSON.stringify({ [args.containerKey]: { [args.entryKey]: args.entry } }, null, 2)}
|
|
425
|
+
`;
|
|
426
|
+
} else {
|
|
427
|
+
const { insertSpaces, tabSize } = detectIndent(raw);
|
|
428
|
+
const unit = insertSpaces ? " ".repeat(tabSize) : " ";
|
|
429
|
+
const containerNode = findNodeAtLocation(tree, [args.containerKey]);
|
|
430
|
+
next = containerNode?.type === "object" ? setProperty(raw, containerNode, args.entryKey, args.entry, unit, 2) : setProperty(
|
|
431
|
+
raw,
|
|
432
|
+
tree,
|
|
433
|
+
args.containerKey,
|
|
434
|
+
{ [args.entryKey]: args.entry },
|
|
435
|
+
unit,
|
|
436
|
+
1
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
await ensureDir(path.dirname(args.file));
|
|
440
|
+
await writeFileAtomic(args.file, next);
|
|
361
441
|
const permissionsApplied = await restrictFile(args.file, args.canRestrict ?? CAN_RESTRICT);
|
|
362
442
|
return { path: args.file, permissionsApplied, replacedExisting };
|
|
363
443
|
}
|
|
364
444
|
|
|
365
|
-
// libs/sdk/cli/src/commands/agent/
|
|
366
|
-
function
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
445
|
+
// libs/sdk/cli/src/commands/agent/gitignore.ts
|
|
446
|
+
function asPattern(relative2) {
|
|
447
|
+
const escaped = relative2.replace(/([\\*?[\]])/g, "\\$1").replace(/ $/, "\\ ");
|
|
448
|
+
return `/${escaped}`;
|
|
449
|
+
}
|
|
450
|
+
function confirmIgnored(root, files) {
|
|
451
|
+
const missed = [];
|
|
452
|
+
for (const file of files) {
|
|
453
|
+
const result = spawnSync2("git", ["check-ignore", "-q", file], { cwd: root });
|
|
454
|
+
if (result.error || result.status === null || result.status > 1)
|
|
455
|
+
return null;
|
|
456
|
+
if (result.status === 1)
|
|
457
|
+
missed.push(path2.basename(file));
|
|
375
458
|
}
|
|
376
|
-
return
|
|
459
|
+
return missed;
|
|
377
460
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
461
|
+
function findRepositoryRoot(from) {
|
|
462
|
+
let dir = path2.resolve(from);
|
|
463
|
+
for (; ; ) {
|
|
464
|
+
if (existsSync(path2.join(dir, ".git")))
|
|
465
|
+
return dir;
|
|
466
|
+
const parent = path2.dirname(dir);
|
|
467
|
+
if (parent === dir)
|
|
468
|
+
return null;
|
|
469
|
+
dir = parent;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function protectFromVersionControl(args) {
|
|
473
|
+
const warnings = [];
|
|
474
|
+
const root = findRepositoryRoot(args.cwd);
|
|
475
|
+
const committed = new Set(args.committedByConvention ?? []);
|
|
476
|
+
for (const file of args.files) {
|
|
477
|
+
if (committed.has(file)) {
|
|
478
|
+
warnings.push(
|
|
479
|
+
`${path2.basename(file)} is normally committed to version control \u2014 it now holds a token, so committing it would publish that token.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!root) {
|
|
484
|
+
warnings.push(
|
|
485
|
+
"This directory is not under version control, so these files are unprotected \u2014 nothing here can exclude them for you."
|
|
486
|
+
);
|
|
487
|
+
return { files: args.files, inRepository: false, excluded: false, warnings };
|
|
488
|
+
}
|
|
489
|
+
const proceed = args.confirmExclude ? await args.confirmExclude(args.files) : false;
|
|
490
|
+
if (!proceed) {
|
|
491
|
+
return { files: args.files, inRepository: true, excluded: false, warnings };
|
|
492
|
+
}
|
|
493
|
+
const gitignorePath = path2.join(root, ".gitignore");
|
|
494
|
+
const current = await readFileIfPresent(gitignorePath) ?? "";
|
|
495
|
+
const already = new Set(current.split("\n").map((line) => line.trim()));
|
|
496
|
+
const relatives = args.files.map((file) => path2.relative(root, file).split(path2.sep).join("/"));
|
|
497
|
+
const toAdd = relatives.filter((rel) => !already.has(rel) && !already.has(asPattern(rel)));
|
|
498
|
+
if (toAdd.length) {
|
|
499
|
+
const prefix = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
500
|
+
const block = `${prefix}
|
|
501
|
+
# dotCMS agent configuration \u2014 contains an access token
|
|
502
|
+
${toAdd.map(asPattern).join("\n")}
|
|
503
|
+
`;
|
|
504
|
+
await fs2.writeFile(gitignorePath, current + block, "utf8");
|
|
505
|
+
}
|
|
506
|
+
const unverified = confirmIgnored(root, args.files);
|
|
507
|
+
if (unverified === null) {
|
|
508
|
+
warnings.push(
|
|
509
|
+
"Could not confirm with git that these files are excluded \u2014 check `git status` before committing."
|
|
510
|
+
);
|
|
511
|
+
} else if (unverified.length) {
|
|
512
|
+
warnings.push(
|
|
513
|
+
`.gitignore was written but git still tracks: ${unverified.join(", ")}. Exclude them by hand before committing.`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
files: args.files,
|
|
518
|
+
inRepository: true,
|
|
519
|
+
excluded: unverified !== null && unverified.length === 0,
|
|
520
|
+
warnings
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// libs/sdk/cli/src/commands/agent/skills.ts
|
|
525
|
+
import * as childProcess2 from "node:child_process";
|
|
526
|
+
function buildSkillsArgs(agentIds, global) {
|
|
527
|
+
return [
|
|
528
|
+
"-y",
|
|
529
|
+
"skills",
|
|
530
|
+
"add",
|
|
531
|
+
SKILLS_SOURCE,
|
|
532
|
+
...agentIds.flatMap((id) => ["-a", id]),
|
|
533
|
+
...global ? ["-g"] : [],
|
|
534
|
+
"-y"
|
|
535
|
+
];
|
|
536
|
+
}
|
|
537
|
+
async function installSkills(args) {
|
|
538
|
+
const argv = buildSkillsArgs(args.agentIds, args.global);
|
|
539
|
+
const command = `npx ${argv.join(" ")}`;
|
|
540
|
+
try {
|
|
541
|
+
const result = childProcess2.spawnSync("npx", argv, {
|
|
542
|
+
stdio: "inherit",
|
|
543
|
+
// The doc comment above says no secret is passed; `spawnSync` defaults to the whole
|
|
544
|
+
// environment, so it was not true until this line.
|
|
545
|
+
env: envWithoutSecrets(),
|
|
546
|
+
// On Windows `npx` is `npx.cmd`, and since the CVE-2024-27980 fix Node refuses to
|
|
547
|
+
// execute `.cmd`/`.bat` without a shell — so this spawn failed on every Windows run
|
|
548
|
+
// and FR-025 never installed anything there.
|
|
549
|
+
shell: process.platform === "win32"
|
|
550
|
+
});
|
|
551
|
+
if (result.error)
|
|
552
|
+
return { ok: false, command, reason: result.error.message };
|
|
553
|
+
if (result.status === 0)
|
|
554
|
+
return { ok: true, command };
|
|
555
|
+
return { ok: false, command, reason: `skills exited with code ${result.status}` };
|
|
556
|
+
} catch (error) {
|
|
557
|
+
return { ok: false, command, reason: error.message };
|
|
558
|
+
}
|
|
388
559
|
}
|
|
389
560
|
|
|
390
561
|
// libs/sdk/cli/src/commands/agent/targets/registry.ts
|
|
391
562
|
import { existsSync as existsSync2 } from "node:fs";
|
|
392
563
|
import * as os from "node:os";
|
|
393
564
|
import * as path3 from "node:path";
|
|
394
|
-
|
|
395
|
-
// libs/sdk/cli/src/shared/env.ts
|
|
396
|
-
var ENV_KEYS = {
|
|
397
|
-
url: "DOTCMS_URL",
|
|
398
|
-
password: "DOTCMS_PASSWORD",
|
|
399
|
-
authToken: "DOTCMS_AUTH_TOKEN",
|
|
400
|
-
codexHome: "CODEX_HOME"
|
|
401
|
-
};
|
|
402
|
-
function readEnv(key) {
|
|
403
|
-
const value = process.env[key];
|
|
404
|
-
return value && value.trim() !== "" ? value : void 0;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// libs/sdk/cli/src/commands/agent/targets/registry.ts
|
|
408
565
|
var home = () => os.homedir();
|
|
409
566
|
var inHome = (...parts) => path3.join(home(), ...parts);
|
|
410
567
|
var inFolder = (cwd, ...parts) => path3.join(cwd ?? process.cwd(), ...parts);
|
|
@@ -431,6 +588,7 @@ var TARGETS = [
|
|
|
431
588
|
containerKey: "mcpServers",
|
|
432
589
|
entryShape: "stdio",
|
|
433
590
|
detect: probe(".claude"),
|
|
591
|
+
folderConfigIsCommitted: true,
|
|
434
592
|
configPath: (scope, cwd) => scope === "global" ? inHome(".claude.json") : inFolder(cwd, ".mcp.json")
|
|
435
593
|
},
|
|
436
594
|
{
|
|
@@ -519,12 +677,48 @@ async function detectTargets() {
|
|
|
519
677
|
return results.filter((t) => t !== null);
|
|
520
678
|
}
|
|
521
679
|
|
|
680
|
+
// libs/sdk/cli/src/commands/agent/targets/entry.ts
|
|
681
|
+
function buildEntry(target, url, token) {
|
|
682
|
+
const env = { [SERVER_ENV.url]: url, [SERVER_ENV.token]: token };
|
|
683
|
+
if (target.entryShape === "opencode-local") {
|
|
684
|
+
return {
|
|
685
|
+
type: "local",
|
|
686
|
+
command: ["npx", "-y", MCP_SERVER_PACKAGE],
|
|
687
|
+
enabled: true,
|
|
688
|
+
environment: env
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return { type: "stdio", command: "npx", args: ["-y", MCP_SERVER_PACKAGE], env };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// libs/sdk/cli/src/commands/agent/targets/json-target.ts
|
|
695
|
+
function hasJsonEntry(file, target) {
|
|
696
|
+
return hasEntry({ file, containerKey: target.containerKey, entryKey: ENTRY_KEY });
|
|
697
|
+
}
|
|
698
|
+
async function writeJsonTarget(args) {
|
|
699
|
+
const file = args.target.configPath(args.scope, args.cwd);
|
|
700
|
+
if (!file)
|
|
701
|
+
throw new NoConfigPathError(args.target.displayName, args.scope);
|
|
702
|
+
return writeMerged({
|
|
703
|
+
file,
|
|
704
|
+
containerKey: args.target.containerKey,
|
|
705
|
+
entryKey: ENTRY_KEY,
|
|
706
|
+
entry: buildEntry(args.target, args.url, args.token)
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
522
710
|
// libs/sdk/cli/src/commands/agent/targets/toml-target.ts
|
|
523
711
|
import { parse, stringify } from "smol-toml";
|
|
524
|
-
import * as fs3 from "node:fs/promises";
|
|
525
712
|
import * as path4 from "node:path";
|
|
713
|
+
async function hasTomlEntry(file, target) {
|
|
714
|
+
const raw = await readFileIfPresent(file);
|
|
715
|
+
return raw === null ? false : findEntrySpan(raw.split("\n"), target.containerKey) !== null;
|
|
716
|
+
}
|
|
526
717
|
function findEntrySpan(lines, containerKey) {
|
|
527
|
-
const
|
|
718
|
+
const spelt = (key) => `(?:${key}|"${key}"|'${key}')`;
|
|
719
|
+
const ours = new RegExp(
|
|
720
|
+
`^\\s*\\[\\s*${spelt(containerKey)}\\s*\\.\\s*${spelt(ENTRY_KEY)}\\s*(\\.[^\\]]*)?\\]`
|
|
721
|
+
);
|
|
528
722
|
const anyHeader = /^\s*\[/;
|
|
529
723
|
const start = lines.findIndex((line) => ours.test(line));
|
|
530
724
|
if (start === -1)
|
|
@@ -551,26 +745,24 @@ async function writeTomlTarget(args) {
|
|
|
551
745
|
if (!file) {
|
|
552
746
|
throw new NoConfigPathError(args.target.displayName, args.scope);
|
|
553
747
|
}
|
|
554
|
-
|
|
555
|
-
try {
|
|
556
|
-
original = await fs3.readFile(file, "utf8");
|
|
557
|
-
} catch {
|
|
558
|
-
}
|
|
748
|
+
const original = await readFileIfPresent(file) ?? "";
|
|
559
749
|
if (original.trim() !== "") {
|
|
560
750
|
try {
|
|
561
751
|
parse(original);
|
|
562
752
|
} catch {
|
|
563
|
-
throw new MalformedConfigError(file);
|
|
753
|
+
throw new MalformedConfigError(file, "TOML");
|
|
564
754
|
}
|
|
565
755
|
}
|
|
566
756
|
const block = renderEntry(args.target, args.url, args.token, args.target.containerKey);
|
|
567
757
|
let next;
|
|
758
|
+
let replacedExisting = false;
|
|
568
759
|
if (original.trim() === "") {
|
|
569
760
|
next = block;
|
|
570
761
|
} else {
|
|
571
762
|
const lines = original.split("\n");
|
|
572
763
|
const span = findEntrySpan(lines, args.target.containerKey);
|
|
573
764
|
if (span) {
|
|
765
|
+
replacedExisting = true;
|
|
574
766
|
lines.splice(span.start, span.end - span.start, ...block.trimEnd().split("\n"));
|
|
575
767
|
next = lines.join("\n");
|
|
576
768
|
} else {
|
|
@@ -579,10 +771,50 @@ async function writeTomlTarget(args) {
|
|
|
579
771
|
}
|
|
580
772
|
}
|
|
581
773
|
await ensureDir(path4.dirname(file));
|
|
582
|
-
await
|
|
583
|
-
|
|
584
|
-
await restrictFile(file);
|
|
585
|
-
|
|
774
|
+
await writeFileAtomic(file, next.endsWith("\n") ? next : `${next}
|
|
775
|
+
`);
|
|
776
|
+
return { path: file, permissionsApplied: await restrictFile(file), replacedExisting };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// libs/sdk/cli/src/commands/agent/targets/writers.ts
|
|
780
|
+
var WRITERS = {
|
|
781
|
+
json: { hasEntry: hasJsonEntry, write: writeJsonTarget },
|
|
782
|
+
toml: { hasEntry: hasTomlEntry, write: writeTomlTarget }
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// libs/http/src/lib/fetch-retry.ts
|
|
786
|
+
function isSuccessStatus(status) {
|
|
787
|
+
return status >= 200 && status < 300;
|
|
788
|
+
}
|
|
789
|
+
function describeRequestFailure(error) {
|
|
790
|
+
if (isHttpError(error)) {
|
|
791
|
+
if (error.code === "ECONNREFUSED") {
|
|
792
|
+
return "Connection refused - service not accepting connections yet";
|
|
793
|
+
}
|
|
794
|
+
if (error.code === "ETIMEDOUT") {
|
|
795
|
+
return "Connection timeout - service too slow or not responding";
|
|
796
|
+
}
|
|
797
|
+
if (error.code === "ENOTFOUND") {
|
|
798
|
+
return "Host not found (DNS lookup failed)";
|
|
799
|
+
}
|
|
800
|
+
if (error.code === "ECONNRESET") {
|
|
801
|
+
return "Connection reset by the server";
|
|
802
|
+
}
|
|
803
|
+
if (error.code === "CERT_HAS_EXPIRED") {
|
|
804
|
+
return "TLS certificate has expired";
|
|
805
|
+
}
|
|
806
|
+
if (error.code === "DEPTH_ZERO_SELF_SIGNED_CERT" || error.code === "SELF_SIGNED_CERT_IN_CHAIN") {
|
|
807
|
+
return "TLS certificate is self-signed and not trusted";
|
|
808
|
+
}
|
|
809
|
+
if (error.response) {
|
|
810
|
+
return `HTTP ${error.response.status}: ${error.response.statusText}`;
|
|
811
|
+
}
|
|
812
|
+
return error.code || error.message;
|
|
813
|
+
}
|
|
814
|
+
if (error instanceof Error) {
|
|
815
|
+
return error.message;
|
|
816
|
+
}
|
|
817
|
+
return String(error);
|
|
586
818
|
}
|
|
587
819
|
|
|
588
820
|
// libs/http/src/lib/http.ts
|
|
@@ -601,11 +833,37 @@ function isHttpError(error) {
|
|
|
601
833
|
return error instanceof HttpError;
|
|
602
834
|
}
|
|
603
835
|
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
|
|
836
|
+
var MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
837
|
+
async function readBody(response, url) {
|
|
838
|
+
const tooLarge = () => new HttpError(`Response from ${url} is too large (over ${MAX_BODY_BYTES} bytes)`, {
|
|
839
|
+
status: response.status,
|
|
840
|
+
code: "EBODYTOOLARGE"
|
|
841
|
+
});
|
|
842
|
+
const declared = Number(response.headers.get("content-length"));
|
|
843
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES)
|
|
844
|
+
throw tooLarge();
|
|
845
|
+
let text = "";
|
|
846
|
+
if (!response.body) {
|
|
847
|
+
text = await response.text().catch(() => "");
|
|
848
|
+
} else {
|
|
849
|
+
const reader = response.body.getReader();
|
|
850
|
+
const decoder = new TextDecoder();
|
|
851
|
+
let size = 0;
|
|
852
|
+
for (; ; ) {
|
|
853
|
+
const { done, value } = await reader.read();
|
|
854
|
+
if (done)
|
|
855
|
+
break;
|
|
856
|
+
if (!value)
|
|
857
|
+
continue;
|
|
858
|
+
size += value.byteLength;
|
|
859
|
+
if (size > MAX_BODY_BYTES) {
|
|
860
|
+
await reader.cancel().catch(() => void 0);
|
|
861
|
+
throw tooLarge();
|
|
862
|
+
}
|
|
863
|
+
text += decoder.decode(value, { stream: true });
|
|
864
|
+
}
|
|
865
|
+
text += decoder.decode();
|
|
866
|
+
}
|
|
609
867
|
if (!text) {
|
|
610
868
|
return void 0;
|
|
611
869
|
}
|
|
@@ -624,25 +882,38 @@ async function request(url, init, { token, timeoutMs = DEFAULT_TIMEOUT_MS2, acce
|
|
|
624
882
|
}
|
|
625
883
|
let response;
|
|
626
884
|
try {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
885
|
+
try {
|
|
886
|
+
response = await fetch(url, { ...init, headers, signal: controller.signal });
|
|
887
|
+
} catch (error) {
|
|
888
|
+
const aborted = error?.name === "AbortError";
|
|
889
|
+
const cause = error?.cause;
|
|
890
|
+
throw new HttpError(
|
|
891
|
+
aborted ? `Request to ${url} timed out after ${timeoutMs}ms` : `Request to ${url} failed: ${error?.message ?? String(error)}`,
|
|
892
|
+
{ status: null, code: aborted ? "ETIMEDOUT" : cause?.code }
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
let data;
|
|
896
|
+
try {
|
|
897
|
+
data = await readBody(response, url);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
if (isHttpError(error))
|
|
900
|
+
throw error;
|
|
901
|
+
const aborted = error?.name === "AbortError";
|
|
902
|
+
throw new HttpError(
|
|
903
|
+
aborted ? `Request to ${url} timed out after ${timeoutMs}ms while reading the response` : `Reading the response from ${url} failed: ${error?.message ?? String(error)}`,
|
|
904
|
+
{ status: response.status, code: aborted ? "ETIMEDOUT" : void 0 }
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
if (!isSuccessStatus(response.status) && !acceptAnyStatus) {
|
|
908
|
+
throw new HttpError(`Request failed with status code ${response.status}`, {
|
|
909
|
+
status: response.status,
|
|
910
|
+
statusText: response.statusText
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
return { status: response.status, data };
|
|
635
914
|
} finally {
|
|
636
915
|
clearTimeout(timer);
|
|
637
916
|
}
|
|
638
|
-
const data = await readBody(response);
|
|
639
|
-
if (!isSuccess(response.status) && !acceptAnyStatus) {
|
|
640
|
-
throw new HttpError(`Request failed with status code ${response.status}`, {
|
|
641
|
-
status: response.status,
|
|
642
|
-
statusText: response.statusText
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
return { status: response.status, data };
|
|
646
917
|
}
|
|
647
918
|
function httpGet(url, options = {}) {
|
|
648
919
|
return request(url, { method: "GET" }, options);
|
|
@@ -659,38 +930,6 @@ function httpPost(url, body, options = {}) {
|
|
|
659
930
|
);
|
|
660
931
|
}
|
|
661
932
|
|
|
662
|
-
// libs/http/src/lib/fetch-retry.ts
|
|
663
|
-
function describeRequestFailure(error) {
|
|
664
|
-
if (isHttpError(error)) {
|
|
665
|
-
if (error.code === "ECONNREFUSED") {
|
|
666
|
-
return "Connection refused - service not accepting connections yet";
|
|
667
|
-
}
|
|
668
|
-
if (error.code === "ETIMEDOUT") {
|
|
669
|
-
return "Connection timeout - service too slow or not responding";
|
|
670
|
-
}
|
|
671
|
-
if (error.code === "ENOTFOUND") {
|
|
672
|
-
return "Host not found (DNS lookup failed)";
|
|
673
|
-
}
|
|
674
|
-
if (error.code === "ECONNRESET") {
|
|
675
|
-
return "Connection reset by the server";
|
|
676
|
-
}
|
|
677
|
-
if (error.code === "CERT_HAS_EXPIRED") {
|
|
678
|
-
return "TLS certificate has expired";
|
|
679
|
-
}
|
|
680
|
-
if (error.code === "DEPTH_ZERO_SELF_SIGNED_CERT" || error.code === "SELF_SIGNED_CERT_IN_CHAIN") {
|
|
681
|
-
return "TLS certificate is self-signed and not trusted";
|
|
682
|
-
}
|
|
683
|
-
if (error.response) {
|
|
684
|
-
return `HTTP ${error.response.status}: ${error.response.statusText}`;
|
|
685
|
-
}
|
|
686
|
-
return error.code || error.message;
|
|
687
|
-
}
|
|
688
|
-
if (error instanceof Error) {
|
|
689
|
-
return error.message;
|
|
690
|
-
}
|
|
691
|
-
return String(error);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
933
|
// libs/http/src/lib/endpoints.ts
|
|
695
934
|
var DOTCMS_API = {
|
|
696
935
|
/** Reachability probe, and the source of the instance version. */
|
|
@@ -737,14 +976,29 @@ async function verifyToken(url, token) {
|
|
|
737
976
|
function normalizeUrl(url) {
|
|
738
977
|
return url.trim().replace(/\/+$/, "");
|
|
739
978
|
}
|
|
979
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
980
|
+
function insecureTransportWarning(url) {
|
|
981
|
+
let parsed;
|
|
982
|
+
try {
|
|
983
|
+
parsed = new URL(url);
|
|
984
|
+
} catch {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
if (parsed.protocol !== "http:" || LOOPBACK.has(parsed.hostname))
|
|
988
|
+
return null;
|
|
989
|
+
return `${parsed.host} is plain http, so your password and the access token cross the network in the clear. Use https:// if the instance supports it.`;
|
|
990
|
+
}
|
|
740
991
|
function validateUrl(url) {
|
|
741
992
|
if (!/^https?:\/\//i.test(url))
|
|
742
993
|
throw new InvalidUrlError(url);
|
|
994
|
+
let parsed;
|
|
743
995
|
try {
|
|
744
|
-
new URL(url);
|
|
996
|
+
parsed = new URL(url);
|
|
745
997
|
} catch {
|
|
746
998
|
throw new InvalidUrlError(url);
|
|
747
999
|
}
|
|
1000
|
+
if (parsed.username || parsed.password)
|
|
1001
|
+
throw new CredentialInUrlError(url);
|
|
748
1002
|
}
|
|
749
1003
|
async function checkReachable(url) {
|
|
750
1004
|
let response;
|
|
@@ -773,6 +1027,26 @@ function readVersion(data) {
|
|
|
773
1027
|
const candidate = info?.version;
|
|
774
1028
|
return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
|
|
775
1029
|
}
|
|
1030
|
+
function parseVersion(value) {
|
|
1031
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
|
|
1032
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
1033
|
+
}
|
|
1034
|
+
function compatibilityWarning(instanceVersion, toolVersion) {
|
|
1035
|
+
if (!instanceVersion)
|
|
1036
|
+
return null;
|
|
1037
|
+
const instance = parseVersion(instanceVersion);
|
|
1038
|
+
const tool = parseVersion(toolVersion);
|
|
1039
|
+
if (!instance || !tool)
|
|
1040
|
+
return null;
|
|
1041
|
+
for (let i = 0; i < 3; i++) {
|
|
1042
|
+
if (instance[i] > tool[i])
|
|
1043
|
+
return null;
|
|
1044
|
+
if (instance[i] < tool[i]) {
|
|
1045
|
+
return `This tool targets dotCMS ${toolVersion}, but the instance reports ${instanceVersion}. Install dotcms@${instanceVersion} to match your instance.`;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
776
1050
|
|
|
777
1051
|
// libs/sdk/cli/src/shared/prompts.ts
|
|
778
1052
|
var DEFAULT_URL = "http://localhost:8082";
|
|
@@ -791,50 +1065,52 @@ async function resolveInstanceUrl(opts, port, interactive = Boolean(port)) {
|
|
|
791
1065
|
return url;
|
|
792
1066
|
}
|
|
793
1067
|
async function resolveRequiredInputs(opts, port, interactive = Boolean(port)) {
|
|
794
|
-
let prompted = false;
|
|
795
1068
|
const ask2 = async (what, run) => {
|
|
796
1069
|
if (!interactive || !port)
|
|
797
1070
|
throw new MissingInputError(what);
|
|
798
|
-
prompted = true;
|
|
799
1071
|
return run();
|
|
800
1072
|
};
|
|
801
1073
|
const url = await resolveInstanceUrl(opts, port, interactive);
|
|
802
|
-
if (!opts.url && !readEnv(ENV_KEYS.url))
|
|
803
|
-
prompted = true;
|
|
804
1074
|
const authToken = opts.authToken ?? readEnv(ENV_KEYS.authToken);
|
|
805
1075
|
const user = opts.user;
|
|
806
1076
|
const password = opts.password ?? readEnv(ENV_KEYS.password);
|
|
807
1077
|
if (authToken)
|
|
808
|
-
return { url, authToken
|
|
1078
|
+
return { url, authToken };
|
|
809
1079
|
if (user && password)
|
|
810
|
-
return { url, user, password
|
|
1080
|
+
return { url, user, password };
|
|
811
1081
|
if (user) {
|
|
812
1082
|
const typed = await ask2("A password", () => port.password("Password"));
|
|
813
|
-
return { url, user, password: typed
|
|
1083
|
+
return { url, user, password: typed };
|
|
814
1084
|
}
|
|
815
1085
|
if (password) {
|
|
816
1086
|
const typed = await ask2("A username", () => port.text("Username"));
|
|
817
|
-
return { url, user: typed, password
|
|
1087
|
+
return { url, user: typed, password };
|
|
818
1088
|
}
|
|
819
1089
|
if (!interactive || !port) {
|
|
820
1090
|
throw new MissingInputError("A username and password, or an authentication token,");
|
|
821
1091
|
}
|
|
822
|
-
|
|
1092
|
+
return { url, ...await promptForAuth(port) };
|
|
1093
|
+
}
|
|
1094
|
+
async function promptForAuth(port) {
|
|
823
1095
|
const mode = await port.select("How should we authenticate?", [
|
|
824
1096
|
{ name: "Sign in with a username and password", value: "signin" },
|
|
825
1097
|
{ name: "Paste an existing authentication token", value: "token" }
|
|
826
1098
|
]);
|
|
827
1099
|
if (mode === "token") {
|
|
828
|
-
return {
|
|
1100
|
+
return { authToken: await port.password("Authentication token") };
|
|
829
1101
|
}
|
|
830
1102
|
return {
|
|
831
|
-
url,
|
|
832
1103
|
user: await port.text("Username"),
|
|
833
|
-
password: await port.password("Password")
|
|
834
|
-
prompted
|
|
1104
|
+
password: await port.password("Password")
|
|
835
1105
|
};
|
|
836
1106
|
}
|
|
837
1107
|
|
|
1108
|
+
// libs/sdk/cli/package.json
|
|
1109
|
+
var version = "26.9.3-1-next.2657";
|
|
1110
|
+
|
|
1111
|
+
// libs/sdk/cli/src/shared/version.ts
|
|
1112
|
+
var TOOL_VERSION = version;
|
|
1113
|
+
|
|
838
1114
|
// libs/sdk/cli/src/commands/agent/setup.ts
|
|
839
1115
|
var MAX_AUTH_ATTEMPTS = 3;
|
|
840
1116
|
function resolveAuthMode(opts) {
|
|
@@ -861,10 +1137,31 @@ async function runSetup(opts) {
|
|
|
861
1137
|
const auth = resolveAuthMode(opts);
|
|
862
1138
|
const explicitTargets = resolveTargets(opts);
|
|
863
1139
|
const scope = opts.scope ?? "folder";
|
|
1140
|
+
const outcome = (targetId, path5, result, reason = null, extra = {}) => ({
|
|
1141
|
+
targetId,
|
|
1142
|
+
scope,
|
|
1143
|
+
path: path5,
|
|
1144
|
+
result,
|
|
1145
|
+
reason,
|
|
1146
|
+
permissionsApplied: false,
|
|
1147
|
+
skillsInstalled: "no",
|
|
1148
|
+
...extra
|
|
1149
|
+
});
|
|
864
1150
|
const step = opts.onProgress ?? (() => void 0);
|
|
1151
|
+
const warnings = [];
|
|
865
1152
|
const url = await resolveInstanceUrl(opts, opts.promptPort);
|
|
866
1153
|
step(`Checking ${url}`);
|
|
867
|
-
await checkReachable(url);
|
|
1154
|
+
const instance = await checkReachable(url);
|
|
1155
|
+
const warning = compatibilityWarning(instance.version, TOOL_VERSION);
|
|
1156
|
+
if (warning) {
|
|
1157
|
+
warnings.push(warning);
|
|
1158
|
+
opts.onWarning?.(warning);
|
|
1159
|
+
}
|
|
1160
|
+
const insecure = insecureTransportWarning(url);
|
|
1161
|
+
if (insecure) {
|
|
1162
|
+
warnings.push(insecure);
|
|
1163
|
+
opts.onWarning?.(insecure);
|
|
1164
|
+
}
|
|
868
1165
|
let inputs = await resolveRequiredInputs(
|
|
869
1166
|
{ ...opts, url, authToken: auth.token, user: auth.user, password: auth.password },
|
|
870
1167
|
opts.promptPort
|
|
@@ -890,7 +1187,8 @@ async function runSetup(opts) {
|
|
|
890
1187
|
if (!rejected || !opts.promptPort || attempt >= MAX_AUTH_ATTEMPTS)
|
|
891
1188
|
throw error;
|
|
892
1189
|
opts.onAuthRetry?.(error.message, attempt, MAX_AUTH_ATTEMPTS);
|
|
893
|
-
|
|
1190
|
+
const fresh = await promptForAuth(opts.promptPort);
|
|
1191
|
+
inputs = { url, ...fresh };
|
|
894
1192
|
}
|
|
895
1193
|
}
|
|
896
1194
|
let targets;
|
|
@@ -923,81 +1221,73 @@ async function runSetup(opts) {
|
|
|
923
1221
|
seen.add(file);
|
|
924
1222
|
plan.push({ target, file });
|
|
925
1223
|
}
|
|
1224
|
+
const configuredNothing = !plan.length && !opts.skipMcp;
|
|
1225
|
+
if (configuredNothing) {
|
|
1226
|
+
const noEditors = `No editor was configured. None of the supported editors was detected, and none was named. Re-run with --agent <id>, choosing from: ${TARGET_IDS.join(", ")}.`;
|
|
1227
|
+
warnings.push(noEditors);
|
|
1228
|
+
opts.onWarning?.(noEditors);
|
|
1229
|
+
}
|
|
926
1230
|
const outcomes = [];
|
|
927
1231
|
if (opts.skipMcp) {
|
|
928
1232
|
for (const { target, file } of plan) {
|
|
929
|
-
outcomes.push(
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
path: file,
|
|
933
|
-
result: "skipped",
|
|
934
|
-
reason: "configuration writing skipped (--skip-mcp)",
|
|
935
|
-
permissionsApplied: false,
|
|
936
|
-
skillsInstalled: "no"
|
|
937
|
-
});
|
|
1233
|
+
outcomes.push(
|
|
1234
|
+
outcome(target.id, file, "skipped", "configuration writing skipped (--skip-mcp)")
|
|
1235
|
+
);
|
|
938
1236
|
}
|
|
939
|
-
}
|
|
940
|
-
if (!opts.skipMcp && plan.length) {
|
|
1237
|
+
} else if (plan.length) {
|
|
941
1238
|
step(`Writing configuration for ${plan.length} editor${plan.length === 1 ? "" : "s"}`);
|
|
942
1239
|
}
|
|
943
1240
|
for (const { target, file } of opts.skipMcp ? [] : plan) {
|
|
944
1241
|
try {
|
|
945
|
-
const
|
|
946
|
-
const existing = await hasEntry(
|
|
947
|
-
file,
|
|
948
|
-
containerKey: target.containerKey,
|
|
949
|
-
entryKey: ENTRY_KEY,
|
|
950
|
-
parse: isToml ? (raw) => parseToml(raw) : void 0
|
|
951
|
-
});
|
|
1242
|
+
const writer = WRITERS[target.format];
|
|
1243
|
+
const existing = await writer.hasEntry(file, target);
|
|
952
1244
|
if (existing && !opts.force && !opts.yes && opts.confirmOverwrite) {
|
|
953
1245
|
const proceed = await opts.confirmOverwrite(file);
|
|
954
1246
|
if (!proceed) {
|
|
955
|
-
outcomes.push(
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
path: file,
|
|
959
|
-
result: "skipped",
|
|
960
|
-
reason: "left the existing entry in place",
|
|
961
|
-
permissionsApplied: false,
|
|
962
|
-
skillsInstalled: "no"
|
|
963
|
-
});
|
|
1247
|
+
outcomes.push(
|
|
1248
|
+
outcome(target.id, file, "skipped", "left the existing entry in place")
|
|
1249
|
+
);
|
|
964
1250
|
continue;
|
|
965
1251
|
}
|
|
966
1252
|
}
|
|
967
|
-
const written2 =
|
|
968
|
-
|
|
969
|
-
permissionsApplied: CAN_RESTRICT,
|
|
970
|
-
replacedExisting: existing
|
|
971
|
-
} : await writeJsonTargetDetailed({ target, scope, url, token: token.value, cwd: opts.cwd });
|
|
972
|
-
outcomes.push({
|
|
973
|
-
targetId: target.id,
|
|
1253
|
+
const written2 = await writer.write({
|
|
1254
|
+
target,
|
|
974
1255
|
scope,
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
permissionsApplied: written2.permissionsApplied,
|
|
979
|
-
skillsInstalled: "no"
|
|
1256
|
+
url,
|
|
1257
|
+
token: token.value,
|
|
1258
|
+
cwd: opts.cwd
|
|
980
1259
|
});
|
|
1260
|
+
outcomes.push(
|
|
1261
|
+
outcome(
|
|
1262
|
+
target.id,
|
|
1263
|
+
written2.path,
|
|
1264
|
+
written2.replacedExisting ? "replaced" : "written",
|
|
1265
|
+
null,
|
|
1266
|
+
{ permissionsApplied: written2.permissionsApplied }
|
|
1267
|
+
)
|
|
1268
|
+
);
|
|
981
1269
|
} catch (error) {
|
|
982
|
-
outcomes.push(
|
|
983
|
-
targetId: target.id,
|
|
984
|
-
scope,
|
|
985
|
-
path: file,
|
|
986
|
-
result: "failed",
|
|
987
|
-
reason: error.message,
|
|
988
|
-
permissionsApplied: false,
|
|
989
|
-
skillsInstalled: "no"
|
|
990
|
-
});
|
|
1270
|
+
outcomes.push(outcome(target.id, file, "failed", error.message));
|
|
991
1271
|
}
|
|
992
1272
|
}
|
|
993
1273
|
let versionControl;
|
|
994
1274
|
const written = outcomes.filter((o) => (o.result === "written" || o.result === "replaced") && o.path).map((o) => o.path);
|
|
995
1275
|
if (scope === "folder" && written.length) {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1276
|
+
try {
|
|
1277
|
+
versionControl = await protectFromVersionControl({
|
|
1278
|
+
files: written,
|
|
1279
|
+
// Which of them a project would normally commit is the registry's to say, not a
|
|
1280
|
+
// basename set inside the gitignore module.
|
|
1281
|
+
committedByConvention: plan.filter(({ target }) => target.folderConfigIsCommitted).map(({ file }) => file),
|
|
1282
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
1283
|
+
confirmExclude: opts.yes ? async () => true : opts.confirmExclude
|
|
1284
|
+
});
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
const vcFailed = `Could not update .gitignore \u2014 ${error.message}. These files hold an access token and are NOT excluded from version control:
|
|
1287
|
+
${written.join("\n ")}`;
|
|
1288
|
+
warnings.push(vcFailed);
|
|
1289
|
+
opts.onWarning?.(vcFailed);
|
|
1290
|
+
}
|
|
1001
1291
|
}
|
|
1002
1292
|
if (!opts.skipSkills) {
|
|
1003
1293
|
const eligible = opts.skipMcp ? plan.map((p) => p.target) : outcomes.filter((o) => o.result !== "failed").map((o) => byId.get(o.targetId)).filter((t) => Boolean(t));
|
|
@@ -1005,6 +1295,12 @@ async function runSetup(opts) {
|
|
|
1005
1295
|
if (ids.length) {
|
|
1006
1296
|
step("Installing the dotCMS skills");
|
|
1007
1297
|
const skills = await installSkills({ agentIds: ids, global: scope === "global" });
|
|
1298
|
+
if (!skills.ok) {
|
|
1299
|
+
const failed = `Skills were not installed${skills.reason ? ` \u2014 ${skills.reason}` : ""}. Run this when the problem is fixed:
|
|
1300
|
+
${skills.command}`;
|
|
1301
|
+
warnings.push(failed);
|
|
1302
|
+
opts.onWarning?.(failed);
|
|
1303
|
+
}
|
|
1008
1304
|
for (const o of outcomes) {
|
|
1009
1305
|
if (o.result === "failed")
|
|
1010
1306
|
continue;
|
|
@@ -1016,14 +1312,24 @@ async function runSetup(opts) {
|
|
|
1016
1312
|
let connection = "skipped";
|
|
1017
1313
|
let connectionReason;
|
|
1018
1314
|
if (!opts.skipVerify && !opts.skipMcp) {
|
|
1019
|
-
step(
|
|
1315
|
+
step(
|
|
1316
|
+
"Starting the server to confirm it responds (this can take a minute on a cold npx cache)"
|
|
1317
|
+
);
|
|
1020
1318
|
const result = await confirmConnection({ url, token: token.value });
|
|
1021
1319
|
connection = result.ok ? "ok" : "failed";
|
|
1022
1320
|
if (!result.ok)
|
|
1023
1321
|
connectionReason = `${result.cause}: ${result.detail}`;
|
|
1024
1322
|
}
|
|
1025
1323
|
const anyFailed = outcomes.some((o) => o.result === "failed") || connection === "failed";
|
|
1026
|
-
return {
|
|
1324
|
+
return {
|
|
1325
|
+
outcomes,
|
|
1326
|
+
versionControl,
|
|
1327
|
+
warnings,
|
|
1328
|
+
connection,
|
|
1329
|
+
connectionReason,
|
|
1330
|
+
skillsSkipped: Boolean(opts.skipSkills),
|
|
1331
|
+
exitCode: anyFailed || configuredNothing ? 1 : 0
|
|
1332
|
+
};
|
|
1027
1333
|
}
|
|
1028
1334
|
|
|
1029
1335
|
// libs/sdk/cli/src/shared/ui.ts
|
|
@@ -1041,10 +1347,11 @@ var RESULT_MARK = {
|
|
|
1041
1347
|
};
|
|
1042
1348
|
function renderSummary(input) {
|
|
1043
1349
|
const lines = [];
|
|
1350
|
+
const idWidth = Math.max(...input.outcomes.map((o) => o.targetId.length), 0);
|
|
1044
1351
|
for (const o of input.outcomes) {
|
|
1045
1352
|
const bits = [
|
|
1046
1353
|
` ${RESULT_MARK[o.result]}`,
|
|
1047
|
-
o.targetId.padEnd(
|
|
1354
|
+
o.targetId.padEnd(idWidth),
|
|
1048
1355
|
o.scope.padEnd(7),
|
|
1049
1356
|
o.path ?? "\u2014"
|
|
1050
1357
|
];
|
|
@@ -1055,9 +1362,14 @@ function renderSummary(input) {
|
|
|
1055
1362
|
lines.push(" could not restrict file permissions on this platform");
|
|
1056
1363
|
}
|
|
1057
1364
|
if (o.skillsInstalled === "unverified") {
|
|
1058
|
-
lines.push(
|
|
1365
|
+
lines.push(
|
|
1366
|
+
" skills location unverified for this editor \u2014 not confirmed installed"
|
|
1367
|
+
);
|
|
1059
1368
|
}
|
|
1060
1369
|
}
|
|
1370
|
+
for (const w of input.warnings ?? []) {
|
|
1371
|
+
lines.push(chalk.yellow(` ! ${w}`));
|
|
1372
|
+
}
|
|
1061
1373
|
if (input.versionControl?.files.length) {
|
|
1062
1374
|
const vc = input.versionControl;
|
|
1063
1375
|
lines.push("");
|
|
@@ -1071,6 +1383,9 @@ function renderSummary(input) {
|
|
|
1071
1383
|
for (const w of vc.warnings)
|
|
1072
1384
|
lines.push(chalk.yellow(` ! ${w}`));
|
|
1073
1385
|
}
|
|
1386
|
+
if (input.skillsSkipped) {
|
|
1387
|
+
lines.push(" \xB7 skills installation skipped (--skip-skills)");
|
|
1388
|
+
}
|
|
1074
1389
|
lines.push("");
|
|
1075
1390
|
if (input.connection === "ok") {
|
|
1076
1391
|
lines.push(chalk.green(" \u2713 server responded"));
|
|
@@ -1080,10 +1395,16 @@ function renderSummary(input) {
|
|
|
1080
1395
|
lines.push(chalk.red(` \u2717 ${input.connectionReason ?? "the server did not start"}`));
|
|
1081
1396
|
lines.push(" Configuration was written and left in place; the server did not come up.");
|
|
1082
1397
|
}
|
|
1083
|
-
const
|
|
1398
|
+
const done = (o) => o.result === "written" || o.result === "replaced";
|
|
1399
|
+
const left = input.outcomes.filter((o) => !done(o) && o.result !== "failed");
|
|
1400
|
+
const allGood = input.connection === "ok" && input.outcomes.length > 0 && input.outcomes.every(done);
|
|
1401
|
+
lines.push("");
|
|
1084
1402
|
if (allGood) {
|
|
1085
|
-
lines.push("");
|
|
1086
1403
|
lines.push(` Ready \u2014 ${input.nextStep ?? "open your editor and start using dotCMS."}`);
|
|
1404
|
+
} else if (left.length && input.connection === "ok") {
|
|
1405
|
+
lines.push(
|
|
1406
|
+
` ${left.length} editor${left.length === 1 ? " was" : "s were"} left unchanged; what they already point at was not verified.`
|
|
1407
|
+
);
|
|
1087
1408
|
}
|
|
1088
1409
|
return lines.join("\n");
|
|
1089
1410
|
}
|
|
@@ -1106,8 +1427,24 @@ function makeProgress(interactive) {
|
|
|
1106
1427
|
},
|
|
1107
1428
|
done() {
|
|
1108
1429
|
spinner?.stop();
|
|
1430
|
+
spinner = null;
|
|
1109
1431
|
},
|
|
1110
|
-
/**
|
|
1432
|
+
/**
|
|
1433
|
+
* Hand the terminal over to a question.
|
|
1434
|
+
*
|
|
1435
|
+
* Keeps the finished step visible, then stops repainting. `done()` erases the line;
|
|
1436
|
+
* here the step really did complete, so it should stay on screen above the prompt.
|
|
1437
|
+
*/
|
|
1438
|
+
pause() {
|
|
1439
|
+
spinner?.succeed();
|
|
1440
|
+
spinner = null;
|
|
1441
|
+
},
|
|
1442
|
+
/**
|
|
1443
|
+
* Leave the failed step visible instead of a spinner that never stops.
|
|
1444
|
+
*
|
|
1445
|
+
* `warn` was a byte-identical second copy of this: a retry notice and a failure render
|
|
1446
|
+
* the same way — mark the attempt failed, then carry on.
|
|
1447
|
+
*/
|
|
1111
1448
|
fail(text) {
|
|
1112
1449
|
if (spinner)
|
|
1113
1450
|
spinner.fail(text);
|
|
@@ -1115,16 +1452,37 @@ function makeProgress(interactive) {
|
|
|
1115
1452
|
writeOut(` \u2717 ${text}`);
|
|
1116
1453
|
spinner = null;
|
|
1117
1454
|
},
|
|
1118
|
-
/** A retry notice: mark the attempt failed, then carry on. */
|
|
1119
1455
|
warn(text) {
|
|
1120
|
-
|
|
1121
|
-
spinner.fail(text);
|
|
1122
|
-
else
|
|
1123
|
-
writeOut(` \u2717 ${text}`);
|
|
1124
|
-
spinner = null;
|
|
1456
|
+
this.fail(text);
|
|
1125
1457
|
}
|
|
1126
1458
|
};
|
|
1127
1459
|
}
|
|
1460
|
+
function pausingPort(port, progress) {
|
|
1461
|
+
return {
|
|
1462
|
+
text(message, defaultValue) {
|
|
1463
|
+
progress.pause();
|
|
1464
|
+
return port.text(message, defaultValue);
|
|
1465
|
+
},
|
|
1466
|
+
password(message) {
|
|
1467
|
+
progress.pause();
|
|
1468
|
+
return port.password(message);
|
|
1469
|
+
},
|
|
1470
|
+
select(message, choices) {
|
|
1471
|
+
progress.pause();
|
|
1472
|
+
return port.select(message, choices);
|
|
1473
|
+
},
|
|
1474
|
+
multiSelect(message, choices) {
|
|
1475
|
+
progress.pause();
|
|
1476
|
+
return port.multiSelect(message, choices);
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function pausingConfirm(confirm, progress) {
|
|
1481
|
+
return (...args) => {
|
|
1482
|
+
progress.pause();
|
|
1483
|
+
return confirm(...args);
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1128
1486
|
function registerAgentCommand(program2) {
|
|
1129
1487
|
const agent = program2.command("agent").description("Connect an AI coding agent to dotCMS");
|
|
1130
1488
|
agent.command("setup").description("Configure your editors to talk to a dotCMS instance").option("--url <url>", "dotCMS instance address (or set DOTCMS_URL)").option("--user <user>", "username, to mint a token").option(
|
|
@@ -1138,17 +1496,18 @@ function registerAgentCommand(program2) {
|
|
|
1138
1496
|
`editor to configure, repeatable (${TARGET_IDS.join(", ")})`,
|
|
1139
1497
|
(value, previous = []) => [...previous, value]
|
|
1140
1498
|
).option("-g, --global", "write to your user account instead of this folder").option("--skip-mcp", "do not write configuration").option("--skip-skills", "do not install the dotCMS skills").option("--skip-verify", "do not launch the server to confirm it responds").option("-y, --yes", "accept confirmations (never skips a required input)").option("--force", "replace an existing dotcms entry without asking").action(async (options) => {
|
|
1141
|
-
if (canPrompt())
|
|
1142
|
-
printBanner();
|
|
1143
1499
|
const interactive = canPrompt();
|
|
1500
|
+
if (interactive)
|
|
1501
|
+
printBanner();
|
|
1144
1502
|
const progress = makeProgress(interactive);
|
|
1145
1503
|
try {
|
|
1146
1504
|
const result = await runSetup({
|
|
1147
1505
|
onProgress: progress.step,
|
|
1148
1506
|
onAuthRetry: (message, attempt, max) => progress.warn(`${message} (attempt ${attempt} of ${max})`),
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1507
|
+
onWarning: (message) => progress.warn(message),
|
|
1508
|
+
promptPort: interactive ? pausingPort(inquirerPort, progress) : void 0,
|
|
1509
|
+
confirmOverwrite: interactive ? pausingConfirm(confirmOverwrite, progress) : void 0,
|
|
1510
|
+
confirmExclude: interactive ? pausingConfirm(confirmExclude, progress) : void 0,
|
|
1152
1511
|
url: options["url"],
|
|
1153
1512
|
user: options["user"],
|
|
1154
1513
|
password: options["password"],
|
|
@@ -1166,8 +1525,10 @@ function registerAgentCommand(program2) {
|
|
|
1166
1525
|
renderSummary({
|
|
1167
1526
|
outcomes: result.outcomes,
|
|
1168
1527
|
versionControl: result.versionControl,
|
|
1528
|
+
warnings: result.warnings,
|
|
1169
1529
|
connection: result.connection,
|
|
1170
|
-
connectionReason: result.connectionReason
|
|
1530
|
+
connectionReason: result.connectionReason,
|
|
1531
|
+
skillsSkipped: result.skillsSkipped
|
|
1171
1532
|
})
|
|
1172
1533
|
);
|
|
1173
1534
|
process.exitCode = result.exitCode;
|