dotcms 0.2.0 → 0.2.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 +397 -243
  2. package/package.json +2 -1
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
 
@@ -62,13 +59,13 @@ var SKILLS_SOURCE = "dotCMS/agent-toolkit";
62
59
 
63
60
  // libs/sdk/cli/src/commands/agent/connect.ts
64
61
  var DEFAULT_TIMEOUT_MS = 6e4;
65
- function classify(stderr, code) {
62
+ function classify(stderr) {
66
63
  if (/404|E404|not found|ETARGET|ENOTFOUND|registry/i.test(stderr))
67
64
  return "fetch-failed";
68
65
  if (/Unsupported engine|requires Node|SyntaxError|Unexpected token/i.test(stderr)) {
69
66
  return "runtime-unsupported";
70
67
  }
71
- return code === null ? "exited" : "exited";
68
+ return "exited";
72
69
  }
73
70
  async function confirmConnection(args) {
74
71
  const timeoutMs = args.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -98,13 +95,19 @@ async function confirmConnection(args) {
98
95
  resolve2(result);
99
96
  };
100
97
  const timer = setTimeout(
101
- () => finish({ ok: false, cause: "timeout", detail: `No response within ${timeoutMs}ms.` }),
98
+ () => finish({
99
+ ok: false,
100
+ cause: "timeout",
101
+ detail: `No response within ${timeoutMs}ms.`
102
+ }),
102
103
  timeoutMs
103
104
  );
104
105
  let buffer = "";
105
106
  child.stdout?.on("data", (chunk) => {
106
107
  buffer += String(chunk);
107
- for (const line of buffer.split("\n")) {
108
+ const frames = buffer.split("\n");
109
+ buffer = frames.pop() ?? "";
110
+ for (const line of frames) {
108
111
  if (!line.trim())
109
112
  continue;
110
113
  try {
@@ -124,7 +127,7 @@ async function confirmConnection(args) {
124
127
  "exit",
125
128
  (code) => finish({
126
129
  ok: false,
127
- cause: classify(stderr, code),
130
+ cause: classify(stderr),
128
131
  detail: stderr.trim().split("\n").slice(-1)[0] || `Server exited with code ${code}.`
129
132
  })
130
133
  );
@@ -133,7 +136,11 @@ async function confirmConnection(args) {
133
136
  jsonrpc: "2.0",
134
137
  id: 1,
135
138
  method: "initialize",
136
- params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "dotcms", version: "0" } }
139
+ params: {
140
+ protocolVersion: "2024-11-05",
141
+ capabilities: {},
142
+ clientInfo: { name: "dotcms", version: "0" }
143
+ }
137
144
  })}
138
145
  `
139
146
  );
@@ -146,7 +153,6 @@ async function confirmConnection(args) {
146
153
  import { existsSync } from "node:fs";
147
154
  import * as fs from "node:fs/promises";
148
155
  import * as path from "node:path";
149
- var CONVENTIONALLY_COMMITTED = /* @__PURE__ */ new Set([".mcp.json"]);
150
156
  function findRepositoryRoot(from) {
151
157
  let dir = path.resolve(from);
152
158
  for (; ; ) {
@@ -161,8 +167,9 @@ function findRepositoryRoot(from) {
161
167
  async function protectFromVersionControl(args) {
162
168
  const warnings = [];
163
169
  const root = findRepositoryRoot(args.cwd);
170
+ const committed = new Set(args.committedByConvention ?? []);
164
171
  for (const file of args.files) {
165
- if (CONVENTIONALLY_COMMITTED.has(path.basename(file))) {
172
+ if (committed.has(file)) {
166
173
  warnings.push(
167
174
  `${path.basename(file)} is normally committed to version control \u2014 it now holds a token, so committing it would publish that token.`
168
175
  );
@@ -223,10 +230,23 @@ async function installSkills(args) {
223
230
  }
224
231
  }
225
232
 
226
- // libs/sdk/cli/src/shared/config-file.ts
227
- import * as fs2 from "node:fs/promises";
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";
228
236
  import * as path2 from "node:path";
229
237
 
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
+ }
249
+
230
250
  // libs/sdk/cli/src/shared/errors.ts
231
251
  var CliError = class extends Error {
232
252
  constructor(message) {
@@ -246,7 +266,9 @@ var InvalidUrlError = class extends CliError {
246
266
  };
247
267
  var InstanceUnreachableError = class extends CliError {
248
268
  constructor(url, reason) {
249
- super(`Could not reach ${url} \u2014 ${reason}. Check the address and that the instance is running.`);
269
+ super(
270
+ `Could not reach ${url} \u2014 ${reason}. Check the address and that the instance is running.`
271
+ );
250
272
  }
251
273
  };
252
274
  var NotADotCmsInstanceError = class extends CliError {
@@ -284,137 +306,39 @@ var ConflictingAuthError = class extends UsageError {
284
306
  };
285
307
  var MissingInputError = class extends UsageError {
286
308
  constructor(what) {
287
- super(`${what} is required and there is no terminal to prompt on. Pass it as an option or set its environment variable.`);
309
+ super(
310
+ `${what} is required and there is no terminal to prompt on. Pass it as an option or set its environment variable.`
311
+ );
288
312
  }
289
313
  };
290
314
  var NoConfigPathError = class extends CliError {
291
315
  constructor(displayName, scope) {
292
316
  super(
293
- `${displayName} has no configuration file at ${scope} scope. Re-run with ${scope === "folder" ? "-g/--global" : "--project"}, or drop it from --agent.`
317
+ `${displayName} has no configuration file at ${scope} scope. Re-run ${scope === "folder" ? "with -g/--global" : "without -g/--global"}, or drop it from --agent.`
294
318
  );
295
319
  }
296
320
  };
297
321
  var MalformedConfigError = class extends CliError {
298
- constructor(file) {
322
+ /** The format is passed, not sniffed from the extension: both callers know exactly which
323
+ * parser just refused the file. */
324
+ constructor(file, format = "JSON") {
299
325
  super(
300
- `${file} is not valid ${file.endsWith(".toml") ? "TOML" : "JSON"} \u2014 fix it or re-run with --skip-mcp. It has been left untouched.`
326
+ `${file} is not valid ${format} \u2014 fix it or re-run with --skip-mcp. It has been left untouched.`
301
327
  );
302
328
  }
303
329
  };
304
330
 
305
- // libs/sdk/cli/src/shared/config-file.ts
306
- var CAN_RESTRICT = process.platform !== "win32";
307
- var FILE_MODE = 384;
308
- var DIR_MODE = 448;
309
- async function readJsonDocument(file) {
310
- let raw;
311
- try {
312
- raw = await fs2.readFile(file, "utf8");
313
- } catch {
314
- return null;
315
- }
316
- if (raw.trim() === "")
317
- return {};
318
- try {
319
- return JSON.parse(raw);
320
- } catch {
321
- throw new MalformedConfigError(file);
322
- }
323
- }
324
- async function hasEntry(args) {
325
- let doc;
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
- }
336
- const container = doc?.[args.containerKey] ?? {};
337
- return Object.prototype.hasOwnProperty.call(container, args.entryKey);
338
- }
339
- async function restrictFile(file, canRestrict = CAN_RESTRICT) {
340
- if (!canRestrict)
341
- return false;
342
- await fs2.chmod(file, FILE_MODE);
343
- return true;
344
- }
345
- async function ensureDir(dir) {
346
- const created = await fs2.mkdir(dir, { recursive: true });
347
- if (created && CAN_RESTRICT)
348
- await fs2.chmod(dir, DIR_MODE).catch(() => void 0);
349
- }
350
- async function writeMerged(args) {
351
- const existing = await readJsonDocument(args.file) ?? {};
352
- const container = existing[args.containerKey] ?? {};
353
- const replacedExisting = Object.prototype.hasOwnProperty.call(container, args.entryKey);
354
- const next = {
355
- ...existing,
356
- [args.containerKey]: { ...container, [args.entryKey]: args.entry }
357
- };
358
- await ensureDir(path2.dirname(args.file));
359
- await fs2.writeFile(args.file, `${JSON.stringify(next, null, 2)}
360
- `, "utf8");
361
- const permissionsApplied = await restrictFile(args.file, args.canRestrict ?? CAN_RESTRICT);
362
- return { path: args.file, permissionsApplied, replacedExisting };
363
- }
364
-
365
- // libs/sdk/cli/src/commands/agent/targets/json-target.ts
366
- function buildEntry(target, url, token) {
367
- const env = { [SERVER_ENV.url]: url, [SERVER_ENV.token]: token };
368
- if (target.entryShape === "opencode-local") {
369
- return {
370
- type: "local",
371
- command: ["npx", "-y", MCP_SERVER_PACKAGE],
372
- enabled: true,
373
- environment: env
374
- };
375
- }
376
- return { type: "stdio", command: "npx", args: ["-y", MCP_SERVER_PACKAGE], env };
377
- }
378
- async function writeJsonTargetDetailed(args) {
379
- const file = args.target.configPath(args.scope, args.cwd);
380
- if (!file)
381
- throw new NoConfigPathError(args.target.displayName, args.scope);
382
- return writeMerged({
383
- file,
384
- containerKey: args.target.containerKey,
385
- entryKey: ENTRY_KEY,
386
- entry: buildEntry(args.target, args.url, args.token)
387
- });
388
- }
389
-
390
- // libs/sdk/cli/src/commands/agent/targets/registry.ts
391
- import { existsSync as existsSync2 } from "node:fs";
392
- import * as os from "node:os";
393
- 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
331
  // libs/sdk/cli/src/commands/agent/targets/registry.ts
408
332
  var home = () => os.homedir();
409
- var inHome = (...parts) => path3.join(home(), ...parts);
410
- var inFolder = (cwd, ...parts) => path3.join(cwd ?? process.cwd(), ...parts);
333
+ var inHome = (...parts) => path2.join(home(), ...parts);
334
+ var inFolder = (cwd, ...parts) => path2.join(cwd ?? process.cwd(), ...parts);
411
335
  var probe = (...parts) => async () => existsSync2(inHome(...parts));
412
336
  function vscodeUserDir() {
413
337
  if (process.platform === "darwin")
414
338
  return inHome("Library", "Application Support", "Code", "User");
415
339
  if (process.platform === "win32") {
416
340
  const appData = process.env["APPDATA"] ?? inHome("AppData", "Roaming");
417
- return path3.join(appData, "Code", "User");
341
+ return path2.join(appData, "Code", "User");
418
342
  }
419
343
  return inHome(".config", "Code", "User");
420
344
  }
@@ -431,6 +355,7 @@ var TARGETS = [
431
355
  containerKey: "mcpServers",
432
356
  entryShape: "stdio",
433
357
  detect: probe(".claude"),
358
+ folderConfigIsCommitted: true,
434
359
  configPath: (scope, cwd) => scope === "global" ? inHome(".claude.json") : inFolder(cwd, ".mcp.json")
435
360
  },
436
361
  {
@@ -459,7 +384,7 @@ var TARGETS = [
459
384
  containerKey: "servers",
460
385
  entryShape: "stdio",
461
386
  detect: async () => existsSync2(vscodeUserDir()),
462
- configPath: (scope, cwd) => scope === "global" ? path3.join(vscodeUserDir(), "mcp.json") : inFolder(cwd, ".vscode", "mcp.json")
387
+ configPath: (scope, cwd) => scope === "global" ? path2.join(vscodeUserDir(), "mcp.json") : inFolder(cwd, ".vscode", "mcp.json")
463
388
  },
464
389
  {
465
390
  id: "codex",
@@ -470,7 +395,7 @@ var TARGETS = [
470
395
  containerKey: "mcp_servers",
471
396
  entryShape: "stdio",
472
397
  detect: probe(".codex"),
473
- configPath: (scope, cwd) => scope === "global" ? path3.join(codexHome(), "config.toml") : inFolder(cwd, ".codex", "config.toml")
398
+ configPath: (scope, cwd) => scope === "global" ? path2.join(codexHome(), "config.toml") : inFolder(cwd, ".codex", "config.toml")
474
399
  },
475
400
  {
476
401
  id: "antigravity",
@@ -519,10 +444,160 @@ async function detectTargets() {
519
444
  return results.filter((t) => t !== null);
520
445
  }
521
446
 
447
+ // libs/sdk/cli/src/commands/agent/targets/entry.ts
448
+ function buildEntry(target, url, token) {
449
+ const env = { [SERVER_ENV.url]: url, [SERVER_ENV.token]: token };
450
+ if (target.entryShape === "opencode-local") {
451
+ return {
452
+ type: "local",
453
+ command: ["npx", "-y", MCP_SERVER_PACKAGE],
454
+ enabled: true,
455
+ environment: env
456
+ };
457
+ }
458
+ return { type: "stdio", command: "npx", args: ["-y", MCP_SERVER_PACKAGE], env };
459
+ }
460
+
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
+ // libs/sdk/cli/src/commands/agent/targets/json-target.ts
573
+ function hasJsonEntry(file, target) {
574
+ return hasEntry({ file, containerKey: target.containerKey, entryKey: ENTRY_KEY });
575
+ }
576
+ async function writeJsonTarget(args) {
577
+ const file = args.target.configPath(args.scope, args.cwd);
578
+ if (!file)
579
+ throw new NoConfigPathError(args.target.displayName, args.scope);
580
+ return writeMerged({
581
+ file,
582
+ containerKey: args.target.containerKey,
583
+ entryKey: ENTRY_KEY,
584
+ entry: buildEntry(args.target, args.url, args.token)
585
+ });
586
+ }
587
+
522
588
  // libs/sdk/cli/src/commands/agent/targets/toml-target.ts
523
589
  import { parse, stringify } from "smol-toml";
524
590
  import * as fs3 from "node:fs/promises";
525
591
  import * as path4 from "node:path";
592
+ 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;
600
+ }
526
601
  function findEntrySpan(lines, containerKey) {
527
602
  const ours = new RegExp(`^\\s*\\[\\s*${containerKey}\\.${ENTRY_KEY}\\s*(\\.[^\\]]+)?\\]`);
528
603
  const anyHeader = /^\s*\[/;
@@ -560,17 +635,19 @@ async function writeTomlTarget(args) {
560
635
  try {
561
636
  parse(original);
562
637
  } catch {
563
- throw new MalformedConfigError(file);
638
+ throw new MalformedConfigError(file, "TOML");
564
639
  }
565
640
  }
566
641
  const block = renderEntry(args.target, args.url, args.token, args.target.containerKey);
567
642
  let next;
643
+ let replacedExisting = false;
568
644
  if (original.trim() === "") {
569
645
  next = block;
570
646
  } else {
571
647
  const lines = original.split("\n");
572
648
  const span = findEntrySpan(lines, args.target.containerKey);
573
649
  if (span) {
650
+ replacedExisting = true;
574
651
  lines.splice(span.start, span.end - span.start, ...block.trimEnd().split("\n"));
575
652
  next = lines.join("\n");
576
653
  } else {
@@ -581,8 +658,48 @@ async function writeTomlTarget(args) {
581
658
  await ensureDir(path4.dirname(file));
582
659
  await fs3.writeFile(file, next.endsWith("\n") ? next : `${next}
583
660
  `, "utf8");
584
- await restrictFile(file);
585
- return file;
661
+ return { path: file, permissionsApplied: await restrictFile(file), replacedExisting };
662
+ }
663
+
664
+ // libs/sdk/cli/src/commands/agent/targets/writers.ts
665
+ var WRITERS = {
666
+ json: { hasEntry: hasJsonEntry, write: writeJsonTarget },
667
+ toml: { hasEntry: hasTomlEntry, write: writeTomlTarget }
668
+ };
669
+
670
+ // libs/http/src/lib/fetch-retry.ts
671
+ function isSuccessStatus(status) {
672
+ return status >= 200 && status < 300;
673
+ }
674
+ function describeRequestFailure(error) {
675
+ if (isHttpError(error)) {
676
+ if (error.code === "ECONNREFUSED") {
677
+ return "Connection refused - service not accepting connections yet";
678
+ }
679
+ if (error.code === "ETIMEDOUT") {
680
+ return "Connection timeout - service too slow or not responding";
681
+ }
682
+ if (error.code === "ENOTFOUND") {
683
+ return "Host not found (DNS lookup failed)";
684
+ }
685
+ if (error.code === "ECONNRESET") {
686
+ return "Connection reset by the server";
687
+ }
688
+ if (error.code === "CERT_HAS_EXPIRED") {
689
+ return "TLS certificate has expired";
690
+ }
691
+ if (error.code === "DEPTH_ZERO_SELF_SIGNED_CERT" || error.code === "SELF_SIGNED_CERT_IN_CHAIN") {
692
+ return "TLS certificate is self-signed and not trusted";
693
+ }
694
+ if (error.response) {
695
+ return `HTTP ${error.response.status}: ${error.response.statusText}`;
696
+ }
697
+ return error.code || error.message;
698
+ }
699
+ if (error instanceof Error) {
700
+ return error.message;
701
+ }
702
+ return String(error);
586
703
  }
587
704
 
588
705
  // libs/http/src/lib/http.ts
@@ -601,9 +718,6 @@ function isHttpError(error) {
601
718
  return error instanceof HttpError;
602
719
  }
603
720
  var DEFAULT_TIMEOUT_MS2 = 1e4;
604
- function isSuccess(status) {
605
- return status >= 200 && status < 300;
606
- }
607
721
  async function readBody(response) {
608
722
  const text = await response.text().catch(() => "");
609
723
  if (!text) {
@@ -636,7 +750,7 @@ async function request(url, init, { token, timeoutMs = DEFAULT_TIMEOUT_MS2, acce
636
750
  clearTimeout(timer);
637
751
  }
638
752
  const data = await readBody(response);
639
- if (!isSuccess(response.status) && !acceptAnyStatus) {
753
+ if (!isSuccessStatus(response.status) && !acceptAnyStatus) {
640
754
  throw new HttpError(`Request failed with status code ${response.status}`, {
641
755
  status: response.status,
642
756
  statusText: response.statusText
@@ -659,38 +773,6 @@ function httpPost(url, body, options = {}) {
659
773
  );
660
774
  }
661
775
 
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
776
  // libs/http/src/lib/endpoints.ts
695
777
  var DOTCMS_API = {
696
778
  /** Reachability probe, and the source of the instance version. */
@@ -773,6 +855,26 @@ function readVersion(data) {
773
855
  const candidate = info?.version;
774
856
  return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
775
857
  }
858
+ function parseVersion(value) {
859
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
860
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
861
+ }
862
+ function compatibilityWarning(instanceVersion, toolVersion) {
863
+ if (!instanceVersion)
864
+ return null;
865
+ const instance = parseVersion(instanceVersion);
866
+ const tool = parseVersion(toolVersion);
867
+ if (!instance || !tool)
868
+ return null;
869
+ for (let i = 0; i < 3; i++) {
870
+ if (instance[i] > tool[i])
871
+ return null;
872
+ if (instance[i] < tool[i]) {
873
+ return `This tool targets dotCMS ${toolVersion}, but the instance reports ${instanceVersion}. Install dotcms@${instanceVersion} to match your instance.`;
874
+ }
875
+ }
876
+ return null;
877
+ }
776
878
 
777
879
  // libs/sdk/cli/src/shared/prompts.ts
778
880
  var DEFAULT_URL = "http://localhost:8082";
@@ -791,50 +893,52 @@ async function resolveInstanceUrl(opts, port, interactive = Boolean(port)) {
791
893
  return url;
792
894
  }
793
895
  async function resolveRequiredInputs(opts, port, interactive = Boolean(port)) {
794
- let prompted = false;
795
896
  const ask2 = async (what, run) => {
796
897
  if (!interactive || !port)
797
898
  throw new MissingInputError(what);
798
- prompted = true;
799
899
  return run();
800
900
  };
801
901
  const url = await resolveInstanceUrl(opts, port, interactive);
802
- if (!opts.url && !readEnv(ENV_KEYS.url))
803
- prompted = true;
804
902
  const authToken = opts.authToken ?? readEnv(ENV_KEYS.authToken);
805
903
  const user = opts.user;
806
904
  const password = opts.password ?? readEnv(ENV_KEYS.password);
807
905
  if (authToken)
808
- return { url, authToken, prompted };
906
+ return { url, authToken };
809
907
  if (user && password)
810
- return { url, user, password, prompted };
908
+ return { url, user, password };
811
909
  if (user) {
812
910
  const typed = await ask2("A password", () => port.password("Password"));
813
- return { url, user, password: typed, prompted };
911
+ return { url, user, password: typed };
814
912
  }
815
913
  if (password) {
816
914
  const typed = await ask2("A username", () => port.text("Username"));
817
- return { url, user: typed, password, prompted };
915
+ return { url, user: typed, password };
818
916
  }
819
917
  if (!interactive || !port) {
820
918
  throw new MissingInputError("A username and password, or an authentication token,");
821
919
  }
822
- prompted = true;
920
+ return { url, ...await promptForAuth(port) };
921
+ }
922
+ async function promptForAuth(port) {
823
923
  const mode = await port.select("How should we authenticate?", [
824
924
  { name: "Sign in with a username and password", value: "signin" },
825
925
  { name: "Paste an existing authentication token", value: "token" }
826
926
  ]);
827
927
  if (mode === "token") {
828
- return { url, authToken: await port.password("Authentication token"), prompted };
928
+ return { authToken: await port.password("Authentication token") };
829
929
  }
830
930
  return {
831
- url,
832
931
  user: await port.text("Username"),
833
- password: await port.password("Password"),
834
- prompted
932
+ password: await port.password("Password")
835
933
  };
836
934
  }
837
935
 
936
+ // libs/sdk/cli/package.json
937
+ var version = "0.2.0";
938
+
939
+ // libs/sdk/cli/src/shared/version.ts
940
+ var TOOL_VERSION = version;
941
+
838
942
  // libs/sdk/cli/src/commands/agent/setup.ts
839
943
  var MAX_AUTH_ATTEMPTS = 3;
840
944
  function resolveAuthMode(opts) {
@@ -861,10 +965,26 @@ async function runSetup(opts) {
861
965
  const auth = resolveAuthMode(opts);
862
966
  const explicitTargets = resolveTargets(opts);
863
967
  const scope = opts.scope ?? "folder";
968
+ const outcome = (targetId, path5, result, reason = null, extra = {}) => ({
969
+ targetId,
970
+ scope,
971
+ path: path5,
972
+ result,
973
+ reason,
974
+ permissionsApplied: false,
975
+ skillsInstalled: "no",
976
+ ...extra
977
+ });
864
978
  const step = opts.onProgress ?? (() => void 0);
979
+ const warnings = [];
865
980
  const url = await resolveInstanceUrl(opts, opts.promptPort);
866
981
  step(`Checking ${url}`);
867
- await checkReachable(url);
982
+ const instance = await checkReachable(url);
983
+ const warning = compatibilityWarning(instance.version, TOOL_VERSION);
984
+ if (warning) {
985
+ warnings.push(warning);
986
+ opts.onWarning?.(warning);
987
+ }
868
988
  let inputs = await resolveRequiredInputs(
869
989
  { ...opts, url, authToken: auth.token, user: auth.user, password: auth.password },
870
990
  opts.promptPort
@@ -890,7 +1010,8 @@ async function runSetup(opts) {
890
1010
  if (!rejected || !opts.promptPort || attempt >= MAX_AUTH_ATTEMPTS)
891
1011
  throw error;
892
1012
  opts.onAuthRetry?.(error.message, attempt, MAX_AUTH_ATTEMPTS);
893
- inputs = await resolveRequiredInputs({ url, cwd: opts.cwd }, opts.promptPort);
1013
+ const fresh = await promptForAuth(opts.promptPort);
1014
+ inputs = { url, ...fresh };
894
1015
  }
895
1016
  }
896
1017
  let targets;
@@ -926,68 +1047,44 @@ async function runSetup(opts) {
926
1047
  const outcomes = [];
927
1048
  if (opts.skipMcp) {
928
1049
  for (const { target, file } of plan) {
929
- outcomes.push({
930
- targetId: target.id,
931
- scope,
932
- path: file,
933
- result: "skipped",
934
- reason: "configuration writing skipped (--skip-mcp)",
935
- permissionsApplied: false,
936
- skillsInstalled: "no"
937
- });
1050
+ outcomes.push(
1051
+ outcome(target.id, file, "skipped", "configuration writing skipped (--skip-mcp)")
1052
+ );
938
1053
  }
939
- }
940
- if (!opts.skipMcp && plan.length) {
1054
+ } else if (plan.length) {
941
1055
  step(`Writing configuration for ${plan.length} editor${plan.length === 1 ? "" : "s"}`);
942
1056
  }
943
1057
  for (const { target, file } of opts.skipMcp ? [] : plan) {
944
1058
  try {
945
- const isToml = target.format === "toml";
946
- const existing = await hasEntry({
947
- file,
948
- containerKey: target.containerKey,
949
- entryKey: ENTRY_KEY,
950
- parse: isToml ? (raw) => parseToml(raw) : void 0
951
- });
1059
+ const writer = WRITERS[target.format];
1060
+ const existing = await writer.hasEntry(file, target);
952
1061
  if (existing && !opts.force && !opts.yes && opts.confirmOverwrite) {
953
1062
  const proceed = await opts.confirmOverwrite(file);
954
1063
  if (!proceed) {
955
- outcomes.push({
956
- targetId: target.id,
957
- scope,
958
- path: file,
959
- result: "skipped",
960
- reason: "left the existing entry in place",
961
- permissionsApplied: false,
962
- skillsInstalled: "no"
963
- });
1064
+ outcomes.push(
1065
+ outcome(target.id, file, "skipped", "left the existing entry in place")
1066
+ );
964
1067
  continue;
965
1068
  }
966
1069
  }
967
- const written2 = isToml ? {
968
- path: await writeTomlTarget({ target, scope, url, token: token.value, cwd: opts.cwd }),
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,
1070
+ const written2 = await writer.write({
1071
+ target,
974
1072
  scope,
975
- path: written2.path,
976
- result: written2.replacedExisting ? "replaced" : "written",
977
- reason: null,
978
- permissionsApplied: written2.permissionsApplied,
979
- skillsInstalled: "no"
1073
+ url,
1074
+ token: token.value,
1075
+ cwd: opts.cwd
980
1076
  });
1077
+ outcomes.push(
1078
+ outcome(
1079
+ target.id,
1080
+ written2.path,
1081
+ written2.replacedExisting ? "replaced" : "written",
1082
+ null,
1083
+ { permissionsApplied: written2.permissionsApplied }
1084
+ )
1085
+ );
981
1086
  } 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
- });
1087
+ outcomes.push(outcome(target.id, file, "failed", error.message));
991
1088
  }
992
1089
  }
993
1090
  let versionControl;
@@ -995,6 +1092,9 @@ async function runSetup(opts) {
995
1092
  if (scope === "folder" && written.length) {
996
1093
  versionControl = await protectFromVersionControl({
997
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),
998
1098
  cwd: opts.cwd ?? process.cwd(),
999
1099
  confirmExclude: opts.yes ? async () => true : opts.confirmExclude
1000
1100
  });
@@ -1016,14 +1116,23 @@ async function runSetup(opts) {
1016
1116
  let connection = "skipped";
1017
1117
  let connectionReason;
1018
1118
  if (!opts.skipVerify && !opts.skipMcp) {
1019
- step("Starting the server to confirm it responds (this can take a minute on a cold npx cache)");
1119
+ step(
1120
+ "Starting the server to confirm it responds (this can take a minute on a cold npx cache)"
1121
+ );
1020
1122
  const result = await confirmConnection({ url, token: token.value });
1021
1123
  connection = result.ok ? "ok" : "failed";
1022
1124
  if (!result.ok)
1023
1125
  connectionReason = `${result.cause}: ${result.detail}`;
1024
1126
  }
1025
1127
  const anyFailed = outcomes.some((o) => o.result === "failed") || connection === "failed";
1026
- return { outcomes, versionControl, connection, connectionReason, exitCode: anyFailed ? 1 : 0 };
1128
+ return {
1129
+ outcomes,
1130
+ versionControl,
1131
+ warnings,
1132
+ connection,
1133
+ connectionReason,
1134
+ exitCode: anyFailed ? 1 : 0
1135
+ };
1027
1136
  }
1028
1137
 
1029
1138
  // libs/sdk/cli/src/shared/ui.ts
@@ -1041,10 +1150,11 @@ var RESULT_MARK = {
1041
1150
  };
1042
1151
  function renderSummary(input) {
1043
1152
  const lines = [];
1153
+ const idWidth = Math.max(...input.outcomes.map((o) => o.targetId.length), 0);
1044
1154
  for (const o of input.outcomes) {
1045
1155
  const bits = [
1046
1156
  ` ${RESULT_MARK[o.result]}`,
1047
- o.targetId.padEnd(13),
1157
+ o.targetId.padEnd(idWidth),
1048
1158
  o.scope.padEnd(7),
1049
1159
  o.path ?? "\u2014"
1050
1160
  ];
@@ -1055,9 +1165,14 @@ function renderSummary(input) {
1055
1165
  lines.push(" could not restrict file permissions on this platform");
1056
1166
  }
1057
1167
  if (o.skillsInstalled === "unverified") {
1058
- lines.push(" skills location unverified for this editor \u2014 not confirmed installed");
1168
+ lines.push(
1169
+ " skills location unverified for this editor \u2014 not confirmed installed"
1170
+ );
1059
1171
  }
1060
1172
  }
1173
+ for (const w of input.warnings ?? []) {
1174
+ lines.push(chalk.yellow(` ! ${w}`));
1175
+ }
1061
1176
  if (input.versionControl?.files.length) {
1062
1177
  const vc = input.versionControl;
1063
1178
  lines.push("");
@@ -1106,8 +1221,24 @@ function makeProgress(interactive) {
1106
1221
  },
1107
1222
  done() {
1108
1223
  spinner?.stop();
1224
+ spinner = null;
1109
1225
  },
1110
- /** Leave the failed step visible instead of a spinner that never stops. */
1226
+ /**
1227
+ * Hand the terminal over to a question.
1228
+ *
1229
+ * Keeps the finished step visible, then stops repainting. `done()` erases the line;
1230
+ * here the step really did complete, so it should stay on screen above the prompt.
1231
+ */
1232
+ pause() {
1233
+ spinner?.succeed();
1234
+ spinner = null;
1235
+ },
1236
+ /**
1237
+ * Leave the failed step visible instead of a spinner that never stops.
1238
+ *
1239
+ * `warn` was a byte-identical second copy of this: a retry notice and a failure render
1240
+ * the same way — mark the attempt failed, then carry on.
1241
+ */
1111
1242
  fail(text) {
1112
1243
  if (spinner)
1113
1244
  spinner.fail(text);
@@ -1115,16 +1246,37 @@ function makeProgress(interactive) {
1115
1246
  writeOut(` \u2717 ${text}`);
1116
1247
  spinner = null;
1117
1248
  },
1118
- /** A retry notice: mark the attempt failed, then carry on. */
1119
1249
  warn(text) {
1120
- if (spinner)
1121
- spinner.fail(text);
1122
- else
1123
- writeOut(` \u2717 ${text}`);
1124
- spinner = null;
1250
+ this.fail(text);
1125
1251
  }
1126
1252
  };
1127
1253
  }
1254
+ function pausingPort(port, progress) {
1255
+ return {
1256
+ text(message, defaultValue) {
1257
+ progress.pause();
1258
+ return port.text(message, defaultValue);
1259
+ },
1260
+ password(message) {
1261
+ progress.pause();
1262
+ return port.password(message);
1263
+ },
1264
+ select(message, choices) {
1265
+ progress.pause();
1266
+ return port.select(message, choices);
1267
+ },
1268
+ multiSelect(message, choices) {
1269
+ progress.pause();
1270
+ return port.multiSelect(message, choices);
1271
+ }
1272
+ };
1273
+ }
1274
+ function pausingConfirm(confirm, progress) {
1275
+ return (...args) => {
1276
+ progress.pause();
1277
+ return confirm(...args);
1278
+ };
1279
+ }
1128
1280
  function registerAgentCommand(program2) {
1129
1281
  const agent = program2.command("agent").description("Connect an AI coding agent to dotCMS");
1130
1282
  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 +1290,18 @@ function registerAgentCommand(program2) {
1138
1290
  `editor to configure, repeatable (${TARGET_IDS.join(", ")})`,
1139
1291
  (value, previous = []) => [...previous, value]
1140
1292
  ).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
1293
  const interactive = canPrompt();
1294
+ if (interactive)
1295
+ printBanner();
1144
1296
  const progress = makeProgress(interactive);
1145
1297
  try {
1146
1298
  const result = await runSetup({
1147
1299
  onProgress: progress.step,
1148
1300
  onAuthRetry: (message, attempt, max) => progress.warn(`${message} (attempt ${attempt} of ${max})`),
1149
- promptPort: interactive ? inquirerPort : void 0,
1150
- confirmOverwrite: interactive ? confirmOverwrite : void 0,
1151
- confirmExclude: interactive ? confirmExclude : void 0,
1301
+ onWarning: (message) => progress.warn(message),
1302
+ promptPort: interactive ? pausingPort(inquirerPort, progress) : void 0,
1303
+ confirmOverwrite: interactive ? pausingConfirm(confirmOverwrite, progress) : void 0,
1304
+ confirmExclude: interactive ? pausingConfirm(confirmExclude, progress) : void 0,
1152
1305
  url: options["url"],
1153
1306
  user: options["user"],
1154
1307
  password: options["password"],
@@ -1166,6 +1319,7 @@ function registerAgentCommand(program2) {
1166
1319
  renderSummary({
1167
1320
  outcomes: result.outcomes,
1168
1321
  versionControl: result.versionControl,
1322
+ warnings: result.warnings,
1169
1323
  connection: result.connection,
1170
1324
  connectionReason: result.connectionReason
1171
1325
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotcms",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "chalk": "^5.6.2",
19
19
  "commander": "^14.0.2",
20
20
  "inquirer": "^13.0.1",
21
+ "jsonc-parser": "^3.3.1",
21
22
  "ora": "^9.0.0",
22
23
  "smol-toml": "^1.8.0"
23
24
  }