cto-agent-system 1.2.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.2.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.2.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.2.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.2.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.2.0",
4
+ "version": "1.2.1",
5
5
  "author": {
6
6
  "name": "xenitV1",
7
7
  "url": "https://github.com/xenitV1"
package/install.js CHANGED
@@ -61,22 +61,26 @@ const ADAPTERS = [
61
61
  {
62
62
  key: "claude", name: "Claude Code", dir: ".claude", cmds: ["claude"],
63
63
  globalDir: () => join(homedir(), ".claude"),
64
- 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
+ ],
65
69
  },
66
70
  {
67
71
  key: "codex", name: "OpenAI Codex", dir: ".codex", cmds: ["codex"],
68
72
  globalDir: () => join(homedir(), ".codex"),
69
- pluginNote: "Codex plugin (.codex-plugin/) installed via folder copy — Codex auto-detects it.",
73
+ pluginCapable: false, // .codex-plugin auto-detected from folder copy
70
74
  },
71
75
  {
72
76
  key: "opencode", name: "OpenCode", dir: ".opencode", cmds: ["opencode"],
73
77
  globalDir: () => join(homedir(), ".config", "opencode"),
74
- pluginNote: "OpenCode agents/rules installed (OpenCode plugins are JS hooks, separate concept).",
78
+ pluginCapable: false, // OpenCode plugins are JS hooks, separate concept
75
79
  },
76
80
  {
77
81
  key: "cursor", name: "Cursor", dir: ".cursor", cmds: ["cursor"],
78
82
  globalDir: () => join(homedir(), ".cursor"),
79
- pluginNote: "Cursor plugin (.cursor-plugin/) installed via folder copy — Cursor auto-detects it.",
83
+ pluginCapable: false, // .cursor-plugin auto-detected from folder copy
80
84
  },
81
85
  ];
82
86
 
@@ -102,30 +106,41 @@ async function askYesNo(rl, question, defaultYes = true) {
102
106
  }
103
107
 
104
108
  // ---------------------------------------------------------------------------
105
- // Interactive multi-select (arrow keys + space to toggle + enter to confirm)
106
- // Zero-dependency: drives raw TTY mode directly. Falls back to a numbered
107
- // prompt when stdin is not a TTY (piped/CI).
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.
108
113
  // ---------------------------------------------------------------------------
109
114
 
110
- const ESC = "\x1b[";
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`;
111
121
 
112
- function hideCursor() { stdout.write(`${ESC}?25l`); }
113
- function showCursor() { stdout.write(`${ESC}?25h`); }
114
- function clearMenu(lines) { stdout.write(`${ESC}${lines}A${ESC}J`); } // move up, clear down
115
- function writeLine(s) { stdout.write(`${s}\r\n`); }
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
+ }
116
136
 
117
137
  /**
118
- * Render a multi-select menu.
119
- * @param {string} title Prompt header.
120
- * @param {{label:string, hint?:string, checked:boolean}[]} items
121
- * @param {boolean} allowToggle If false, behaves like a single-select.
122
- * @returns {Promise<number[]>} Indices of checked items (on confirm).
138
+ * Multi-select checkbox menu.
139
+ * @returns {Promise<number[]>} indices of checked items at confirm time.
123
140
  */
124
- function checkboxMenu(title, items, { allowToggle = true } = {}) {
141
+ function checkboxMenu(title, items) {
125
142
  return new Promise((resolvePromise) => {
126
- // Non-TTY fallback: numbered prompt.
127
- const isTTY = stdin.isTTY && stdin instanceof ReadStream;
128
- if (!isTTY) {
143
+ if (!isInteractiveTty()) {
129
144
  const rl = createInterface({ input: stdin, output: stdout });
130
145
  const fallback = async () => {
131
146
  console.log(title);
@@ -134,13 +149,12 @@ function checkboxMenu(title, items, { allowToggle = true } = {}) {
134
149
  console.log(` ${i + 1}. ${mark} ${it.label}${it.hint ? ` — ${it.hint}` : ""}`);
135
150
  });
136
151
  const ans = (await rl.question(
137
- `Enter numbers comma-separated (e.g. 1,3) or 'all': `
152
+ `Enter numbers comma-separated (e.g. 1,3) or 'all' (blank=all): `
138
153
  )).trim().toLowerCase();
139
154
  rl.close();
140
155
  if (ans === "all" || ans === "") return items.map((_, i) => i);
141
- const picked = ans.split(/[,\s]+/).map((n) => parseInt(n, 10) - 1)
156
+ return ans.split(/[,\s]+/).map((n) => parseInt(n, 10) - 1)
142
157
  .filter((n) => n >= 0 && n < items.length);
143
- return picked;
144
158
  };
145
159
  fallback().then(resolvePromise);
146
160
  return;
@@ -148,67 +162,55 @@ function checkboxMenu(title, items, { allowToggle = true } = {}) {
148
162
 
149
163
  let cursor = 0;
150
164
  const state = items.map((it) => !!it.checked);
151
- const headerLines = title.split("\n").length + 1; // title + blank
165
+ const titleLines = title.split("\n");
166
+ const totalLines = titleLines.length + 1 + items.length; // title + hint + items
167
+ let drawn = 0;
152
168
 
153
169
  const render = () => {
154
- writeLine(title);
155
- writeLine(" (↑/↓ move, space toggle, a all, enter confirm)");
170
+ if (drawn > 0) rewind(drawn);
171
+ const lines = [];
172
+ lines.push(...titleLines);
173
+ lines.push(" (↑/↓ move · space toggle · a = all · enter = confirm)");
156
174
  items.forEach((it, i) => {
157
175
  const arrow = i === cursor ? "❯" : " ";
158
176
  const box = state[i] ? "◉" : "◯";
159
- const hint = it.hint ? ` ${it.hint}` : "";
160
- writeLine(` ${arrow} ${box} ${it.label}${hint}`);
177
+ const hint = it.hint ? ` ${it.hint}` : "";
178
+ lines.push(` ${arrow} ${box} ${it.label}${hint}`);
161
179
  });
162
- };
163
-
164
- // Render once, then capture how many lines we wrote so we can clear on re-render.
165
- let lastLines = 0;
166
- const paint = () => {
167
- if (lastLines > 0) clearMenu(lastLines);
168
- const before = stdout.rows; // unused; we count lines instead
169
- const lineCount = headerLines + 1 + items.length; // title(=headerLines) + hint + items
170
- render();
171
- lastLines = lineCount;
180
+ stdout.write(lines.join("\r\n") + "\r\n");
181
+ drawn = totalLines;
172
182
  };
173
183
 
174
184
  stdin.setRawMode(true);
175
185
  stdin.resume();
176
186
  stdin.setEncoding("utf8");
177
- hideCursor();
178
- paint();
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
+ };
179
197
 
180
198
  const onData = (ch) => {
181
- // Ctrl-C
182
199
  if (ch === "\x03") { cleanup(); process.exit(0); }
183
- // Enter
184
200
  if (ch === "\r" || ch === "\n") {
185
201
  cleanup();
186
- const result = items.map((_, i) => i).filter((i) => state[i]);
187
- resolvePromise(result);
202
+ resolvePromise(items.map((_, i) => i).filter((i) => state[i]));
188
203
  return;
189
204
  }
190
- // 'a' = toggle all
191
205
  if (ch === "a" || ch === "A") {
192
206
  const allOn = state.every(Boolean);
193
207
  for (let i = 0; i < state.length; i++) state[i] = !allOn;
194
- paint();
195
- return;
196
- }
197
- // space = toggle current
198
- if (ch === " ") {
199
- if (allowToggle) { state[cursor] = !state[cursor]; paint(); }
208
+ render();
200
209
  return;
201
210
  }
202
- // arrows
203
- if (ch === `${ESC}A`) { cursor = (cursor - 1 + items.length) % items.length; paint(); return; } // up
204
- if (ch === `${ESC}B`) { cursor = (cursor + 1) % items.length; paint(); return; } // down
205
- };
206
-
207
- const cleanup = () => {
208
- stdin.removeListener("data", onData);
209
- stdin.setRawMode(false);
210
- stdin.pause();
211
- showCursor();
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; }
212
214
  };
213
215
 
214
216
  stdin.on("data", onData);
@@ -220,8 +222,7 @@ function checkboxMenu(title, items, { allowToggle = true } = {}) {
220
222
  */
221
223
  function selectMenu(title, items) {
222
224
  return new Promise((resolvePromise) => {
223
- const isTTY = stdin.isTTY && stdin instanceof ReadStream;
224
- if (!isTTY) {
225
+ if (!isInteractiveTty()) {
225
226
  const rl = createInterface({ input: stdin, output: stdout });
226
227
  const fallback = async () => {
227
228
  console.log(title);
@@ -235,36 +236,43 @@ function selectMenu(title, items) {
235
236
  }
236
237
 
237
238
  let cursor = 0;
238
- let lastLines = 0;
239
- const paint = () => {
240
- if (lastLines > 0) clearMenu(lastLines);
241
- writeLine(title);
242
- writeLine(" (↑/↓ move, enter select)");
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)");
243
248
  items.forEach((it, i) => {
244
249
  const arrow = i === cursor ? "❯" : " ";
245
- writeLine(` ${arrow} ${it.label}`);
250
+ lines.push(` ${arrow} ${it.label}`);
246
251
  });
247
- lastLines = 1 + 1 + items.length;
252
+ stdout.write(lines.join("\r\n") + "\r\n");
253
+ drawn = totalLines;
248
254
  };
249
255
 
250
256
  stdin.setRawMode(true);
251
257
  stdin.resume();
252
258
  stdin.setEncoding("utf8");
253
- hideCursor();
254
- paint();
259
+ stdout.write(HIDE);
260
+ render();
255
261
 
256
- const onData = (ch) => {
257
- if (ch === "\x03") { cleanup(); process.exit(0); }
258
- if (ch === "\r" || ch === "\n") { cleanup(); resolvePromise(cursor); return; }
259
- if (ch === `${ESC}A`) { cursor = (cursor - 1 + items.length) % items.length; paint(); return; }
260
- if (ch === `${ESC}B`) { cursor = (cursor + 1) % items.length; paint(); return; }
261
- };
262
262
  const cleanup = () => {
263
263
  stdin.removeListener("data", onData);
264
- stdin.setRawMode(false);
264
+ try { stdin.setRawMode(false); } catch {}
265
265
  stdin.pause();
266
- showCursor();
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; }
267
274
  };
275
+
268
276
  stdin.on("data", onData);
269
277
  });
270
278
  }
@@ -276,6 +284,15 @@ function selectMenu(title, items) {
276
284
  function log(icon, msg) { console.log(` ${icon} ${msg}`); }
277
285
 
278
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.
279
296
  mkdirSync(dst, { recursive: true });
280
297
  for (const name of readdirSync(src)) {
281
298
  const s = join(src, name);
@@ -314,21 +331,32 @@ function installAdapter(adapter, scope, force) {
314
331
  log("⚠", `${adapter.dir}/ not shipped (adapter not built yet) — skipping ${adapter.name}`);
315
332
  return;
316
333
  }
317
- const dst = join(adapterBase(adapter, scope), adapter.dir);
318
- if (existsSync(dst) && !force) {
319
- copyTree(srcDir, dst, { overwrite: false });
320
- 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}`);
321
346
  } else {
322
- copyTree(srcDir, dst, { overwrite: true });
323
- 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
+ }
324
355
  }
325
356
  }
326
357
 
327
- // Adapter base depends on scope; for project it's the project target.
358
+ // Project target (set in main()).
328
359
  let PROJECT_TARGET = process.cwd();
329
- function adapterBase(adapter, scope) {
330
- return scope === "global" ? adapter.globalDir() : PROJECT_TARGET;
331
- }
332
360
 
333
361
  function initState(target, force) {
334
362
  const cto = join(target, ".cto");
@@ -464,6 +492,27 @@ Examples:
464
492
  chosen = scope === "global" && installed.length ? installed : ADAPTERS;
465
493
  }
466
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
+
467
516
  // 5. Init .cto/ state
468
517
  let initCto;
469
518
  if (scope !== "project") {
@@ -482,17 +531,34 @@ Examples:
482
531
 
483
532
  installConstitution(target, force);
484
533
  if (scope === "project") installSrc(target, force);
485
- 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
+ }
486
541
  if (initCto) initState(target, force);
487
542
 
488
543
  console.log("");
489
544
  console.log(" ✅ Done.");
490
545
  console.log("");
491
546
  // Per-CLI next steps: each tool has a different plugin model.
492
- console.log(" Next steps per installed CLI:");
547
+ console.log(" Next steps per selected CLI:");
493
548
  for (const a of chosen) {
494
549
  console.log(` ── ${a.name} ──`);
495
- 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
+ }
496
562
  console.log("");
497
563
  }
498
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.2.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": {