quadwork 0.1.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -83
- package/bin/quadwork.js +512 -97
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +1 -1
- package/out/__next._full.txt +2 -2
- package/out/__next._head.txt +1 -1
- package/out/__next._index.txt +2 -2
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/chunks/0ahp74n0wkel0.js +1 -0
- package/out/_next/static/chunks/{0jsosmtclw5n5.js → 0dmi9pk2bd712.js} +3 -3
- package/out/_next/static/chunks/0ezniz80psxr6.js +1 -0
- package/out/_next/static/chunks/0g-nq4.uckan-.js +1 -0
- package/out/_next/static/chunks/0io_y3d0p5v~b.js +2 -0
- package/out/_next/static/chunks/0jt42fqe6jaw6.js +1 -0
- package/out/_next/static/chunks/{03hi.hdp6l230.js → 0q5hwcek8vu2q.js} +12 -12
- package/out/_next/static/chunks/0r_tb4lmfa_yb.js +1 -0
- package/out/_next/static/chunks/0s8jbc4nxw6y6.css +2 -0
- package/out/_next/static/chunks/0z~0.4hivi.f2.js +31 -0
- package/out/_next/static/chunks/135rms05ismy4.js +13 -0
- package/out/_next/static/chunks/14kr4rvjq-2md.js +1 -0
- package/out/_next/static/chunks/turbopack-0sammtvunroor.js +1 -0
- package/out/_not-found/__next._full.txt +2 -2
- package/out/_not-found/__next._head.txt +1 -1
- package/out/_not-found/__next._index.txt +2 -2
- package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/out/_not-found/__next._not-found.txt +1 -1
- package/out/_not-found/__next._tree.txt +2 -2
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +2 -2
- package/out/app-shell/__next._full.txt +18 -0
- package/out/app-shell/__next._head.txt +6 -0
- package/out/app-shell/__next._index.txt +6 -0
- package/out/app-shell/__next._tree.txt +3 -0
- package/out/app-shell/__next.app-shell.__PAGE__.txt +5 -0
- package/out/app-shell/__next.app-shell.txt +5 -0
- package/out/app-shell.html +1 -0
- package/out/app-shell.txt +18 -0
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/project/_/__next._full.txt +3 -4
- package/out/project/_/__next._head.txt +1 -1
- package/out/project/_/__next._index.txt +2 -2
- package/out/project/_/__next._tree.txt +2 -3
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +2 -3
- package/out/project/_/__next.project.$d$id.txt +1 -1
- package/out/project/_/__next.project.txt +1 -1
- package/out/project/_/memory/__next._full.txt +3 -3
- package/out/project/_/memory/__next._head.txt +1 -1
- package/out/project/_/memory/__next._index.txt +2 -2
- package/out/project/_/memory/__next._tree.txt +2 -2
- package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +2 -2
- package/out/project/_/memory/__next.project.$d$id.memory.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.txt +1 -1
- package/out/project/_/memory/__next.project.txt +1 -1
- package/out/project/_/memory.html +1 -1
- package/out/project/_/memory.txt +3 -3
- package/out/project/_/queue/__next._full.txt +3 -3
- package/out/project/_/queue/__next._head.txt +1 -1
- package/out/project/_/queue/__next._index.txt +2 -2
- package/out/project/_/queue/__next._tree.txt +2 -2
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +2 -2
- package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.txt +1 -1
- package/out/project/_/queue/__next.project.txt +1 -1
- package/out/project/_/queue.html +1 -1
- package/out/project/_/queue.txt +3 -3
- package/out/project/_.html +1 -1
- package/out/project/_.txt +3 -4
- package/out/settings/__next._full.txt +3 -3
- package/out/settings/__next._head.txt +1 -1
- package/out/settings/__next._index.txt +2 -2
- package/out/settings/__next._tree.txt +2 -2
- package/out/settings/__next.settings.__PAGE__.txt +2 -2
- package/out/settings/__next.settings.txt +1 -1
- package/out/settings.html +1 -1
- package/out/settings.txt +3 -3
- package/out/setup/__next._full.txt +3 -3
- package/out/setup/__next._head.txt +1 -1
- package/out/setup/__next._index.txt +2 -2
- package/out/setup/__next._tree.txt +2 -2
- package/out/setup/__next.setup.__PAGE__.txt +2 -2
- package/out/setup/__next.setup.txt +1 -1
- package/out/setup.html +1 -1
- package/out/setup.txt +3 -3
- package/package.json +1 -1
- package/server/config.js +66 -2
- package/server/index.js +344 -63
- package/server/routes.js +195 -68
- package/templates/CLAUDE.md +16 -17
- package/templates/config.toml +12 -12
- package/templates/seeds/{t3.AGENTS.md → dev.AGENTS.md} +19 -19
- package/templates/seeds/{t1.AGENTS.md → head.AGENTS.md} +18 -18
- package/templates/seeds/{t2a.AGENTS.md → reviewer1.AGENTS.md} +16 -16
- package/templates/seeds/{t2b.AGENTS.md → reviewer2.AGENTS.md} +16 -16
- package/out/_next/static/chunks/0.dzh0qf9zq1l.js +0 -2
- package/out/_next/static/chunks/03yov._jigv17.js +0 -1
- package/out/_next/static/chunks/0iqqouh_3i5y5.js +0 -13
- package/out/_next/static/chunks/13uu.sohs74zg.js +0 -31
- package/out/_next/static/chunks/15kwal..m9r49.css +0 -2
- package/out/_next/static/chunks/17sk4qv6_d0co.js +0 -1
- package/out/_next/static/chunks/turbopack-06pqx~0d8czn_.js +0 -1
- /package/out/_next/static/{vELqtMegFMn5_6zFOkhtG → 4vrILyy2mh_Ox4JMTaqx8}/_buildManifest.js +0 -0
- /package/out/_next/static/{vELqtMegFMn5_6zFOkhtG → 4vrILyy2mh_Ox4JMTaqx8}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{vELqtMegFMn5_6zFOkhtG → 4vrILyy2mh_Ox4JMTaqx8}/_ssgManifest.js +0 -0
package/bin/quadwork.js
CHANGED
|
@@ -11,7 +11,7 @@ const readline = require("readline");
|
|
|
11
11
|
const CONFIG_DIR = path.join(os.homedir(), ".quadwork");
|
|
12
12
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
13
13
|
const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
|
|
14
|
-
const AGENTS = ["
|
|
14
|
+
const AGENTS = ["head", "reviewer1", "reviewer2", "dev"];
|
|
15
15
|
|
|
16
16
|
// ─── ANSI Helpers ──────────────────────────────────────────────────────────
|
|
17
17
|
|
|
@@ -123,9 +123,33 @@ function askYN(rl, question, defaultYes = false) {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// Migration: rename old agent keys to new ones
|
|
127
|
+
const AGENT_KEY_MAP = { t1: "head", t2a: "reviewer1", t2b: "reviewer2", t3: "dev" };
|
|
128
|
+
|
|
129
|
+
function migrateAgentKeys(config) {
|
|
130
|
+
let changed = false;
|
|
131
|
+
if (config.projects) {
|
|
132
|
+
for (const project of config.projects) {
|
|
133
|
+
if (!project.agents) continue;
|
|
134
|
+
for (const [oldKey, newKey] of Object.entries(AGENT_KEY_MAP)) {
|
|
135
|
+
if (project.agents[oldKey] && !project.agents[newKey]) {
|
|
136
|
+
project.agents[newKey] = project.agents[oldKey];
|
|
137
|
+
delete project.agents[oldKey];
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (changed) {
|
|
144
|
+
try { writeConfig(config); } catch {}
|
|
145
|
+
}
|
|
146
|
+
return config;
|
|
147
|
+
}
|
|
148
|
+
|
|
126
149
|
function readConfig() {
|
|
127
150
|
try {
|
|
128
|
-
|
|
151
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
152
|
+
return migrateAgentKeys(config);
|
|
129
153
|
} catch {
|
|
130
154
|
return { port: 8400, agentchattr_url: "http://127.0.0.1:8300", projects: [] };
|
|
131
155
|
}
|
|
@@ -140,46 +164,295 @@ function writeConfig(config) {
|
|
|
140
164
|
|
|
141
165
|
let agentChattrFound = false;
|
|
142
166
|
|
|
143
|
-
function
|
|
167
|
+
function detectPlatform() {
|
|
168
|
+
const p = os.platform();
|
|
169
|
+
if (p === "darwin") return "macos";
|
|
170
|
+
if (p === "linux") {
|
|
171
|
+
// Check for apt vs dnf vs yum
|
|
172
|
+
if (which("apt")) return "linux-apt";
|
|
173
|
+
if (which("dnf")) return "linux-dnf";
|
|
174
|
+
if (which("yum")) return "linux-yum";
|
|
175
|
+
return "linux";
|
|
176
|
+
}
|
|
177
|
+
return "other";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function tryInstall(rl, name, description, commands, { platform } = {}) {
|
|
181
|
+
const cmd = typeof commands === "function" ? commands(platform) : commands;
|
|
182
|
+
if (!cmd) {
|
|
183
|
+
warn(`${name} cannot be auto-installed on your system.`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
console.log("");
|
|
187
|
+
log(`${description}`);
|
|
188
|
+
const doInstall = await askYN(rl, `Install ${name} now?`, true);
|
|
189
|
+
if (!doInstall) {
|
|
190
|
+
log("Skipped.");
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
const sp = spinner(`Installing ${name}...`);
|
|
194
|
+
const result = run(`${cmd} 2>&1`, { timeout: 120000 });
|
|
195
|
+
if (result !== null) {
|
|
196
|
+
sp.stop(true);
|
|
197
|
+
return true;
|
|
198
|
+
} else {
|
|
199
|
+
sp.stop(false);
|
|
200
|
+
warn(`Auto-install failed. You can install manually and try again.`);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function checkPrereqs(rl) {
|
|
144
206
|
header("Step 1: Prerequisites");
|
|
207
|
+
const platform = detectPlatform();
|
|
145
208
|
let allOk = true;
|
|
209
|
+
let hasPython = false;
|
|
210
|
+
let hasPipx = false;
|
|
146
211
|
|
|
147
|
-
// Node.js 20+
|
|
212
|
+
// ── 1. Node.js 20+ (must already exist — user ran npx) ──
|
|
148
213
|
const nodeVer = run("node --version");
|
|
149
214
|
if (nodeVer) {
|
|
150
215
|
const major = parseInt(nodeVer.replace("v", "").split(".")[0], 10);
|
|
151
|
-
if (major >= 20)
|
|
152
|
-
|
|
153
|
-
|
|
216
|
+
if (major >= 20) {
|
|
217
|
+
ok(`Node.js ${nodeVer}`);
|
|
218
|
+
} else {
|
|
219
|
+
fail(`Node.js ${nodeVer} — version 20 or newer is required`);
|
|
220
|
+
log("Update from: https://nodejs.org");
|
|
221
|
+
allOk = false;
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
fail("Node.js not found (this shouldn't happen since you ran npx)");
|
|
225
|
+
allOk = false;
|
|
226
|
+
}
|
|
154
227
|
|
|
155
|
-
// Python 3.10+
|
|
228
|
+
// ── 2. Python 3.10+ (manual install — guide only) ──
|
|
156
229
|
const pyVer = run("python3 --version");
|
|
157
230
|
if (pyVer) {
|
|
158
231
|
const parts = pyVer.replace("Python ", "").split(".");
|
|
159
232
|
const minor = parseInt(parts[1], 10);
|
|
160
|
-
if (parseInt(parts[0], 10) >= 3 && minor >= 10)
|
|
161
|
-
|
|
162
|
-
|
|
233
|
+
if (parseInt(parts[0], 10) >= 3 && minor >= 10) {
|
|
234
|
+
ok(`${pyVer}`);
|
|
235
|
+
hasPython = true;
|
|
236
|
+
} else {
|
|
237
|
+
console.log("");
|
|
238
|
+
warn(`${pyVer} found, but version 3.10 or newer is required.`);
|
|
239
|
+
log("Python powers the agent communication layer.");
|
|
240
|
+
log("Download the latest version from:");
|
|
241
|
+
log(` → https://python.org/downloads`);
|
|
242
|
+
log("");
|
|
243
|
+
log("After installing, close and reopen your terminal, then run:");
|
|
244
|
+
log(" → npx quadwork init");
|
|
245
|
+
allOk = false;
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
console.log("");
|
|
249
|
+
warn("Python 3 is required but not installed on your system.");
|
|
250
|
+
log("");
|
|
251
|
+
log("Python powers the agent communication layer. Install it from:");
|
|
252
|
+
log(" → https://python.org/downloads (download and run the installer)");
|
|
253
|
+
log("");
|
|
254
|
+
log("After installing, close and reopen your terminal, then run:");
|
|
255
|
+
log(" → npx quadwork init");
|
|
256
|
+
allOk = false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!hasPython) {
|
|
260
|
+
// Can't continue with pipx/AgentChattr without Python
|
|
261
|
+
console.log("");
|
|
262
|
+
fail("Python is required before we can set up the remaining tools.");
|
|
263
|
+
log("Install Python first, then re-run: npx quadwork init");
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
163
266
|
|
|
164
|
-
//
|
|
267
|
+
// ── 3. pipx (needs Python) ──
|
|
268
|
+
if (which("pipx")) {
|
|
269
|
+
ok("pipx");
|
|
270
|
+
hasPipx = true;
|
|
271
|
+
} else {
|
|
272
|
+
console.log("");
|
|
273
|
+
warn("pipx is needed to install AgentChattr safely.");
|
|
274
|
+
log("(pipx keeps Python tools isolated so they don't conflict with your system)");
|
|
275
|
+
const installed = await tryInstall(rl, "pipx", "We can install it automatically.",
|
|
276
|
+
"python3 -m pip install --user pipx && python3 -m pipx ensurepath");
|
|
277
|
+
if (installed && which("pipx")) {
|
|
278
|
+
ok("pipx installed");
|
|
279
|
+
hasPipx = true;
|
|
280
|
+
} else if (installed) {
|
|
281
|
+
// pipx installed but not in PATH yet
|
|
282
|
+
warn("pipx was installed but isn't in your PATH yet.");
|
|
283
|
+
log("Close and reopen your terminal, then run: npx quadwork init");
|
|
284
|
+
return false;
|
|
285
|
+
} else {
|
|
286
|
+
warn("pipx skipped — you can install it later:");
|
|
287
|
+
log(" → python3 -m pip install --user pipx && pipx ensurepath");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── 4. AgentChattr (needs pipx) ──
|
|
165
292
|
const acVer = run("agentchattr --version") || run("python3 -m agentchattr --version");
|
|
166
|
-
if (acVer) {
|
|
167
|
-
|
|
293
|
+
if (acVer) {
|
|
294
|
+
ok(`AgentChattr ${acVer}`);
|
|
295
|
+
agentChattrFound = true;
|
|
296
|
+
} else if (hasPipx) {
|
|
297
|
+
console.log("");
|
|
298
|
+
warn("AgentChattr lets your AI agents communicate with each other.");
|
|
299
|
+
const installed = await tryInstall(rl, "AgentChattr",
|
|
300
|
+
"We can install it now using pipx.", "pipx install agentchattr");
|
|
301
|
+
const acVerAfter = run("agentchattr --version") || run("python3 -m agentchattr --version");
|
|
302
|
+
if (acVerAfter) {
|
|
303
|
+
ok(`AgentChattr ${acVerAfter} installed`);
|
|
304
|
+
agentChattrFound = true;
|
|
305
|
+
} else {
|
|
306
|
+
warn("AgentChattr not available — agents won't be able to chat until it's installed.");
|
|
307
|
+
log(" → Install later: pipx install agentchattr");
|
|
308
|
+
allOk = false;
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
warn("AgentChattr not found — install pipx first, then: pipx install agentchattr");
|
|
312
|
+
allOk = false;
|
|
313
|
+
}
|
|
168
314
|
|
|
169
|
-
//
|
|
170
|
-
if (which("gh"))
|
|
171
|
-
|
|
315
|
+
// ── 5. GitHub CLI (independent) ──
|
|
316
|
+
if (which("gh")) {
|
|
317
|
+
ok("GitHub CLI (gh)");
|
|
318
|
+
} else {
|
|
319
|
+
console.log("");
|
|
320
|
+
warn("GitHub CLI is required for agents to create branches, PRs, and reviews.");
|
|
321
|
+
const ghCmd = (p) => {
|
|
322
|
+
if (p === "macos") return "brew install gh";
|
|
323
|
+
if (p === "linux-apt") return "sudo apt install gh -y";
|
|
324
|
+
if (p === "linux-dnf") return "sudo dnf install gh -y";
|
|
325
|
+
return null;
|
|
326
|
+
};
|
|
327
|
+
const cmd = ghCmd(platform);
|
|
328
|
+
if (cmd) {
|
|
329
|
+
const installed = await tryInstall(rl, "GitHub CLI",
|
|
330
|
+
"We can install it now.", ghCmd, { platform });
|
|
331
|
+
if (installed && which("gh")) {
|
|
332
|
+
ok("GitHub CLI installed");
|
|
333
|
+
} else {
|
|
334
|
+
fail("GitHub CLI is required. Install from: https://cli.github.com");
|
|
335
|
+
allOk = false;
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
fail("GitHub CLI is required. Install from: https://cli.github.com");
|
|
339
|
+
allOk = false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── 6. AI CLIs — at least one required (independent) ──
|
|
344
|
+
let hasClaude = which("claude");
|
|
345
|
+
let hasCodex = which("codex");
|
|
172
346
|
|
|
173
|
-
// Claude Code or Codex
|
|
174
|
-
const hasClaude = which("claude");
|
|
175
|
-
const hasCodex = which("codex");
|
|
176
347
|
if (hasClaude) ok("Claude Code");
|
|
177
348
|
if (hasCodex) ok("Codex CLI");
|
|
349
|
+
|
|
350
|
+
if (!hasClaude && !hasCodex) {
|
|
351
|
+
console.log("");
|
|
352
|
+
warn("You need at least one AI CLI to power your agents.");
|
|
353
|
+
log("Choose one (or both) to install:");
|
|
354
|
+
console.log("");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Offer to install Claude Code if missing
|
|
358
|
+
if (!hasClaude) {
|
|
359
|
+
const isRequired = !hasCodex;
|
|
360
|
+
log("Claude Code — Anthropic's AI coding assistant");
|
|
361
|
+
const installClaude = await askYN(rl, "Install Claude Code?", isRequired);
|
|
362
|
+
if (installClaude) {
|
|
363
|
+
const sp = spinner("Installing Claude Code...");
|
|
364
|
+
const result = run("npm install -g @anthropic-ai/claude-code 2>&1", { timeout: 120000 });
|
|
365
|
+
sp.stop(result !== null);
|
|
366
|
+
hasClaude = which("claude");
|
|
367
|
+
if (hasClaude) ok("Claude Code installed");
|
|
368
|
+
else warn("Install failed — try manually: npm install -g @anthropic-ai/claude-code");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Offer to install Codex CLI if missing
|
|
373
|
+
if (!hasCodex) {
|
|
374
|
+
const isRequired = !hasClaude;
|
|
375
|
+
if (hasClaude) {
|
|
376
|
+
console.log("");
|
|
377
|
+
log("Tip: Installing Codex CLI too gives your team different AI perspectives.");
|
|
378
|
+
}
|
|
379
|
+
log("Codex CLI — OpenAI's AI coding assistant");
|
|
380
|
+
const installCodex = await askYN(rl, "Install Codex CLI?", isRequired);
|
|
381
|
+
if (installCodex) {
|
|
382
|
+
const sp = spinner("Installing Codex CLI...");
|
|
383
|
+
const result = run("npm install -g codex 2>&1", { timeout: 120000 });
|
|
384
|
+
sp.stop(result !== null);
|
|
385
|
+
hasCodex = which("codex");
|
|
386
|
+
if (hasCodex) ok("Codex CLI installed");
|
|
387
|
+
else warn("Install failed — try manually: npm install -g codex");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
178
391
|
if (!hasClaude && !hasCodex) {
|
|
179
|
-
fail("
|
|
392
|
+
fail("At least one AI CLI is required (Claude Code or Codex CLI).");
|
|
393
|
+
log("Install one and re-run: npx quadwork init");
|
|
180
394
|
allOk = false;
|
|
181
395
|
}
|
|
182
396
|
|
|
397
|
+
// ── CLI Authentication Checks ──
|
|
398
|
+
if (allOk) {
|
|
399
|
+
console.log("");
|
|
400
|
+
log("Checking CLI authentication...");
|
|
401
|
+
console.log("");
|
|
402
|
+
|
|
403
|
+
// GitHub CLI auth
|
|
404
|
+
const ghAuth = run("gh auth status 2>&1");
|
|
405
|
+
if (ghAuth && ghAuth.includes("Logged in")) {
|
|
406
|
+
ok("GitHub CLI — authenticated");
|
|
407
|
+
} else {
|
|
408
|
+
warn("GitHub CLI is installed but not logged in.");
|
|
409
|
+
log(" Run this command to log in:");
|
|
410
|
+
log(" → gh auth login");
|
|
411
|
+
log("");
|
|
412
|
+
const recheck = await askYN(rl, "Done? Press Y to re-check, or N to continue anyway", false);
|
|
413
|
+
if (recheck) {
|
|
414
|
+
const ghAuth2 = run("gh auth status 2>&1");
|
|
415
|
+
if (ghAuth2 && ghAuth2.includes("Logged in")) {
|
|
416
|
+
ok("GitHub CLI — authenticated");
|
|
417
|
+
} else {
|
|
418
|
+
warn("Still not authenticated — you can set this up later.");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Claude Code auth
|
|
424
|
+
if (hasClaude) {
|
|
425
|
+
const claudeAuth = run("claude auth status 2>&1") || run("claude --version 2>&1");
|
|
426
|
+
if (claudeAuth && (claudeAuth.includes("authenticated") || claudeAuth.includes("Logged in") || claudeAuth.includes("@"))) {
|
|
427
|
+
ok("Claude Code — authenticated");
|
|
428
|
+
} else {
|
|
429
|
+
warn("Claude Code may need authentication.");
|
|
430
|
+
log(" If prompted when agents start, run: claude auth login");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Codex CLI auth
|
|
435
|
+
if (hasCodex) {
|
|
436
|
+
const codexAuth = run("codex auth status 2>&1") || run("codex --version 2>&1");
|
|
437
|
+
if (codexAuth && (codexAuth.includes("authenticated") || codexAuth.includes("Logged in") || codexAuth.includes("@"))) {
|
|
438
|
+
ok("Codex CLI — authenticated");
|
|
439
|
+
} else {
|
|
440
|
+
warn("Codex CLI may need authentication.");
|
|
441
|
+
log(" If prompted when agents start, run: codex auth");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── Summary ──
|
|
447
|
+
console.log("");
|
|
448
|
+
if (allOk) {
|
|
449
|
+
ok("All prerequisites ready!");
|
|
450
|
+
} else {
|
|
451
|
+
console.log("");
|
|
452
|
+
log("Some prerequisites are missing. Fix the issues above and re-run:");
|
|
453
|
+
log(" → npx quadwork init");
|
|
454
|
+
}
|
|
455
|
+
|
|
183
456
|
return allOk;
|
|
184
457
|
}
|
|
185
458
|
|
|
@@ -223,31 +496,55 @@ async function setupGitHub(rl) {
|
|
|
223
496
|
async function setupAgents(rl, repo) {
|
|
224
497
|
header("Step 3: Agent Configuration");
|
|
225
498
|
|
|
226
|
-
//
|
|
499
|
+
// Detect available CLIs
|
|
227
500
|
const hasClaude = which("claude");
|
|
228
501
|
const hasCodex = which("codex");
|
|
502
|
+
const bothAvailable = hasClaude && hasCodex;
|
|
503
|
+
const onlyOneCli = (hasClaude && !hasCodex) || (!hasClaude && hasCodex);
|
|
229
504
|
let defaultBackend = hasClaude ? "claude" : "codex";
|
|
230
|
-
log("Choose which AI CLI to run in agent terminals. Claude Code (`claude`) or OpenAI Codex (`codex`).");
|
|
231
|
-
const backend = await ask(rl, "Default CLI backend (claude/codex)", defaultBackend);
|
|
232
|
-
if (backend !== "claude" && backend !== "codex") {
|
|
233
|
-
fail("Backend must be 'claude' or 'codex'");
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
505
|
|
|
237
|
-
// Per-agent backend selection
|
|
238
506
|
const backends = {};
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
507
|
+
|
|
508
|
+
if (onlyOneCli) {
|
|
509
|
+
// Single-CLI mode: default all agents, no prompt needed
|
|
510
|
+
const cliName = hasClaude ? "Claude Code" : "Codex CLI";
|
|
511
|
+
const otherName = hasClaude ? "Codex CLI" : "Claude Code";
|
|
512
|
+
const installCmd = hasClaude ? "npm install -g codex" : "npm install -g @anthropic-ai/claude-code";
|
|
513
|
+
ok(`${cliName} detected — all 4 agents will use ${cliName}.`);
|
|
514
|
+
console.log("");
|
|
515
|
+
log(`Tip: Installing ${otherName} too gives your team different AI perspectives,`);
|
|
516
|
+
log(`which can improve code review quality. You can add it anytime:`);
|
|
517
|
+
log(` → ${installCmd}`);
|
|
518
|
+
console.log("");
|
|
519
|
+
for (const agent of AGENTS) backends[agent] = defaultBackend;
|
|
520
|
+
} else if (bothAvailable) {
|
|
521
|
+
log("Both Claude Code and Codex CLI are available.");
|
|
522
|
+
log("Choose which AI CLI to run in agent terminals.");
|
|
523
|
+
const backend = await ask(rl, "Default CLI backend (claude/codex)", defaultBackend);
|
|
524
|
+
if (backend !== "claude" && backend !== "codex") {
|
|
525
|
+
fail("Backend must be 'claude' or 'codex'");
|
|
526
|
+
return null;
|
|
246
527
|
}
|
|
528
|
+
defaultBackend = backend;
|
|
529
|
+
|
|
530
|
+
// Per-agent backend selection
|
|
531
|
+
const customPerAgent = await askYN(rl, "Use same backend for all agents?", true);
|
|
532
|
+
if (customPerAgent) {
|
|
533
|
+
for (const agent of AGENTS) backends[agent] = backend;
|
|
534
|
+
} else {
|
|
535
|
+
for (const agent of AGENTS) {
|
|
536
|
+
const agentBackend = await ask(rl, `${agent.toUpperCase()} backend (claude/codex)`, backend);
|
|
537
|
+
backends[agent] = (agentBackend === "claude" || agentBackend === "codex") ? agentBackend : backend;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
fail("No AI CLI found — install Claude Code or Codex CLI first.");
|
|
542
|
+
return null;
|
|
247
543
|
}
|
|
544
|
+
const backend = defaultBackend;
|
|
248
545
|
|
|
249
546
|
log("Path to your local clone of the repo. Four worktrees will be created next to it");
|
|
250
|
-
log("(e.g., project-
|
|
547
|
+
log("(e.g., project-head/, project-reviewer1/, project-reviewer2/, project-dev/).");
|
|
251
548
|
const projectDir = await ask(rl, "Project directory", process.cwd());
|
|
252
549
|
const absDir = path.resolve(projectDir);
|
|
253
550
|
|
|
@@ -263,12 +560,12 @@ async function setupAgents(rl, repo) {
|
|
|
263
560
|
}
|
|
264
561
|
|
|
265
562
|
// Prompt for reviewer credentials (optional)
|
|
266
|
-
log("A separate reviewer account lets
|
|
267
|
-
const wantReviewer = await askYN(rl, "Use a separate GitHub account for reviewers (
|
|
563
|
+
log("A separate reviewer account lets Reviewer1/Reviewer2 approve PRs independently. You can set this up later in Settings.");
|
|
564
|
+
const wantReviewer = await askYN(rl, "Use a separate GitHub account for reviewers (Reviewer1/Reviewer2)?", false);
|
|
268
565
|
let reviewerUser = "";
|
|
269
566
|
let reviewerTokenPath = "";
|
|
270
567
|
if (wantReviewer) {
|
|
271
|
-
log("GitHub username for the reviewer account (used in
|
|
568
|
+
log("GitHub username for the reviewer account (used in Reviewer1/Reviewer2 seed files for PR reviews).");
|
|
272
569
|
reviewerUser = await ask(rl, "Reviewer GitHub username", "");
|
|
273
570
|
log("Path to a file containing a GitHub PAT for the reviewer account.");
|
|
274
571
|
reviewerTokenPath = await ask(rl, "Reviewer token file path", path.join(os.homedir(), ".quadwork", "reviewer-token"));
|
|
@@ -342,7 +639,7 @@ function writeAgentChattrConfig(setup, configTomlPath, { skipInstall = false } =
|
|
|
342
639
|
|
|
343
640
|
let tomlContent = fs.readFileSync(path.join(TEMPLATES_DIR, "config.toml"), "utf-8");
|
|
344
641
|
for (const agent of AGENTS) {
|
|
345
|
-
tomlContent = tomlContent.replace(
|
|
642
|
+
tomlContent = tomlContent.replace(new RegExp(`\\{\\{${agent}_cwd\\}\\}`, "g"), setup.worktrees[agent]);
|
|
346
643
|
}
|
|
347
644
|
// Replace placeholders
|
|
348
645
|
tomlContent = tomlContent.replace(/\{\{project_name\}\}/g, setup.projectName);
|
|
@@ -356,6 +653,27 @@ function writeAgentChattrConfig(setup, configTomlPath, { skipInstall = false } =
|
|
|
356
653
|
);
|
|
357
654
|
}
|
|
358
655
|
|
|
656
|
+
// Per-project: isolated data dir and port
|
|
657
|
+
const dataDir = path.join(path.dirname(configTomlPath), "data");
|
|
658
|
+
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
659
|
+
// Read assigned port from config (set by writeQuadWorkConfig)
|
|
660
|
+
const existingConfig = readConfig();
|
|
661
|
+
const existingProject = existingConfig.projects?.find((p) => p.id === setup.projectName);
|
|
662
|
+
const chattrPort = existingProject?.agentchattr_url
|
|
663
|
+
? new URL(existingProject.agentchattr_url).port
|
|
664
|
+
: "8300";
|
|
665
|
+
const mcpHttp = existingProject?.mcp_http_port || 8200;
|
|
666
|
+
const mcpSse = existingProject?.mcp_sse_port || 8201;
|
|
667
|
+
tomlContent = tomlContent.replace(/^port = \d+/m, `port = ${chattrPort}`);
|
|
668
|
+
tomlContent = tomlContent.replace(/^data_dir = .+/m, `data_dir = "${dataDir}"`);
|
|
669
|
+
// Add session_token to [server] section if project has one
|
|
670
|
+
const sessionToken = existingProject?.agentchattr_token || "";
|
|
671
|
+
if (sessionToken) {
|
|
672
|
+
tomlContent = tomlContent.replace(/^(data_dir = .+)$/m, `$1\nsession_token = "${sessionToken}"`);
|
|
673
|
+
}
|
|
674
|
+
tomlContent = tomlContent.replace(/^http_port = \d+/m, `http_port = ${mcpHttp}`);
|
|
675
|
+
tomlContent = tomlContent.replace(/^sse_port = \d+/m, `sse_port = ${mcpSse}`);
|
|
676
|
+
|
|
359
677
|
// Write config.toml
|
|
360
678
|
const configDir = path.dirname(configTomlPath);
|
|
361
679
|
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -366,14 +684,14 @@ function writeAgentChattrConfig(setup, configTomlPath, { skipInstall = false } =
|
|
|
366
684
|
let acAvailable = which("agentchattr");
|
|
367
685
|
if (!acAvailable && !skipInstall) {
|
|
368
686
|
const acSpinner = spinner("Installing AgentChattr...");
|
|
369
|
-
const installResult = run("
|
|
687
|
+
const installResult = run("pipx install agentchattr 2>&1");
|
|
370
688
|
if (installResult !== null) {
|
|
371
689
|
acSpinner.stop(true);
|
|
372
690
|
acAvailable = which("agentchattr");
|
|
373
691
|
if (!acAvailable) warn("agentchattr binary not found in PATH after install");
|
|
374
692
|
} else {
|
|
375
693
|
acSpinner.stop(false);
|
|
376
|
-
warn("Install manually:
|
|
694
|
+
warn("Install manually: pipx install agentchattr");
|
|
377
695
|
}
|
|
378
696
|
}
|
|
379
697
|
|
|
@@ -390,8 +708,9 @@ function writeAgentChattrConfig(setup, configTomlPath, { skipInstall = false } =
|
|
|
390
708
|
acProc.unref();
|
|
391
709
|
if (acProc.pid) {
|
|
392
710
|
ok(`AgentChattr started (PID: ${acProc.pid})`);
|
|
393
|
-
|
|
711
|
+
// Per-project PID file
|
|
394
712
|
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
713
|
+
const pidFile = path.join(CONFIG_DIR, `agentchattr-${setup.projectName}.pid`);
|
|
395
714
|
fs.writeFileSync(pidFile, String(acProc.pid));
|
|
396
715
|
} else {
|
|
397
716
|
warn("Could not start AgentChattr — start manually: agentchattr --config " + configTomlPath);
|
|
@@ -464,12 +783,17 @@ async function setupAddons(rl, setup, configTomlPath) {
|
|
|
464
783
|
bridge_dir: telegramDir,
|
|
465
784
|
};
|
|
466
785
|
|
|
786
|
+
// Resolve per-project AgentChattr URL
|
|
787
|
+
const projectCfg = readConfig();
|
|
788
|
+
const projectEntry = projectCfg.projects?.find((p) => p.id === setup.projectName);
|
|
789
|
+
const projectChattrUrl = projectEntry?.agentchattr_url || "http://127.0.0.1:8300";
|
|
790
|
+
|
|
467
791
|
// Append telegram section to config.toml (token read from env at runtime)
|
|
468
792
|
const telegramSection = `
|
|
469
793
|
[telegram]
|
|
470
794
|
bot_token = "env:${envKey}"
|
|
471
795
|
chat_id = "${chatId}"
|
|
472
|
-
agentchattr_url = "
|
|
796
|
+
agentchattr_url = "${projectChattrUrl}"
|
|
473
797
|
poll_interval = 2
|
|
474
798
|
bridge_sender = "telegram-bridge"
|
|
475
799
|
`;
|
|
@@ -481,7 +805,7 @@ bridge_sender = "telegram-bridge"
|
|
|
481
805
|
if (fs.existsSync(bridgeScript)) {
|
|
482
806
|
log("Starting Telegram bridge...");
|
|
483
807
|
const bridgeToml = path.join(CONFIG_DIR, `telegram-${setup.projectName}.toml`);
|
|
484
|
-
const bridgeTomlContent = `[telegram]\nbot_token = "${botToken}"\nchat_id = "${chatId}"\n\n[agentchattr]\nurl = "
|
|
808
|
+
const bridgeTomlContent = `[telegram]\nbot_token = "${botToken}"\nchat_id = "${chatId}"\n\n[agentchattr]\nurl = "${projectChattrUrl}"\n`;
|
|
485
809
|
fs.writeFileSync(bridgeToml, bridgeTomlContent, { mode: 0o600 });
|
|
486
810
|
fs.chmodSync(bridgeToml, 0o600);
|
|
487
811
|
const bridgeProc = spawn("python3", [bridgeScript, "--config", bridgeToml], {
|
|
@@ -586,9 +910,25 @@ function writeQuadWorkConfig(setup) {
|
|
|
586
910
|
};
|
|
587
911
|
}
|
|
588
912
|
|
|
913
|
+
// Auto-assign per-project AgentChattr and MCP ports (scan existing to avoid collisions)
|
|
914
|
+
const existingIdx = config.projects.findIndex((p) => p.id === setup.projectName);
|
|
915
|
+
const usedChattrPorts = new Set(config.projects.map((p) => {
|
|
916
|
+
try { return parseInt(new URL(p.agentchattr_url).port, 10); } catch { return 0; }
|
|
917
|
+
}).filter(Boolean));
|
|
918
|
+
const usedMcpPorts = new Set(config.projects.flatMap((p) => [p.mcp_http_port, p.mcp_sse_port]).filter(Boolean));
|
|
919
|
+
let chattrPort = 8300;
|
|
920
|
+
while (usedChattrPorts.has(chattrPort)) chattrPort++;
|
|
921
|
+
let mcp_http = 8200;
|
|
922
|
+
while (usedMcpPorts.has(mcp_http)) mcp_http++;
|
|
923
|
+
let mcp_sse = mcp_http + 1;
|
|
924
|
+
while (usedMcpPorts.has(mcp_sse)) mcp_sse++;
|
|
925
|
+
project.agentchattr_url = `http://127.0.0.1:${chattrPort}`;
|
|
926
|
+
project.agentchattr_token = require("crypto").randomBytes(16).toString("hex");
|
|
927
|
+
project.mcp_http_port = mcp_http;
|
|
928
|
+
project.mcp_sse_port = mcp_sse;
|
|
929
|
+
|
|
589
930
|
// Upsert project
|
|
590
|
-
|
|
591
|
-
if (idx >= 0) config.projects[idx] = project;
|
|
931
|
+
if (existingIdx >= 0) config.projects[existingIdx] = project;
|
|
592
932
|
else config.projects.push(project);
|
|
593
933
|
|
|
594
934
|
writeConfig(config);
|
|
@@ -601,53 +941,102 @@ async function cmdInit() {
|
|
|
601
941
|
console.log("");
|
|
602
942
|
console.log(` ${c.cyan}${c.bold}╔══════════════════════════════════════════╗${c.reset}`);
|
|
603
943
|
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.white}${c.bold}QuadWork Init${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
604
|
-
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.dim}
|
|
944
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.dim}Global setup — projects via web UI${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
605
945
|
console.log(` ${c.cyan}${c.bold}╚══════════════════════════════════════════╝${c.reset}`);
|
|
606
|
-
console.log(`\n ${c.dim}
|
|
946
|
+
console.log(`\n ${c.dim}Press Enter to accept defaults. Takes under 30 seconds.${c.reset}\n`);
|
|
607
947
|
|
|
608
948
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
609
949
|
|
|
610
950
|
try {
|
|
611
|
-
// Step 1: Prerequisites
|
|
612
|
-
const prereqsOk = checkPrereqs();
|
|
951
|
+
// Step 1: Prerequisites (header printed by checkPrereqs)
|
|
952
|
+
const prereqsOk = await checkPrereqs(rl);
|
|
613
953
|
if (!prereqsOk) {
|
|
614
954
|
const proceed = await askYN(rl, "Some prerequisites missing. Continue anyway?", false);
|
|
615
955
|
if (!proceed) { rl.close(); process.exit(1); }
|
|
616
956
|
}
|
|
617
957
|
|
|
618
|
-
// Step 2:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
958
|
+
// Step 2: Dashboard port
|
|
959
|
+
header("Step 2: Dashboard Port");
|
|
960
|
+
const port = await ask(rl, "Port for the QuadWork dashboard (Enter for default)", "8400");
|
|
961
|
+
|
|
962
|
+
// Write global config
|
|
963
|
+
const config = readConfig();
|
|
964
|
+
config.port = parseInt(port, 10) || 8400;
|
|
965
|
+
writeConfig(config);
|
|
966
|
+
ok(`Wrote ${CONFIG_PATH}`);
|
|
967
|
+
|
|
968
|
+
// Step 3: Start server
|
|
969
|
+
header("Step 3: Starting Dashboard");
|
|
970
|
+
const quadworkDir = path.join(__dirname, "..");
|
|
971
|
+
const serverDir = path.join(quadworkDir, "server");
|
|
972
|
+
let serverPid = null;
|
|
973
|
+
if (fs.existsSync(path.join(serverDir, "index.js"))) {
|
|
974
|
+
const server = spawn("node", [serverDir], {
|
|
975
|
+
stdio: "ignore",
|
|
976
|
+
detached: true,
|
|
977
|
+
env: { ...process.env },
|
|
978
|
+
});
|
|
979
|
+
server.unref();
|
|
980
|
+
if (server.pid) {
|
|
981
|
+
serverPid = server.pid;
|
|
982
|
+
ok(`Server started (PID: ${serverPid})`);
|
|
983
|
+
const pidFile = path.join(CONFIG_DIR, "server.pid");
|
|
984
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
985
|
+
fs.writeFileSync(pidFile, String(serverPid));
|
|
986
|
+
}
|
|
987
|
+
} else {
|
|
988
|
+
warn("Server not found — run from the quadwork directory");
|
|
989
|
+
}
|
|
635
990
|
|
|
636
|
-
// Done
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
log(
|
|
641
|
-
log(`
|
|
642
|
-
log(`
|
|
643
|
-
log(`
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
log(
|
|
647
|
-
log(
|
|
648
|
-
log(
|
|
649
|
-
log(
|
|
650
|
-
log(
|
|
991
|
+
// Done — celebratory welcome
|
|
992
|
+
const dashPort = parseInt(port, 10) || 8400;
|
|
993
|
+
const dashboardUrl = `http://127.0.0.1:${dashPort}`;
|
|
994
|
+
|
|
995
|
+
console.log("");
|
|
996
|
+
console.log(` ${c.cyan}${c.bold}╔══════════════════════════════════════════════════════════╗${c.reset}`);
|
|
997
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
998
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}${c.bold}Welcome to QuadWork!${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
999
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
1000
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} Your AI-powered dev team is ready to ship. ${c.cyan}${c.bold}║${c.reset}`);
|
|
1001
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
1002
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}*${c.reset} ${c.bold}Head${c.reset} ${c.dim}— coordinates & merges${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
1003
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}*${c.reset} ${c.bold}Dev${c.reset} ${c.dim}— writes all the code${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
1004
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}*${c.reset} ${c.bold}Reviewer1${c.reset} ${c.dim}— independent code review${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
1005
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}*${c.reset} ${c.bold}Reviewer2${c.reset} ${c.dim}— independent code review${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
1006
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
1007
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} 4 agents. Full GitHub workflow. Runs while you sleep. ${c.cyan}${c.bold}║${c.reset}`);
|
|
1008
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
1009
|
+
console.log(` ${c.cyan}${c.bold}╚══════════════════════════════════════════════════════════╝${c.reset}`);
|
|
1010
|
+
console.log("");
|
|
1011
|
+
if (serverPid) {
|
|
1012
|
+
console.log(` ${c.green}*${c.reset} Server running at ${c.cyan}${dashboardUrl}${c.reset} ${c.dim}(PID: ${serverPid})${c.reset}`);
|
|
1013
|
+
} else {
|
|
1014
|
+
console.log(` ${c.yellow}*${c.reset} Server not started — run ${c.dim}npx quadwork start${c.reset} to launch`);
|
|
1015
|
+
}
|
|
1016
|
+
console.log(` ${c.green}*${c.reset} Config saved to ${c.dim}${CONFIG_PATH}${c.reset}`);
|
|
1017
|
+
console.log("");
|
|
1018
|
+
console.log(` ${c.cyan}${c.bold}--- Create Your First Project ---${c.reset}`);
|
|
1019
|
+
console.log("");
|
|
1020
|
+
console.log(` Your browser is opening now. If not, visit:`);
|
|
1021
|
+
console.log("");
|
|
1022
|
+
console.log(` ${c.cyan}${c.bold}${dashboardUrl}/setup${c.reset}`);
|
|
1023
|
+
console.log("");
|
|
1024
|
+
console.log(` ${c.dim}1.${c.reset} Connect a GitHub repo`);
|
|
1025
|
+
console.log(` ${c.dim}2.${c.reset} Pick models for each agent`);
|
|
1026
|
+
console.log(` ${c.dim}3.${c.reset} Hit Start — your team takes it from there`);
|
|
1027
|
+
console.log("");
|
|
1028
|
+
console.log(` ${c.dim}Commands:${c.reset}`);
|
|
1029
|
+
console.log(` ${c.dim}npx quadwork start${c.reset} — restart everything`);
|
|
1030
|
+
console.log(` ${c.dim}npx quadwork stop${c.reset} — shut it all down`);
|
|
1031
|
+
console.log("");
|
|
1032
|
+
console.log(` ${c.green}${c.bold}Happy shipping!${c.reset}`);
|
|
1033
|
+
console.log("");
|
|
1034
|
+
|
|
1035
|
+
// Open browser
|
|
1036
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1037
|
+
setTimeout(() => {
|
|
1038
|
+
try { execSync(`${openCmd} ${dashboardUrl}/setup`, { stdio: "ignore" }); } catch {}
|
|
1039
|
+
}, 1500);
|
|
651
1040
|
|
|
652
1041
|
rl.close();
|
|
653
1042
|
} catch (err) {
|
|
@@ -698,11 +1087,12 @@ function cmdStart() {
|
|
|
698
1087
|
const pidFile = path.join(CONFIG_DIR, "server.pid");
|
|
699
1088
|
fs.writeFileSync(pidFile, String(server.pid));
|
|
700
1089
|
|
|
701
|
-
// Start AgentChattr
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1090
|
+
// Start AgentChattr for each project that has a config.toml
|
|
1091
|
+
if (which("agentchattr")) {
|
|
1092
|
+
for (const project of config.projects) {
|
|
1093
|
+
if (!project.working_dir) continue;
|
|
1094
|
+
const configToml = path.join(project.working_dir, "agentchattr", "config.toml");
|
|
1095
|
+
if (!fs.existsSync(configToml)) continue;
|
|
706
1096
|
const acProc = spawn("agentchattr", ["--config", configToml], {
|
|
707
1097
|
stdio: "ignore",
|
|
708
1098
|
detached: true,
|
|
@@ -710,8 +1100,8 @@ function cmdStart() {
|
|
|
710
1100
|
acProc.on("error", () => {});
|
|
711
1101
|
acProc.unref();
|
|
712
1102
|
if (acProc.pid) {
|
|
713
|
-
ok(`AgentChattr started (PID: ${acProc.pid})`);
|
|
714
|
-
fs.writeFileSync(path.join(CONFIG_DIR,
|
|
1103
|
+
ok(`AgentChattr started for ${project.id} (PID: ${acProc.pid})`);
|
|
1104
|
+
fs.writeFileSync(path.join(CONFIG_DIR, `agentchattr-${project.id}.pid`), String(acProc.pid));
|
|
715
1105
|
}
|
|
716
1106
|
}
|
|
717
1107
|
}
|
|
@@ -750,9 +1140,30 @@ function cmdStop() {
|
|
|
750
1140
|
|
|
751
1141
|
let stopped = 0;
|
|
752
1142
|
if (stopPid("Telegram bridge", "telegram-bridge.pid")) stopped++;
|
|
1143
|
+
|
|
1144
|
+
// Stop per-project AgentChattr instances
|
|
1145
|
+
const config = readConfig();
|
|
1146
|
+
for (const project of (config.projects || [])) {
|
|
1147
|
+
if (stopPid(`AgentChattr (${project.id})`, `agentchattr-${project.id}.pid`)) stopped++;
|
|
1148
|
+
}
|
|
1149
|
+
// Also stop legacy single-instance PID if present
|
|
753
1150
|
if (stopPid("AgentChattr", "agentchattr.pid")) stopped++;
|
|
1151
|
+
|
|
754
1152
|
if (stopPid("Server", "server.pid")) stopped++;
|
|
755
1153
|
|
|
1154
|
+
// Stop caffeinate via the running server's API (targets only QuadWork's instance)
|
|
1155
|
+
if (process.platform === "darwin") {
|
|
1156
|
+
const cfg = readConfig();
|
|
1157
|
+
const qwPort = cfg.port || 8400;
|
|
1158
|
+
try {
|
|
1159
|
+
const result = run(`curl -s -X POST http://127.0.0.1:${qwPort}/api/caffeinate/stop 2>/dev/null`);
|
|
1160
|
+
if (result && result.includes('"ok":true')) {
|
|
1161
|
+
ok("Stopped caffeinate (sleep prevention)");
|
|
1162
|
+
stopped++;
|
|
1163
|
+
}
|
|
1164
|
+
} catch {}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
756
1167
|
if (stopped === 0) warn("No running processes found");
|
|
757
1168
|
else ok(`Stopped ${stopped} process(es)`);
|
|
758
1169
|
log("");
|
|
@@ -772,11 +1183,11 @@ async function cmdAddProject() {
|
|
|
772
1183
|
const setup = await setupAgents(rl, repo);
|
|
773
1184
|
if (!setup) { rl.close(); process.exit(1); }
|
|
774
1185
|
|
|
775
|
-
const configTomlPath = path.join(setup.absDir, "config.toml");
|
|
776
|
-
writeAgentChattrConfig(setup, configTomlPath);
|
|
777
|
-
|
|
778
1186
|
writeQuadWorkConfig(setup);
|
|
779
1187
|
|
|
1188
|
+
const configTomlPath = path.join(setup.absDir, "agentchattr", "config.toml");
|
|
1189
|
+
writeAgentChattrConfig(setup, configTomlPath);
|
|
1190
|
+
|
|
780
1191
|
header("Project Added");
|
|
781
1192
|
log(`Project: ${setup.projectName}`);
|
|
782
1193
|
log(`Repo: ${setup.repo}`);
|
|
@@ -813,16 +1224,20 @@ switch (command) {
|
|
|
813
1224
|
Usage: quadwork <command>
|
|
814
1225
|
|
|
815
1226
|
Commands:
|
|
816
|
-
init
|
|
1227
|
+
init Global setup (prereqs, port, backend) — then open web UI
|
|
817
1228
|
start Start the QuadWork dashboard and backend
|
|
818
1229
|
stop Stop all QuadWork processes
|
|
819
|
-
add-project Add a project to
|
|
1230
|
+
add-project Add a project via CLI (alternative to web UI /setup)
|
|
1231
|
+
|
|
1232
|
+
Workflow:
|
|
1233
|
+
1. npx quadwork init — one-time global setup, opens dashboard
|
|
1234
|
+
2. Open /setup in browser — create projects with guided web UI
|
|
1235
|
+
3. npx quadwork stop — stop everything when done
|
|
820
1236
|
|
|
821
1237
|
Examples:
|
|
822
1238
|
npx quadwork init
|
|
823
1239
|
npx quadwork start
|
|
824
1240
|
npx quadwork stop
|
|
825
|
-
npx quadwork add-project
|
|
826
1241
|
`);
|
|
827
1242
|
if (command) process.exit(1);
|
|
828
1243
|
}
|