cto-agent-system 1.1.0 → 1.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.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "cto-agent-system",
11
11
  "description": "An autonomous software company: CEO (you) + CTO/CPO/CMO leading 15 specialist agents. Run /cto and the CTO takes over — digests the project, fixes fires, improves the product, reports back with a roadmap.",
12
- "version": "1.1.0",
12
+ "version": "1.2.1",
13
13
  "source": "./",
14
14
  "author": {
15
15
  "name": "xenitV1",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cto-agent-system",
3
3
  "description": "An autonomous software company: CEO (you) + CTO/CPO/CMO leading 15 specialist agents. Run /cto and the CTO takes over — digests the project, fixes fires, improves the product, reports back with a roadmap.",
4
- "version": "1.1.0",
4
+ "version": "1.2.1",
5
5
  "author": {
6
6
  "name": "xenitV1",
7
7
  "url": "https://github.com/xenitV1"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cto-agent-system",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "An autonomous software company: CEO (you) + CTO/CPO/CMO leading 15 specialist agents. Run /cto and the CTO takes over — digests the project, fixes fires, improves the product, reports back with a roadmap.",
5
5
  "author": {
6
6
  "name": "xenitV1",
@@ -2,7 +2,7 @@
2
2
  "name": "cto-agent-system",
3
3
  "displayName": "Software Company Agent System",
4
4
  "description": "An autonomous software company: CEO (you) + CTO/CPO/CMO leading 15 specialist agents. Run /cto and the CTO takes over.",
5
- "version": "1.1.0",
5
+ "version": "1.2.1",
6
6
  "author": {
7
7
  "name": "xenitV1",
8
8
  "url": "https://github.com/xenitV1"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cto-agent-system",
3
3
  "description": "An autonomous software company: CEO (you) + CTO/CPO/CMO leading 15 specialist agents. Run /cto and the CTO takes over — digests the project, fixes fires, improves the product, reports back with a roadmap.",
4
- "version": "1.1.0",
4
+ "version": "1.2.1",
5
5
  "author": {
6
6
  "name": "xenitV1",
7
7
  "url": "https://github.com/xenitV1"
package/install.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  import { dirname, join, resolve, isAbsolute } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { homedir, platform } from "node:os";
25
+ import { ReadStream } from "node:tty";
25
26
 
26
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
27
28
  // install.js lives at the package root (next to AGENTS.md, src/, .claude/).
@@ -60,22 +61,26 @@ const ADAPTERS = [
60
61
  {
61
62
  key: "claude", name: "Claude Code", dir: ".claude", cmds: ["claude"],
62
63
  globalDir: () => join(homedir(), ".claude"),
63
- pluginNote: "In Claude Code/ZCode, also install as a plugin for auto-updates:\n /plugin marketplace add xenitV1/cto-agent-system\n /plugin install cto-agent-system@cto-agent-marketplace",
64
+ pluginCapable: true,
65
+ pluginCmds: [
66
+ "/plugin marketplace add xenitV1/cto-agent-system",
67
+ "/plugin install cto-agent-system@cto-agent-marketplace",
68
+ ],
64
69
  },
65
70
  {
66
71
  key: "codex", name: "OpenAI Codex", dir: ".codex", cmds: ["codex"],
67
72
  globalDir: () => join(homedir(), ".codex"),
68
- pluginNote: "Codex plugin (.codex-plugin/) installed via folder copy — Codex auto-detects it.",
73
+ pluginCapable: false, // .codex-plugin auto-detected from folder copy
69
74
  },
70
75
  {
71
76
  key: "opencode", name: "OpenCode", dir: ".opencode", cmds: ["opencode"],
72
77
  globalDir: () => join(homedir(), ".config", "opencode"),
73
- pluginNote: "OpenCode agents/rules installed (OpenCode plugins are JS hooks, separate concept).",
78
+ pluginCapable: false, // OpenCode plugins are JS hooks, separate concept
74
79
  },
75
80
  {
76
81
  key: "cursor", name: "Cursor", dir: ".cursor", cmds: ["cursor"],
77
82
  globalDir: () => join(homedir(), ".cursor"),
78
- pluginNote: "Cursor plugin (.cursor-plugin/) installed via folder copy — Cursor auto-detects it.",
83
+ pluginCapable: false, // .cursor-plugin auto-detected from folder copy
79
84
  },
80
85
  ];
81
86
 
@@ -100,6 +105,178 @@ async function askYesNo(rl, question, defaultYes = true) {
100
105
  return answer === "y" || answer === "yes";
101
106
  }
102
107
 
108
+ // ---------------------------------------------------------------------------
109
+ // Interactive menus (arrow keys + space toggle + enter).
110
+ // Zero-dependency: raw TTY via node:tty. Non-TTY falls back to a numbered text
111
+ // prompt. We redraw by clearing each line individually with \x1b[2K and moving
112
+ // up, which is more reliable than whole-block clears.
113
+ // ---------------------------------------------------------------------------
114
+
115
+ const ESC = "\x1b";
116
+ const HIDE = `${ESC}[?25l`;
117
+ const SHOW = `${ESC}[?25h`;
118
+ const UP = `${ESC}[1A`;
119
+ const CLR = `${ESC}[2K`; // erase entire current line
120
+ const DOWN = `${ESC}[1B`;
121
+
122
+ function isInteractiveTty() {
123
+ return Boolean(stdin.isTTY && stdin instanceof ReadStream);
124
+ }
125
+
126
+ // Move up N lines and clear each, leaving the cursor at the top of the cleared block.
127
+ function rewind(lines) {
128
+ if (lines <= 0) return;
129
+ let out = "";
130
+ for (let i = 0; i < lines; i++) out += `${UP}${CLR}`;
131
+ // Drop back down to the first line of the block so we can re-render there.
132
+ for (let i = 0; i < lines; i++) out += DOWN;
133
+ out += UP.repeat(lines);
134
+ stdout.write(out);
135
+ }
136
+
137
+ /**
138
+ * Multi-select checkbox menu.
139
+ * @returns {Promise<number[]>} indices of checked items at confirm time.
140
+ */
141
+ function checkboxMenu(title, items) {
142
+ return new Promise((resolvePromise) => {
143
+ if (!isInteractiveTty()) {
144
+ const rl = createInterface({ input: stdin, output: stdout });
145
+ const fallback = async () => {
146
+ console.log(title);
147
+ items.forEach((it, i) => {
148
+ const mark = it.checked ? "[x]" : "[ ]";
149
+ console.log(` ${i + 1}. ${mark} ${it.label}${it.hint ? ` — ${it.hint}` : ""}`);
150
+ });
151
+ const ans = (await rl.question(
152
+ `Enter numbers comma-separated (e.g. 1,3) or 'all' (blank=all): `
153
+ )).trim().toLowerCase();
154
+ rl.close();
155
+ if (ans === "all" || ans === "") return items.map((_, i) => i);
156
+ return ans.split(/[,\s]+/).map((n) => parseInt(n, 10) - 1)
157
+ .filter((n) => n >= 0 && n < items.length);
158
+ };
159
+ fallback().then(resolvePromise);
160
+ return;
161
+ }
162
+
163
+ let cursor = 0;
164
+ const state = items.map((it) => !!it.checked);
165
+ const titleLines = title.split("\n");
166
+ const totalLines = titleLines.length + 1 + items.length; // title + hint + items
167
+ let drawn = 0;
168
+
169
+ const render = () => {
170
+ if (drawn > 0) rewind(drawn);
171
+ const lines = [];
172
+ lines.push(...titleLines);
173
+ lines.push(" (↑/↓ move · space toggle · a = all · enter = confirm)");
174
+ items.forEach((it, i) => {
175
+ const arrow = i === cursor ? "❯" : " ";
176
+ const box = state[i] ? "◉" : "◯";
177
+ const hint = it.hint ? ` ${it.hint}` : "";
178
+ lines.push(` ${arrow} ${box} ${it.label}${hint}`);
179
+ });
180
+ stdout.write(lines.join("\r\n") + "\r\n");
181
+ drawn = totalLines;
182
+ };
183
+
184
+ stdin.setRawMode(true);
185
+ stdin.resume();
186
+ stdin.setEncoding("utf8");
187
+ stdin.setRawMode(true);
188
+ stdout.write(HIDE);
189
+ render();
190
+
191
+ const cleanup = () => {
192
+ stdin.removeListener("data", onData);
193
+ try { stdin.setRawMode(false); } catch {}
194
+ stdin.pause();
195
+ stdout.write(SHOW);
196
+ };
197
+
198
+ const onData = (ch) => {
199
+ if (ch === "\x03") { cleanup(); process.exit(0); }
200
+ if (ch === "\r" || ch === "\n") {
201
+ cleanup();
202
+ resolvePromise(items.map((_, i) => i).filter((i) => state[i]));
203
+ return;
204
+ }
205
+ if (ch === "a" || ch === "A") {
206
+ const allOn = state.every(Boolean);
207
+ for (let i = 0; i < state.length; i++) state[i] = !allOn;
208
+ render();
209
+ return;
210
+ }
211
+ if (ch === " ") { state[cursor] = !state[cursor]; render(); return; }
212
+ if (ch === `${ESC}[A`) { cursor = (cursor - 1 + items.length) % items.length; render(); return; }
213
+ if (ch === `${ESC}[B`) { cursor = (cursor + 1) % items.length; render(); return; }
214
+ };
215
+
216
+ stdin.on("data", onData);
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Single-select menu (arrows + enter). Returns chosen index.
222
+ */
223
+ function selectMenu(title, items) {
224
+ return new Promise((resolvePromise) => {
225
+ if (!isInteractiveTty()) {
226
+ const rl = createInterface({ input: stdin, output: stdout });
227
+ const fallback = async () => {
228
+ console.log(title);
229
+ items.forEach((it, i) => console.log(` ${i + 1}. ${it.label}`));
230
+ const ans = parseInt((await rl.question("Choice (number): ")).trim(), 10);
231
+ rl.close();
232
+ return Number.isNaN(ans) ? 0 : Math.max(0, Math.min(items.length - 1, ans - 1));
233
+ };
234
+ fallback().then(resolvePromise);
235
+ return;
236
+ }
237
+
238
+ let cursor = 0;
239
+ const titleLines = title.split("\n");
240
+ const totalLines = titleLines.length + 1 + items.length;
241
+ let drawn = 0;
242
+
243
+ const render = () => {
244
+ if (drawn > 0) rewind(drawn);
245
+ const lines = [];
246
+ lines.push(...titleLines);
247
+ lines.push(" (↑/↓ move · enter = select)");
248
+ items.forEach((it, i) => {
249
+ const arrow = i === cursor ? "❯" : " ";
250
+ lines.push(` ${arrow} ${it.label}`);
251
+ });
252
+ stdout.write(lines.join("\r\n") + "\r\n");
253
+ drawn = totalLines;
254
+ };
255
+
256
+ stdin.setRawMode(true);
257
+ stdin.resume();
258
+ stdin.setEncoding("utf8");
259
+ stdout.write(HIDE);
260
+ render();
261
+
262
+ const cleanup = () => {
263
+ stdin.removeListener("data", onData);
264
+ try { stdin.setRawMode(false); } catch {}
265
+ stdin.pause();
266
+ stdout.write(SHOW);
267
+ };
268
+
269
+ const onData = (ch) => {
270
+ if (ch === "\x03") { cleanup(); process.exit(0); }
271
+ if (ch === "\r" || ch === "\n") { cleanup(); resolvePromise(cursor); return; }
272
+ if (ch === `${ESC}[A`) { cursor = (cursor - 1 + items.length) % items.length; render(); return; }
273
+ if (ch === `${ESC}[B`) { cursor = (cursor + 1) % items.length; render(); return; }
274
+ };
275
+
276
+ stdin.on("data", onData);
277
+ });
278
+ }
279
+
103
280
  // ---------------------------------------------------------------------------
104
281
  // File helpers
105
282
  // ---------------------------------------------------------------------------
@@ -107,6 +284,15 @@ async function askYesNo(rl, question, defaultYes = true) {
107
284
  function log(icon, msg) { console.log(` ${icon} ${msg}`); }
108
285
 
109
286
  function copyTree(src, dst, { overwrite = false } = {}) {
287
+ const stat = lstatSync(src);
288
+ if (stat.isFile()) {
289
+ // src is a single file — copy it (ensuring the parent dir exists).
290
+ if (!overwrite && existsSync(dst)) return;
291
+ mkdirSync(dirname(dst), { recursive: true });
292
+ cpSync(src, dst, { overwrite: true });
293
+ return;
294
+ }
295
+ // src is a directory — recurse.
110
296
  mkdirSync(dst, { recursive: true });
111
297
  for (const name of readdirSync(src)) {
112
298
  const s = join(src, name);
@@ -145,21 +331,32 @@ function installAdapter(adapter, scope, force) {
145
331
  log("⚠", `${adapter.dir}/ not shipped (adapter not built yet) — skipping ${adapter.name}`);
146
332
  return;
147
333
  }
148
- const dst = join(adapterBase(adapter, scope), adapter.dir);
149
- if (existsSync(dst) && !force) {
150
- copyTree(srcDir, dst, { overwrite: false });
151
- log("✓", `${adapter.dir}/ (merged) ${adapter.name}`);
334
+ // Project scope: write the adapter dir (e.g. <project>/.claude) as-is.
335
+ // Global scope: merge the CONTENTS of the adapter dir into the tool's
336
+ // global config dir (e.g. ~/.claude/), NOT a nested ~/.claude/.claude/.
337
+ // That's how Claude Code / Codex / OpenCode actually discover them.
338
+ if (scope === "global") {
339
+ const globalBase = adapter.globalDir();
340
+ mkdirSync(globalBase, { recursive: true });
341
+ // Merge each child of srcDir into globalBase.
342
+ for (const name of readdirSync(srcDir)) {
343
+ copyTree(join(srcDir, name), join(globalBase, name), { overwrite: force });
344
+ }
345
+ log("✓", `${adapter.dir}/* → ${globalBase} (global) — ${adapter.name}`);
152
346
  } else {
153
- copyTree(srcDir, dst, { overwrite: true });
154
- log("✓", `${adapter.dir}/ ${adapter.name}`);
347
+ const dst = join(PROJECT_TARGET, adapter.dir);
348
+ if (existsSync(dst) && !force) {
349
+ copyTree(srcDir, dst, { overwrite: false });
350
+ log("✓", `${adapter.dir}/ (merged) — ${adapter.name}`);
351
+ } else {
352
+ copyTree(srcDir, dst, { overwrite: true });
353
+ log("✓", `${adapter.dir}/ — ${adapter.name}`);
354
+ }
155
355
  }
156
356
  }
157
357
 
158
- // Adapter base depends on scope; for project it's the project target.
358
+ // Project target (set in main()).
159
359
  let PROJECT_TARGET = process.cwd();
160
- function adapterBase(adapter, scope) {
161
- return scope === "global" ? adapter.globalDir() : PROJECT_TARGET;
162
- }
163
360
 
164
361
  function initState(target, force) {
165
362
  const cto = join(target, ".cto");
@@ -236,20 +433,25 @@ Examples:
236
433
  console.log("");
237
434
 
238
435
  const interactive = !yes && stdin.isTTY;
239
- const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
240
436
 
241
- // 2. Scope
437
+ // 2. Scope — arrow-key single select (interactive) or flags/default.
242
438
  let scope;
243
439
  if (goGlobal) scope = "global";
244
440
  else if (goProject) scope = "project";
245
441
  else if (interactive) {
246
- if (installed.length > 0) {
247
- const g = await askYesNo(rl, "Install GLOBALLY for detected CLI(s) (user-wide)?", true);
248
- scope = g ? "global" : "project";
249
- } else scope = "project";
442
+ const scopeItems = installed.length > 0
443
+ ? [
444
+ { label: "Global — install into the user config of detected CLIs (user-wide)" },
445
+ { label: "Project install into a specific project directory" },
446
+ ]
447
+ : [
448
+ { label: "Project — install into a specific project directory" },
449
+ { label: "Global — install into the user config of detected CLIs" },
450
+ ];
451
+ const idx = await selectMenu("Where do you want to install?", scopeItems);
452
+ scope = scopeItems[idx].label.startsWith("Global") ? "global" : "project";
250
453
  } else {
251
- // Non-interactive default: project scope.
252
- scope = "project";
454
+ scope = "project"; // non-interactive default
253
455
  }
254
456
 
255
457
  // 3. Target
@@ -269,7 +471,7 @@ Examples:
269
471
  log("→", `Target: ${target}`);
270
472
  console.log("");
271
473
 
272
- // 4. Which adapters?
474
+ // 4. Which adapters — checkbox multi-select (interactive) or flags/default.
273
475
  let chosen;
274
476
  if (toolList) {
275
477
  const want = toolList.split(",").map((s) => s.trim()).filter(Boolean);
@@ -277,52 +479,86 @@ Examples:
277
479
  } else if (installAll) {
278
480
  chosen = scope === "global" && installed.length ? installed : ADAPTERS;
279
481
  } else if (interactive) {
280
- const all = await askYesNo(rl, "Install ALL adapters (claude/codex/opencode/cursor)?", true);
281
- if (all) {
282
- chosen = scope === "global" && installed.length ? installed : ADAPTERS;
283
- } else if (scope === "global" && installed.length) {
284
- chosen = [];
285
- for (const a of installed) {
286
- if (await askYesNo(rl, ` Install ${a.name}?`, true)) chosen.push(a);
287
- }
288
- } else {
289
- chosen = [];
290
- for (const a of ADAPTERS) {
291
- if (await askYesNo(rl, ` Install ${a.name} adapter?`, false)) chosen.push(a);
292
- }
293
- }
482
+ // Build checkbox items. Detected CLIs are pre-checked; others unchecked.
483
+ const detectedKeys = new Set(installed.map((a) => a.key));
484
+ const items = ADAPTERS.map((a) => ({
485
+ label: a.name,
486
+ hint: detectedKeys.has(a.key) ? "detected" : (existsSync(join(PKG_ROOT, a.dir)) ? "available" : "not shipped yet"),
487
+ checked: detectedKeys.has(a.key),
488
+ }));
489
+ const pickedIdx = await checkboxMenu("Select which CLI adapters to install:", items);
490
+ chosen = pickedIdx.map((i) => ADAPTERS[i]);
294
491
  } else {
295
- // Non-interactive default: all detected CLIs (global) or all adapters (project).
296
492
  chosen = scope === "global" && installed.length ? installed : ADAPTERS;
297
493
  }
298
494
 
495
+ // 4b. For plugin-capable adapters, ask: install as plugin (show commands) or copy files?
496
+ // npx cannot run a CLI's internal /plugin command, so "plugin" means: print the
497
+ // exact commands for the user to run inside their CLI, and skip file copy.
498
+ const installMethod = new Map(); // adapter.key -> "plugin" | "files"
499
+ const pluginCapable = chosen.filter((a) => a.pluginCapable);
500
+ if (interactive && pluginCapable.length > 0) {
501
+ console.log("");
502
+ for (const a of pluginCapable) {
503
+ const idx = await selectMenu(
504
+ `${a.name} supports plugins (auto-updates, namespaced). How to install?`,
505
+ [
506
+ { label: `Plugin — I'll run the /plugin commands in ${a.name} (recommended)` },
507
+ { label: "Files — copy the adapter files directly (no auto-updates)" },
508
+ ],
509
+ );
510
+ installMethod.set(a.key, idx === 0 ? "plugin" : "files");
511
+ }
512
+ } else {
513
+ for (const a of chosen) installMethod.set(a.key, "files");
514
+ }
515
+
299
516
  // 5. Init .cto/ state
300
517
  let initCto;
301
518
  if (scope !== "project") {
302
519
  initCto = false;
303
520
  } else if (interactive) {
304
- initCto = await askYesNo(rl, "Initialize .cto/ state files in the project?", true);
521
+ const idx = await selectMenu("Initialize .cto/ state files in the project?", [
522
+ { label: "Yes — create .cto/ state files" },
523
+ { label: "No — skip state init" },
524
+ ]);
525
+ initCto = idx === 0;
305
526
  } else {
306
- initCto = true; // non-interactive default for project scope
527
+ initCto = true;
307
528
  }
308
-
309
- if (rl) rl.close();
310
529
  console.log("");
311
530
  console.log(" ── Installing ──────────────────────────────────────────────");
312
531
 
313
532
  installConstitution(target, force);
314
533
  if (scope === "project") installSrc(target, force);
315
- for (const a of chosen) installAdapter(a, scope, force);
534
+ for (const a of chosen) {
535
+ if (installMethod.get(a.key) === "plugin") {
536
+ log("⏭", `${a.name}: skipped file copy — install via plugin commands below`);
537
+ } else {
538
+ installAdapter(a, scope, force);
539
+ }
540
+ }
316
541
  if (initCto) initState(target, force);
317
542
 
318
543
  console.log("");
319
544
  console.log(" ✅ Done.");
320
545
  console.log("");
321
546
  // Per-CLI next steps: each tool has a different plugin model.
322
- console.log(" Next steps per installed CLI:");
547
+ console.log(" Next steps per selected CLI:");
323
548
  for (const a of chosen) {
324
549
  console.log(` ── ${a.name} ──`);
325
- console.log(` ${a.pluginNote}`);
550
+ if (installMethod.get(a.key) === "plugin") {
551
+ console.log(` Install as a plugin (run inside ${a.name}):`);
552
+ for (const c of (a.pluginCmds || [])) console.log(` ${c}`);
553
+ } else if (a.key === "codex") {
554
+ console.log(` Codex plugin (.codex-plugin/) copied — Codex auto-detects it.`);
555
+ } else if (a.key === "opencode") {
556
+ console.log(` OpenCode agents/rules copied (OpenCode plugins are JS hooks, separate).`);
557
+ } else if (a.key === "cursor") {
558
+ console.log(` Cursor plugin (.cursor-plugin/) copied — Cursor auto-detects it.`);
559
+ } else {
560
+ console.log(` Adapter files copied into ${scope === "global" ? a.globalDir() : "the project"}.`);
561
+ }
326
562
  console.log("");
327
563
  }
328
564
  console.log(" Then start the CTO's daily loop:");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cto-agent-system",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "An autonomous software company: CEO (you) + CTO/CPO/CMO leading 15 specialist agents. Run /cto and the CTO takes over — digests the project, fixes fires, improves the product, reports back with a roadmap.",
5
5
  "license": "MIT",
6
6
  "author": {