dotcms 0.2.1 → 26.9.9-1

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.
Files changed (2) hide show
  1. package/index.js +473 -266
  2. package/package.json +23 -23
package/index.js CHANGED
@@ -57,6 +57,24 @@ var SERVER_ENV = {
57
57
  };
58
58
  var SKILLS_SOURCE = "dotCMS/agent-toolkit";
59
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
+
60
78
  // libs/sdk/cli/src/commands/agent/connect.ts
61
79
  var DEFAULT_TIMEOUT_MS = 6e4;
62
80
  function classify(stderr) {
@@ -71,11 +89,17 @@ async function confirmConnection(args) {
71
89
  const timeoutMs = args.timeoutMs ?? DEFAULT_TIMEOUT_MS;
72
90
  const child = childProcess.spawn("npx", ["-y", MCP_SERVER_PACKAGE], {
73
91
  stdio: ["pipe", "pipe", "pipe"],
74
- env: {
75
- ...process.env,
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({
76
100
  [SERVER_ENV.url]: args.url,
77
101
  [SERVER_ENV.token]: args.token
78
- }
102
+ })
79
103
  });
80
104
  let stderr = "";
81
105
  child.stderr?.on("data", (chunk) => {
@@ -89,7 +113,15 @@ async function confirmConnection(args) {
89
113
  settled = true;
90
114
  clearTimeout(timer);
91
115
  try {
92
- child.kill();
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
+ }
93
125
  } catch {
94
126
  }
95
127
  resolve2(result);
@@ -112,6 +144,17 @@ async function confirmConnection(args) {
112
144
  continue;
113
145
  try {
114
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
+ }
115
158
  const tools = message.result?.tools;
116
159
  if (Array.isArray(tools))
117
160
  finish({ ok: true, toolCount: tools.length });
@@ -144,108 +187,23 @@ async function confirmConnection(args) {
144
187
  })}
145
188
  `
146
189
  );
147
- child.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" })}
148
- `);
149
190
  });
150
191
  }
151
192
 
152
193
  // libs/sdk/cli/src/commands/agent/gitignore.ts
194
+ import { spawnSync as spawnSync2 } from "node:child_process";
153
195
  import { existsSync } from "node:fs";
154
- import * as fs from "node:fs/promises";
155
- import * as path from "node:path";
156
- function findRepositoryRoot(from) {
157
- let dir = path.resolve(from);
158
- for (; ; ) {
159
- if (existsSync(path.join(dir, ".git")))
160
- return dir;
161
- const parent = path.dirname(dir);
162
- if (parent === dir)
163
- return null;
164
- dir = parent;
165
- }
166
- }
167
- async function protectFromVersionControl(args) {
168
- const warnings = [];
169
- const root = findRepositoryRoot(args.cwd);
170
- const committed = new Set(args.committedByConvention ?? []);
171
- for (const file of args.files) {
172
- if (committed.has(file)) {
173
- warnings.push(
174
- `${path.basename(file)} is normally committed to version control \u2014 it now holds a token, so committing it would publish that token.`
175
- );
176
- }
177
- }
178
- if (!root) {
179
- warnings.push(
180
- "This directory is not under version control, so these files are unprotected \u2014 nothing here can exclude them for you."
181
- );
182
- return { files: args.files, inRepository: false, excluded: false, warnings };
183
- }
184
- const proceed = args.confirmExclude ? await args.confirmExclude(args.files) : false;
185
- if (!proceed) {
186
- return { files: args.files, inRepository: true, excluded: false, warnings };
187
- }
188
- const gitignorePath = path.join(root, ".gitignore");
189
- let current = "";
190
- try {
191
- current = await fs.readFile(gitignorePath, "utf8");
192
- } catch {
193
- }
194
- const already = new Set(current.split("\n").map((line) => line.trim()));
195
- const toAdd = args.files.map((file) => path.relative(root, file).split(path.sep).join("/")).filter((entry) => !already.has(entry));
196
- if (toAdd.length) {
197
- const prefix = current === "" || current.endsWith("\n") ? "" : "\n";
198
- const block = `${prefix}
199
- # dotCMS agent configuration \u2014 contains an access token
200
- ${toAdd.join("\n")}
201
- `;
202
- await fs.writeFile(gitignorePath, current + block, "utf8");
203
- }
204
- return { files: args.files, inRepository: true, excluded: true, warnings };
205
- }
206
-
207
- // libs/sdk/cli/src/commands/agent/skills.ts
208
- import * as childProcess2 from "node:child_process";
209
- function buildSkillsArgs(agentIds, global) {
210
- return [
211
- "-y",
212
- "skills",
213
- "add",
214
- SKILLS_SOURCE,
215
- ...agentIds.flatMap((id) => ["-a", id]),
216
- ...global ? ["-g"] : [],
217
- "-y"
218
- ];
219
- }
220
- async function installSkills(args) {
221
- const argv = buildSkillsArgs(args.agentIds, args.global);
222
- const command = `npx ${argv.join(" ")}`;
223
- try {
224
- const result = childProcess2.spawnSync("npx", argv, { stdio: "inherit" });
225
- if (result.status === 0)
226
- return { ok: true, command };
227
- return { ok: false, command, reason: `skills exited with code ${result.status}` };
228
- } catch (error) {
229
- return { ok: false, command, reason: error.message };
230
- }
231
- }
232
-
233
- // libs/sdk/cli/src/commands/agent/targets/registry.ts
234
- import { existsSync as existsSync2 } from "node:fs";
235
- import * as os from "node:os";
196
+ import * as fs2 from "node:fs/promises";
236
197
  import * as path2 from "node:path";
237
198
 
238
- // libs/sdk/cli/src/shared/env.ts
239
- var ENV_KEYS = {
240
- url: "DOTCMS_URL",
241
- password: "DOTCMS_PASSWORD",
242
- authToken: "DOTCMS_AUTH_TOKEN",
243
- codexHome: "CODEX_HOME"
244
- };
245
- function readEnv(key) {
246
- const value = process.env[key];
247
- return value && value.trim() !== "" ? value : void 0;
248
- }
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";
249
207
 
250
208
  // libs/sdk/cli/src/shared/errors.ts
251
209
  var CliError = class extends Error {
@@ -256,11 +214,31 @@ var CliError = class extends Error {
256
214
  };
257
215
  var UsageError = class extends CliError {
258
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
+ }
259
228
  var InvalidUrlError = class extends CliError {
260
229
  constructor(value) {
261
- const host = value.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/^\/+/, "");
230
+ const safe = withoutCredentials(value);
231
+ const host = safe.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/^\/+/, "");
232
+ super(
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);
262
240
  super(
263
- `"${value}" is not a valid instance address. Pass it with an http:// or https:// scheme, e.g. https://${host || "demo.dotcms.com"}`
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.`
264
242
  );
265
243
  }
266
244
  };
@@ -273,7 +251,15 @@ var InstanceUnreachableError = class extends CliError {
273
251
  };
274
252
  var NotADotCmsInstanceError = class extends CliError {
275
253
  constructor(url) {
276
- super(`${url} is not a valid dotCMS instance. Check the address.`);
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}`);
277
263
  }
278
264
  };
279
265
  var CredentialsRejectedError = class extends CliError {
@@ -318,6 +304,13 @@ var NoConfigPathError = class extends CliError {
318
304
  );
319
305
  }
320
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.`
311
+ );
312
+ }
313
+ };
321
314
  var MalformedConfigError = class extends CliError {
322
315
  /** The format is passed, not sniffed from the extension: both callers know exactly which
323
316
  * parser just refused the file. */
@@ -328,17 +321,257 @@ var MalformedConfigError = class extends CliError {
328
321
  }
329
322
  };
330
323
 
324
+ // libs/sdk/cli/src/shared/config-file.ts
325
+ var CAN_RESTRICT = process.platform !== "win32";
326
+ var FILE_MODE = 384;
327
+ var DIR_MODE = 448;
328
+ async function readFileIfPresent(file) {
329
+ try {
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 });
341
+ } catch {
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;
350
+ }
351
+ }
352
+ var PARSE_OPTIONS = { allowTrailingComma: true, allowEmptyContent: true };
353
+ function parseOrThrow(raw, file) {
354
+ if (raw.trim() === "")
355
+ return {};
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)) {
361
+ throw new MalformedConfigError(file);
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 };
375
+ }
376
+ async function hasEntry(args) {
377
+ const doc = await readJsonDocument(args.file).catch(() => null);
378
+ const container = doc?.[args.containerKey] ?? {};
379
+ return Object.prototype.hasOwnProperty.call(container, args.entryKey);
380
+ }
381
+ async function restrictFile(file, canRestrict = CAN_RESTRICT) {
382
+ if (!canRestrict)
383
+ return false;
384
+ await fs.chmod(file, FILE_MODE);
385
+ return true;
386
+ }
387
+ async function ensureDir(dir) {
388
+ const created = await fs.mkdir(dir, { recursive: true });
389
+ if (created && CAN_RESTRICT)
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)}`;
415
+ }
416
+ async function writeMerged(args) {
417
+ const raw = await readFileIfPresent(args.file);
418
+ const existing = raw === null ? {} : parseOrThrow(raw, args.file);
419
+ const container = existing[args.containerKey] ?? {};
420
+ const replacedExisting = Object.prototype.hasOwnProperty.call(container, args.entryKey);
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);
441
+ const permissionsApplied = await restrictFile(args.file, args.canRestrict ?? CAN_RESTRICT);
442
+ return { path: args.file, permissionsApplied, replacedExisting };
443
+ }
444
+
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));
458
+ }
459
+ return missed;
460
+ }
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
+ }
559
+ }
560
+
331
561
  // libs/sdk/cli/src/commands/agent/targets/registry.ts
562
+ import { existsSync as existsSync2 } from "node:fs";
563
+ import * as os from "node:os";
564
+ import * as path3 from "node:path";
332
565
  var home = () => os.homedir();
333
- var inHome = (...parts) => path2.join(home(), ...parts);
334
- var inFolder = (cwd, ...parts) => path2.join(cwd ?? process.cwd(), ...parts);
566
+ var inHome = (...parts) => path3.join(home(), ...parts);
567
+ var inFolder = (cwd, ...parts) => path3.join(cwd ?? process.cwd(), ...parts);
335
568
  var probe = (...parts) => async () => existsSync2(inHome(...parts));
336
569
  function vscodeUserDir() {
337
570
  if (process.platform === "darwin")
338
571
  return inHome("Library", "Application Support", "Code", "User");
339
572
  if (process.platform === "win32") {
340
573
  const appData = process.env["APPDATA"] ?? inHome("AppData", "Roaming");
341
- return path2.join(appData, "Code", "User");
574
+ return path3.join(appData, "Code", "User");
342
575
  }
343
576
  return inHome(".config", "Code", "User");
344
577
  }
@@ -384,7 +617,7 @@ var TARGETS = [
384
617
  containerKey: "servers",
385
618
  entryShape: "stdio",
386
619
  detect: async () => existsSync2(vscodeUserDir()),
387
- configPath: (scope, cwd) => scope === "global" ? path2.join(vscodeUserDir(), "mcp.json") : inFolder(cwd, ".vscode", "mcp.json")
620
+ configPath: (scope, cwd) => scope === "global" ? path3.join(vscodeUserDir(), "mcp.json") : inFolder(cwd, ".vscode", "mcp.json")
388
621
  },
389
622
  {
390
623
  id: "codex",
@@ -395,7 +628,7 @@ var TARGETS = [
395
628
  containerKey: "mcp_servers",
396
629
  entryShape: "stdio",
397
630
  detect: probe(".codex"),
398
- configPath: (scope, cwd) => scope === "global" ? path2.join(codexHome(), "config.toml") : inFolder(cwd, ".codex", "config.toml")
631
+ configPath: (scope, cwd) => scope === "global" ? path3.join(codexHome(), "config.toml") : inFolder(cwd, ".codex", "config.toml")
399
632
  },
400
633
  {
401
634
  id: "antigravity",
@@ -458,117 +691,6 @@ function buildEntry(target, url, token) {
458
691
  return { type: "stdio", command: "npx", args: ["-y", MCP_SERVER_PACKAGE], env };
459
692
  }
460
693
 
461
- // libs/sdk/cli/src/shared/config-file.ts
462
- import {
463
- findNodeAtLocation,
464
- parse as parseJsonc,
465
- parseTree
466
- } from "jsonc-parser";
467
- import * as fs2 from "node:fs/promises";
468
- import * as path3 from "node:path";
469
- var CAN_RESTRICT = process.platform !== "win32";
470
- var FILE_MODE = 384;
471
- var DIR_MODE = 448;
472
- var PARSE_OPTIONS = { allowTrailingComma: true, allowEmptyContent: true };
473
- function parseOrThrow(raw, file) {
474
- if (raw.trim() === "")
475
- return {};
476
- const errors = [];
477
- const doc = parseJsonc(raw, errors, PARSE_OPTIONS);
478
- if (errors.length > 0 || doc === void 0)
479
- throw new MalformedConfigError(file);
480
- return doc;
481
- }
482
- async function readJsonDocument(file) {
483
- let raw;
484
- try {
485
- raw = await fs2.readFile(file, "utf8");
486
- } catch {
487
- return null;
488
- }
489
- return parseOrThrow(raw, file);
490
- }
491
- function detectIndent(raw) {
492
- const match = raw.match(/\n([ \t]+)\S/);
493
- if (!match)
494
- return { insertSpaces: true, tabSize: 2 };
495
- const indent = match[1];
496
- return indent.startsWith(" ") ? { insertSpaces: false, tabSize: 1 } : { insertSpaces: true, tabSize: indent.length };
497
- }
498
- async function hasEntry(args) {
499
- const doc = await readJsonDocument(args.file).catch(() => null);
500
- const container = doc?.[args.containerKey] ?? {};
501
- return Object.prototype.hasOwnProperty.call(container, args.entryKey);
502
- }
503
- async function restrictFile(file, canRestrict = CAN_RESTRICT) {
504
- if (!canRestrict)
505
- return false;
506
- await fs2.chmod(file, FILE_MODE);
507
- return true;
508
- }
509
- async function ensureDir(dir) {
510
- const created = await fs2.mkdir(dir, { recursive: true });
511
- if (created && CAN_RESTRICT)
512
- await fs2.chmod(dir, DIR_MODE).catch(() => void 0);
513
- }
514
- function renderAt(value, unit, depth) {
515
- const base = unit.repeat(depth);
516
- return JSON.stringify(value, null, unit).split("\n").map((line, i) => i === 0 ? line : base + line).join("\n");
517
- }
518
- function setProperty(raw, object, key, value, unit, depth) {
519
- const rendered = renderAt(value, unit, depth);
520
- const properties = object.children ?? [];
521
- const existing = properties.find((p) => p.children?.[0]?.value === key);
522
- if (existing?.children?.[1]) {
523
- const node = existing.children[1];
524
- return raw.slice(0, node.offset) + rendered + raw.slice(node.offset + node.length);
525
- }
526
- const insertion = `"${key}": ${rendered}`;
527
- const last = properties[properties.length - 1];
528
- if (last) {
529
- const end = last.offset + last.length;
530
- return `${raw.slice(0, end)},
531
- ${unit.repeat(depth)}${insertion}${raw.slice(end)}`;
532
- }
533
- const close = raw.lastIndexOf("}", object.offset + object.length);
534
- return `${raw.slice(0, object.offset + 1)}
535
- ${unit.repeat(depth)}${insertion}
536
- ${unit.repeat(depth - 1)}${raw.slice(close)}`;
537
- }
538
- async function writeMerged(args) {
539
- let raw;
540
- try {
541
- raw = await fs2.readFile(args.file, "utf8");
542
- } catch {
543
- raw = null;
544
- }
545
- const existing = raw === null ? {} : parseOrThrow(raw, args.file);
546
- const container = existing[args.containerKey] ?? {};
547
- const replacedExisting = Object.prototype.hasOwnProperty.call(container, args.entryKey);
548
- let next;
549
- const tree = raw === null ? void 0 : parseTree(raw, [], PARSE_OPTIONS);
550
- if (raw === null || raw.trim() === "" || tree?.type !== "object") {
551
- next = `${JSON.stringify({ [args.containerKey]: { [args.entryKey]: args.entry } }, null, 2)}
552
- `;
553
- } else {
554
- const { insertSpaces, tabSize } = detectIndent(raw);
555
- const unit = insertSpaces ? " ".repeat(tabSize) : " ";
556
- const containerNode = findNodeAtLocation(tree, [args.containerKey]);
557
- next = containerNode?.type === "object" ? setProperty(raw, containerNode, args.entryKey, args.entry, unit, 2) : setProperty(
558
- raw,
559
- tree,
560
- args.containerKey,
561
- { [args.entryKey]: args.entry },
562
- unit,
563
- 1
564
- );
565
- }
566
- await ensureDir(path3.dirname(args.file));
567
- await fs2.writeFile(args.file, next, "utf8");
568
- const permissionsApplied = await restrictFile(args.file, args.canRestrict ?? CAN_RESTRICT);
569
- return { path: args.file, permissionsApplied, replacedExisting };
570
- }
571
-
572
694
  // libs/sdk/cli/src/commands/agent/targets/json-target.ts
573
695
  function hasJsonEntry(file, target) {
574
696
  return hasEntry({ file, containerKey: target.containerKey, entryKey: ENTRY_KEY });
@@ -587,19 +709,16 @@ async function writeJsonTarget(args) {
587
709
 
588
710
  // libs/sdk/cli/src/commands/agent/targets/toml-target.ts
589
711
  import { parse, stringify } from "smol-toml";
590
- import * as fs3 from "node:fs/promises";
591
712
  import * as path4 from "node:path";
592
713
  async function hasTomlEntry(file, target) {
593
- let raw;
594
- try {
595
- raw = await fs3.readFile(file, "utf8");
596
- } catch {
597
- return false;
598
- }
599
- return findEntrySpan(raw.split("\n"), target.containerKey) !== null;
714
+ const raw = await readFileIfPresent(file);
715
+ return raw === null ? false : findEntrySpan(raw.split("\n"), target.containerKey) !== null;
600
716
  }
601
717
  function findEntrySpan(lines, containerKey) {
602
- const ours = new RegExp(`^\\s*\\[\\s*${containerKey}\\.${ENTRY_KEY}\\s*(\\.[^\\]]+)?\\]`);
718
+ const spelt = (key) => `(?:${key}|"${key}"|'${key}')`;
719
+ const ours = new RegExp(
720
+ `^\\s*\\[\\s*${spelt(containerKey)}\\s*\\.\\s*${spelt(ENTRY_KEY)}\\s*(\\.[^\\]]*)?\\]`
721
+ );
603
722
  const anyHeader = /^\s*\[/;
604
723
  const start = lines.findIndex((line) => ours.test(line));
605
724
  if (start === -1)
@@ -626,11 +745,7 @@ async function writeTomlTarget(args) {
626
745
  if (!file) {
627
746
  throw new NoConfigPathError(args.target.displayName, args.scope);
628
747
  }
629
- let original = "";
630
- try {
631
- original = await fs3.readFile(file, "utf8");
632
- } catch {
633
- }
748
+ const original = await readFileIfPresent(file) ?? "";
634
749
  if (original.trim() !== "") {
635
750
  try {
636
751
  parse(original);
@@ -656,8 +771,8 @@ async function writeTomlTarget(args) {
656
771
  }
657
772
  }
658
773
  await ensureDir(path4.dirname(file));
659
- await fs3.writeFile(file, next.endsWith("\n") ? next : `${next}
660
- `, "utf8");
774
+ await writeFileAtomic(file, next.endsWith("\n") ? next : `${next}
775
+ `);
661
776
  return { path: file, permissionsApplied: await restrictFile(file), replacedExisting };
662
777
  }
663
778
 
@@ -718,8 +833,37 @@ function isHttpError(error) {
718
833
  return error instanceof HttpError;
719
834
  }
720
835
  var DEFAULT_TIMEOUT_MS2 = 1e4;
721
- async function readBody(response) {
722
- const text = await response.text().catch(() => "");
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
+ }
723
867
  if (!text) {
724
868
  return void 0;
725
869
  }
@@ -738,25 +882,38 @@ async function request(url, init, { token, timeoutMs = DEFAULT_TIMEOUT_MS2, acce
738
882
  }
739
883
  let response;
740
884
  try {
741
- response = await fetch(url, { ...init, headers, signal: controller.signal });
742
- } catch (error) {
743
- const aborted = error?.name === "AbortError";
744
- const cause = error?.cause;
745
- throw new HttpError(
746
- aborted ? `Request to ${url} timed out after ${timeoutMs}ms` : `Request to ${url} failed: ${error?.message ?? String(error)}`,
747
- { status: null, code: aborted ? "ETIMEDOUT" : cause?.code }
748
- );
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 };
749
914
  } finally {
750
915
  clearTimeout(timer);
751
916
  }
752
- const data = await readBody(response);
753
- if (!isSuccessStatus(response.status) && !acceptAnyStatus) {
754
- throw new HttpError(`Request failed with status code ${response.status}`, {
755
- status: response.status,
756
- statusText: response.statusText
757
- });
758
- }
759
- return { status: response.status, data };
760
917
  }
761
918
  function httpGet(url, options = {}) {
762
919
  return request(url, { method: "GET" }, options);
@@ -819,14 +976,29 @@ async function verifyToken(url, token) {
819
976
  function normalizeUrl(url) {
820
977
  return url.trim().replace(/\/+$/, "");
821
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
+ }
822
991
  function validateUrl(url) {
823
992
  if (!/^https?:\/\//i.test(url))
824
993
  throw new InvalidUrlError(url);
994
+ let parsed;
825
995
  try {
826
- new URL(url);
996
+ parsed = new URL(url);
827
997
  } catch {
828
998
  throw new InvalidUrlError(url);
829
999
  }
1000
+ if (parsed.username || parsed.password)
1001
+ throw new CredentialInUrlError(url);
830
1002
  }
831
1003
  async function checkReachable(url) {
832
1004
  let response;
@@ -934,7 +1106,7 @@ async function promptForAuth(port) {
934
1106
  }
935
1107
 
936
1108
  // libs/sdk/cli/package.json
937
- var version = "0.2.0";
1109
+ var version = "26.09.09-01";
938
1110
 
939
1111
  // libs/sdk/cli/src/shared/version.ts
940
1112
  var TOOL_VERSION = version;
@@ -985,6 +1157,11 @@ async function runSetup(opts) {
985
1157
  warnings.push(warning);
986
1158
  opts.onWarning?.(warning);
987
1159
  }
1160
+ const insecure = insecureTransportWarning(url);
1161
+ if (insecure) {
1162
+ warnings.push(insecure);
1163
+ opts.onWarning?.(insecure);
1164
+ }
988
1165
  let inputs = await resolveRequiredInputs(
989
1166
  { ...opts, url, authToken: auth.token, user: auth.user, password: auth.password },
990
1167
  opts.promptPort
@@ -1044,6 +1221,12 @@ async function runSetup(opts) {
1044
1221
  seen.add(file);
1045
1222
  plan.push({ target, file });
1046
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
+ }
1047
1230
  const outcomes = [];
1048
1231
  if (opts.skipMcp) {
1049
1232
  for (const { target, file } of plan) {
@@ -1090,14 +1273,21 @@ async function runSetup(opts) {
1090
1273
  let versionControl;
1091
1274
  const written = outcomes.filter((o) => (o.result === "written" || o.result === "replaced") && o.path).map((o) => o.path);
1092
1275
  if (scope === "folder" && written.length) {
1093
- versionControl = await protectFromVersionControl({
1094
- files: written,
1095
- // Which of them a project would normally commit is the registry's to say, not a
1096
- // basename set inside the gitignore module.
1097
- committedByConvention: plan.filter(({ target }) => target.folderConfigIsCommitted).map(({ file }) => file),
1098
- cwd: opts.cwd ?? process.cwd(),
1099
- confirmExclude: opts.yes ? async () => true : opts.confirmExclude
1100
- });
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
+ }
1101
1291
  }
1102
1292
  if (!opts.skipSkills) {
1103
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));
@@ -1105,6 +1295,12 @@ async function runSetup(opts) {
1105
1295
  if (ids.length) {
1106
1296
  step("Installing the dotCMS skills");
1107
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
+ }
1108
1304
  for (const o of outcomes) {
1109
1305
  if (o.result === "failed")
1110
1306
  continue;
@@ -1131,7 +1327,8 @@ async function runSetup(opts) {
1131
1327
  warnings,
1132
1328
  connection,
1133
1329
  connectionReason,
1134
- exitCode: anyFailed ? 1 : 0
1330
+ skillsSkipped: Boolean(opts.skipSkills),
1331
+ exitCode: anyFailed || configuredNothing ? 1 : 0
1135
1332
  };
1136
1333
  }
1137
1334
 
@@ -1186,6 +1383,9 @@ function renderSummary(input) {
1186
1383
  for (const w of vc.warnings)
1187
1384
  lines.push(chalk.yellow(` ! ${w}`));
1188
1385
  }
1386
+ if (input.skillsSkipped) {
1387
+ lines.push(" \xB7 skills installation skipped (--skip-skills)");
1388
+ }
1189
1389
  lines.push("");
1190
1390
  if (input.connection === "ok") {
1191
1391
  lines.push(chalk.green(" \u2713 server responded"));
@@ -1195,10 +1395,16 @@ function renderSummary(input) {
1195
1395
  lines.push(chalk.red(` \u2717 ${input.connectionReason ?? "the server did not start"}`));
1196
1396
  lines.push(" Configuration was written and left in place; the server did not come up.");
1197
1397
  }
1198
- const allGood = input.connection === "ok" && input.outcomes.every((o) => o.result !== "failed");
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("");
1199
1402
  if (allGood) {
1200
- lines.push("");
1201
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
+ );
1202
1408
  }
1203
1409
  return lines.join("\n");
1204
1410
  }
@@ -1321,7 +1527,8 @@ function registerAgentCommand(program2) {
1321
1527
  versionControl: result.versionControl,
1322
1528
  warnings: result.warnings,
1323
1529
  connection: result.connection,
1324
- connectionReason: result.connectionReason
1530
+ connectionReason: result.connectionReason,
1531
+ skillsSkipped: result.skillsSkipped
1325
1532
  })
1326
1533
  );
1327
1534
  process.exitCode = result.exitCode;
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
- "name": "dotcms",
3
- "version": "0.2.1",
4
- "type": "module",
5
- "main": "./index.js",
6
- "bin": {
7
- "dotcms": "./index.js"
8
- },
9
- "files": [
10
- "*.js",
11
- "README.md"
12
- ],
13
- "publishConfig": {
14
- "access": "public"
15
- },
16
- "dependencies": {
17
- "cfonts": "^3.3.1",
18
- "chalk": "^5.6.2",
19
- "commander": "^14.0.2",
20
- "inquirer": "^13.0.1",
21
- "jsonc-parser": "^3.3.1",
22
- "ora": "^9.0.0",
23
- "smol-toml": "^1.8.0"
24
- }
2
+ "name": "dotcms",
3
+ "version": "26.09.09-01",
4
+ "type": "module",
5
+ "main": "./index.js",
6
+ "bin": {
7
+ "dotcms": "./index.js"
8
+ },
9
+ "files": [
10
+ "*.js",
11
+ "README.md"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "dependencies": {
17
+ "cfonts": "^3.3.1",
18
+ "chalk": "^5.6.2",
19
+ "commander": "^14.0.2",
20
+ "inquirer": "^13.0.1",
21
+ "jsonc-parser": "^3.3.1",
22
+ "ora": "^9.0.0",
23
+ "smol-toml": "^1.8.0"
24
+ }
25
25
  }