quadwork 0.1.1 → 0.1.3
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 +4 -4
- package/bin/quadwork.js +285 -94
- 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/02ul7y114vj2f.js +13 -0
- package/out/_next/static/chunks/{0jsosmtclw5n5.js → 038g944ax83al.js} +1 -1
- package/out/_next/static/chunks/0gy_9ugdx7ueh.js +1 -0
- package/out/_next/static/chunks/0idtc5k0469of.js +1 -0
- package/out/_next/static/chunks/{03hi.hdp6l230.js → 0wda-2lcle8c4.js} +8 -8
- package/out/_next/static/chunks/0yxmvmvm1dx_d.css +2 -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/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/project/_/__next._full.txt +3 -3
- package/out/project/_/__next._head.txt +1 -1
- package/out/project/_/__next._index.txt +2 -2
- package/out/project/_/__next._tree.txt +2 -2
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +2 -2
- 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 -3
- 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 +42 -2
- package/server/index.js +103 -55
- package/server/routes.js +104 -66
- 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/{t2b.AGENTS.md → reviewer1.AGENTS.md} +16 -16
- package/templates/seeds/{t2a.AGENTS.md → reviewer2.AGENTS.md} +16 -16
- 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/15kwal..m9r49.css +0 -2
- package/out/_next/static/chunks/17sk4qv6_d0co.js +0 -1
- /package/out/_next/static/{Swn5YST3ZJXv1zBHSCNFU → 91YUiFoMbLQ9sZW4uk45J}/_buildManifest.js +0 -0
- /package/out/_next/static/{Swn5YST3ZJXv1zBHSCNFU → 91YUiFoMbLQ9sZW4uk45J}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{Swn5YST3ZJXv1zBHSCNFU → 91YUiFoMbLQ9sZW4uk45J}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -84,10 +84,10 @@ Config is stored at `~/.quadwork/config.json`:
|
|
|
84
84
|
"repo": "owner/repo",
|
|
85
85
|
"working_dir": "/path/to/project",
|
|
86
86
|
"agents": {
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
87
|
+
"head": { "cwd": "/path/to/project-head", "command": "claude" },
|
|
88
|
+
"reviewer1": { "cwd": "/path/to/project-reviewer1", "command": "claude" },
|
|
89
|
+
"reviewer2": { "cwd": "/path/to/project-reviewer2", "command": "claude" },
|
|
90
|
+
"dev": { "cwd": "/path/to/project-dev", "command": "claude" }
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
]
|
package/bin/quadwork.js
CHANGED
|
@@ -11,15 +11,45 @@ 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 = ["
|
|
15
|
-
|
|
16
|
-
// ─── Helpers
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
const AGENTS = ["head", "reviewer1", "reviewer2", "dev"];
|
|
15
|
+
|
|
16
|
+
// ─── ANSI Helpers ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const isTTY = process.stdout.isTTY;
|
|
19
|
+
const c = isTTY ? {
|
|
20
|
+
reset: "\x1b[0m",
|
|
21
|
+
bold: "\x1b[1m",
|
|
22
|
+
dim: "\x1b[2m",
|
|
23
|
+
green: "\x1b[32m",
|
|
24
|
+
yellow: "\x1b[33m",
|
|
25
|
+
red: "\x1b[31m",
|
|
26
|
+
cyan: "\x1b[36m",
|
|
27
|
+
white: "\x1b[37m",
|
|
28
|
+
} : { reset: "", bold: "", dim: "", green: "", yellow: "", red: "", cyan: "", white: "" };
|
|
29
|
+
|
|
30
|
+
function log(msg) { console.log(` ${c.dim}${msg}${c.reset}`); }
|
|
31
|
+
function ok(msg) { console.log(` ${c.green}✓${c.reset} ${msg}`); }
|
|
32
|
+
function warn(msg) { console.log(` ${c.yellow}⚠ ${msg}${c.reset}`); }
|
|
33
|
+
function fail(msg) { console.error(` ${c.red}✗ ${msg}${c.reset}`); }
|
|
34
|
+
function header(msg) { console.log(`\n ${c.cyan}${c.bold}┌─ ${msg} ${"─".repeat(Math.max(0, 54 - msg.length))}┐${c.reset}\n`); }
|
|
35
|
+
|
|
36
|
+
function spinner(msg) {
|
|
37
|
+
if (!isTTY) {
|
|
38
|
+
console.log(` ${msg}`);
|
|
39
|
+
return { stop(result) { console.log(` ${result ? "✓" : "✗"} ${msg}`); } };
|
|
40
|
+
}
|
|
41
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
42
|
+
let i = 0;
|
|
43
|
+
const id = setInterval(() => {
|
|
44
|
+
process.stdout.write(`\r ${c.cyan}${frames[i++ % frames.length]}${c.reset} ${msg}`);
|
|
45
|
+
}, 80);
|
|
46
|
+
return {
|
|
47
|
+
stop(result) {
|
|
48
|
+
clearInterval(id);
|
|
49
|
+
process.stdout.write(`\r ${result ? `${c.green}✓${c.reset} ${msg}` : `${c.red}✗${c.reset} ${msg}`}${" ".repeat(10)}\n`);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
23
53
|
|
|
24
54
|
function run(cmd, opts = {}) {
|
|
25
55
|
try {
|
|
@@ -35,26 +65,91 @@ function which(cmd) {
|
|
|
35
65
|
|
|
36
66
|
function ask(rl, question, defaultVal) {
|
|
37
67
|
return new Promise((resolve) => {
|
|
38
|
-
const suffix = defaultVal ? `
|
|
39
|
-
rl.question(` ${question}${suffix}
|
|
68
|
+
const suffix = defaultVal ? ` ${c.dim}[${defaultVal}]${c.reset}` : "";
|
|
69
|
+
rl.question(` ${c.bold}${question}${c.reset}${suffix}${c.cyan} > ${c.reset}`, (answer) => {
|
|
40
70
|
resolve(answer.trim() || defaultVal || "");
|
|
41
71
|
});
|
|
42
72
|
});
|
|
43
73
|
}
|
|
44
74
|
|
|
75
|
+
function askSecret(rl, question) {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
const stdin = process.stdin;
|
|
78
|
+
const stdout = process.stdout;
|
|
79
|
+
stdout.write(` ${c.bold}${question}${c.reset}${c.cyan} > ${c.reset}`);
|
|
80
|
+
let secret = "";
|
|
81
|
+
const wasRaw = stdin.isRaw;
|
|
82
|
+
stdin.setRawMode(true);
|
|
83
|
+
stdin.resume();
|
|
84
|
+
const onData = (ch) => {
|
|
85
|
+
// Iterate per character to handle pasted multi-char input
|
|
86
|
+
const str = ch.toString("utf-8");
|
|
87
|
+
for (const c of str) {
|
|
88
|
+
if (c === "\n" || c === "\r") {
|
|
89
|
+
stdin.setRawMode(wasRaw || false);
|
|
90
|
+
stdin.removeListener("data", onData);
|
|
91
|
+
stdout.write("\n");
|
|
92
|
+
resolve(secret);
|
|
93
|
+
return;
|
|
94
|
+
} else if (c === "\u007F" || c === "\b") {
|
|
95
|
+
if (secret.length > 0) {
|
|
96
|
+
secret = secret.slice(0, -1);
|
|
97
|
+
stdout.write("\b \b");
|
|
98
|
+
}
|
|
99
|
+
} else if (c === "\u0003") {
|
|
100
|
+
process.exit(1);
|
|
101
|
+
} else if (c >= " ") {
|
|
102
|
+
secret += c;
|
|
103
|
+
stdout.write("*");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
stdin.on("data", onData);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function maskValue(val) {
|
|
112
|
+
if (!val || val.length < 8) return "****";
|
|
113
|
+
return val.slice(0, 4) + "***" + val.slice(-3);
|
|
114
|
+
}
|
|
115
|
+
|
|
45
116
|
function askYN(rl, question, defaultYes = false) {
|
|
46
117
|
return new Promise((resolve) => {
|
|
47
118
|
const hint = defaultYes ? "Y/n" : "y/N";
|
|
48
|
-
rl.question(` ${question} [${hint}]
|
|
119
|
+
rl.question(` ${c.bold}${question}${c.reset} ${c.dim}[${hint}]${c.reset}${c.cyan} > ${c.reset}`, (answer) => {
|
|
49
120
|
const a = answer.trim().toLowerCase();
|
|
50
121
|
resolve(a === "" ? defaultYes : a === "y" || a === "yes");
|
|
51
122
|
});
|
|
52
123
|
});
|
|
53
124
|
}
|
|
54
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
|
+
|
|
55
149
|
function readConfig() {
|
|
56
150
|
try {
|
|
57
|
-
|
|
151
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
152
|
+
return migrateAgentKeys(config);
|
|
58
153
|
} catch {
|
|
59
154
|
return { port: 8400, agentchattr_url: "http://127.0.0.1:8300", projects: [] };
|
|
60
155
|
}
|
|
@@ -67,6 +162,8 @@ function writeConfig(config) {
|
|
|
67
162
|
|
|
68
163
|
// ─── Prerequisites ──────────────────────────────────────────────────────────
|
|
69
164
|
|
|
165
|
+
let agentChattrFound = false;
|
|
166
|
+
|
|
70
167
|
function checkPrereqs() {
|
|
71
168
|
header("Step 1: Prerequisites");
|
|
72
169
|
let allOk = true;
|
|
@@ -90,7 +187,7 @@ function checkPrereqs() {
|
|
|
90
187
|
|
|
91
188
|
// AgentChattr
|
|
92
189
|
const acVer = run("agentchattr --version") || run("python3 -m agentchattr --version");
|
|
93
|
-
if (acVer) ok(`AgentChattr ${acVer}`);
|
|
190
|
+
if (acVer) { ok(`AgentChattr ${acVer}`); agentChattrFound = true; }
|
|
94
191
|
else { warn("AgentChattr not found — install: pip install agentchattr"); allOk = false; }
|
|
95
192
|
|
|
96
193
|
// gh CLI
|
|
@@ -132,10 +229,12 @@ async function setupGitHub(rl) {
|
|
|
132
229
|
}
|
|
133
230
|
|
|
134
231
|
// Verify repo exists
|
|
232
|
+
const sp = spinner(`Verifying ${repo}...`);
|
|
135
233
|
const repoCheck = run(`gh repo view ${repo} --json name 2>&1`);
|
|
136
234
|
if (repoCheck && repoCheck.includes('"name"')) {
|
|
137
|
-
|
|
235
|
+
sp.stop(true);
|
|
138
236
|
} else {
|
|
237
|
+
sp.stop(false);
|
|
139
238
|
fail(`Cannot access ${repo} — check permissions`);
|
|
140
239
|
return null;
|
|
141
240
|
}
|
|
@@ -171,7 +270,8 @@ async function setupAgents(rl, repo) {
|
|
|
171
270
|
}
|
|
172
271
|
}
|
|
173
272
|
|
|
174
|
-
log("Path to your local clone of the repo.
|
|
273
|
+
log("Path to your local clone of the repo. Four worktrees will be created next to it");
|
|
274
|
+
log("(e.g., project-head/, project-reviewer1/, project-reviewer2/, project-dev/).");
|
|
175
275
|
const projectDir = await ask(rl, "Project directory", process.cwd());
|
|
176
276
|
const absDir = path.resolve(projectDir);
|
|
177
277
|
|
|
@@ -187,12 +287,12 @@ async function setupAgents(rl, repo) {
|
|
|
187
287
|
}
|
|
188
288
|
|
|
189
289
|
// Prompt for reviewer credentials (optional)
|
|
190
|
-
log("A separate reviewer account lets
|
|
191
|
-
const wantReviewer = await askYN(rl, "Use a separate GitHub account for reviewers (
|
|
290
|
+
log("A separate reviewer account lets Reviewer1/Reviewer2 approve PRs independently. You can set this up later in Settings.");
|
|
291
|
+
const wantReviewer = await askYN(rl, "Use a separate GitHub account for reviewers (Reviewer1/Reviewer2)?", false);
|
|
192
292
|
let reviewerUser = "";
|
|
193
293
|
let reviewerTokenPath = "";
|
|
194
294
|
if (wantReviewer) {
|
|
195
|
-
log("GitHub username for the reviewer account (used in
|
|
295
|
+
log("GitHub username for the reviewer account (used in Reviewer1/Reviewer2 seed files for PR reviews).");
|
|
196
296
|
reviewerUser = await ask(rl, "Reviewer GitHub username", "");
|
|
197
297
|
log("Path to a file containing a GitHub PAT for the reviewer account.");
|
|
198
298
|
reviewerTokenPath = await ask(rl, "Reviewer token file path", path.join(os.homedir(), ".quadwork", "reviewer-token"));
|
|
@@ -200,25 +300,19 @@ async function setupAgents(rl, repo) {
|
|
|
200
300
|
|
|
201
301
|
const projectName = path.basename(absDir);
|
|
202
302
|
log(`Project: ${projectName}`);
|
|
203
|
-
|
|
303
|
+
const wtSpinner = spinner("Creating worktrees and seeding files...");
|
|
204
304
|
|
|
205
305
|
const worktrees = {};
|
|
306
|
+
let wtFailed = null;
|
|
206
307
|
for (const agent of AGENTS) {
|
|
207
308
|
const wtDir = path.join(path.dirname(absDir), `${projectName}-${agent}`);
|
|
208
|
-
if (fs.existsSync(wtDir)) {
|
|
209
|
-
ok(`Worktree exists: ${agent} → ${wtDir}`);
|
|
210
|
-
} else {
|
|
309
|
+
if (!fs.existsSync(wtDir)) {
|
|
211
310
|
const branchName = `worktree-${agent}`;
|
|
212
|
-
// Create branch if needed
|
|
213
311
|
run(`git -C "${absDir}" branch ${branchName} HEAD 2>&1`);
|
|
214
312
|
const result = run(`git -C "${absDir}" worktree add "${wtDir}" ${branchName} 2>&1`);
|
|
215
|
-
if (result
|
|
216
|
-
ok(`Created worktree: ${agent} → ${wtDir}`);
|
|
217
|
-
} else {
|
|
218
|
-
// Try without branch (detached)
|
|
313
|
+
if (!result) {
|
|
219
314
|
const result2 = run(`git -C "${absDir}" worktree add --detach "${wtDir}" HEAD 2>&1`);
|
|
220
|
-
if (result2
|
|
221
|
-
else { fail(`Failed to create worktree for ${agent}`); return null; }
|
|
315
|
+
if (!result2) { wtFailed = agent; break; }
|
|
222
316
|
}
|
|
223
317
|
}
|
|
224
318
|
worktrees[agent] = wtDir;
|
|
@@ -238,10 +332,15 @@ async function setupAgents(rl, repo) {
|
|
|
238
332
|
seedContent = seedContent.replace(/\{\{reviewer_token_path\}\}/g, "");
|
|
239
333
|
}
|
|
240
334
|
fs.writeFileSync(seedDst, seedContent);
|
|
241
|
-
log(` Copied ${agent}.AGENTS.md`);
|
|
242
335
|
}
|
|
243
336
|
}
|
|
244
337
|
|
|
338
|
+
if (wtFailed) {
|
|
339
|
+
wtSpinner.stop(false);
|
|
340
|
+
fail(`Failed to create worktree for ${wtFailed}`);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
245
344
|
// Copy CLAUDE.md to each worktree
|
|
246
345
|
const claudeSrc = path.join(TEMPLATES_DIR, "CLAUDE.md");
|
|
247
346
|
if (fs.existsSync(claudeSrc)) {
|
|
@@ -249,25 +348,25 @@ async function setupAgents(rl, repo) {
|
|
|
249
348
|
claudeContent = claudeContent.replace(/\{\{project_name\}\}/g, projectName);
|
|
250
349
|
for (const agent of AGENTS) {
|
|
251
350
|
const dst = path.join(worktrees[agent], "CLAUDE.md");
|
|
252
|
-
// Don't overwrite if CLAUDE.md already exists
|
|
253
351
|
if (!fs.existsSync(dst)) {
|
|
254
352
|
fs.writeFileSync(dst, claudeContent);
|
|
255
353
|
}
|
|
256
354
|
}
|
|
257
|
-
ok("Copied CLAUDE.md to all worktrees");
|
|
258
355
|
}
|
|
259
356
|
|
|
357
|
+
wtSpinner.stop(true);
|
|
358
|
+
|
|
260
359
|
return { projectName, absDir, worktrees, repo, backend, backends };
|
|
261
360
|
}
|
|
262
361
|
|
|
263
362
|
// ─── AgentChattr Config ─────────────────────────────────────────────────────
|
|
264
363
|
|
|
265
|
-
function writeAgentChattrConfig(setup, configTomlPath) {
|
|
364
|
+
function writeAgentChattrConfig(setup, configTomlPath, { skipInstall = false } = {}) {
|
|
266
365
|
header("Step 4: AgentChattr Setup");
|
|
267
366
|
|
|
268
367
|
let tomlContent = fs.readFileSync(path.join(TEMPLATES_DIR, "config.toml"), "utf-8");
|
|
269
368
|
for (const agent of AGENTS) {
|
|
270
|
-
tomlContent = tomlContent.replace(
|
|
369
|
+
tomlContent = tomlContent.replace(new RegExp(`\\{\\{${agent}_cwd\\}\\}`, "g"), setup.worktrees[agent]);
|
|
271
370
|
}
|
|
272
371
|
// Replace placeholders
|
|
273
372
|
tomlContent = tomlContent.replace(/\{\{project_name\}\}/g, setup.projectName);
|
|
@@ -281,25 +380,45 @@ function writeAgentChattrConfig(setup, configTomlPath) {
|
|
|
281
380
|
);
|
|
282
381
|
}
|
|
283
382
|
|
|
383
|
+
// Per-project: isolated data dir and port
|
|
384
|
+
const dataDir = path.join(path.dirname(configTomlPath), "data");
|
|
385
|
+
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
386
|
+
// Read assigned port from config (set by writeQuadWorkConfig)
|
|
387
|
+
const existingConfig = readConfig();
|
|
388
|
+
const existingProject = existingConfig.projects?.find((p) => p.id === setup.projectName);
|
|
389
|
+
const chattrPort = existingProject?.agentchattr_url
|
|
390
|
+
? new URL(existingProject.agentchattr_url).port
|
|
391
|
+
: "8300";
|
|
392
|
+
const mcpHttp = existingProject?.mcp_http_port || 8200;
|
|
393
|
+
const mcpSse = existingProject?.mcp_sse_port || 8201;
|
|
394
|
+
tomlContent = tomlContent.replace(/^port = \d+/m, `port = ${chattrPort}`);
|
|
395
|
+
tomlContent = tomlContent.replace(/^data_dir = .+/m, `data_dir = "${dataDir}"`);
|
|
396
|
+
// Add session_token to [server] section if project has one
|
|
397
|
+
const sessionToken = existingProject?.agentchattr_token || "";
|
|
398
|
+
if (sessionToken) {
|
|
399
|
+
tomlContent = tomlContent.replace(/^(data_dir = .+)$/m, `$1\nsession_token = "${sessionToken}"`);
|
|
400
|
+
}
|
|
401
|
+
tomlContent = tomlContent.replace(/^http_port = \d+/m, `http_port = ${mcpHttp}`);
|
|
402
|
+
tomlContent = tomlContent.replace(/^sse_port = \d+/m, `sse_port = ${mcpSse}`);
|
|
403
|
+
|
|
284
404
|
// Write config.toml
|
|
285
405
|
const configDir = path.dirname(configTomlPath);
|
|
286
406
|
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
287
407
|
fs.writeFileSync(configTomlPath, tomlContent);
|
|
288
408
|
ok(`Wrote ${configTomlPath}`);
|
|
289
409
|
|
|
290
|
-
//
|
|
291
|
-
// Only check for the actual binary — python3 -m availability doesn't mean the CLI is in PATH
|
|
410
|
+
// Start AgentChattr if available; optionally skip install attempt
|
|
292
411
|
let acAvailable = which("agentchattr");
|
|
293
|
-
if (!acAvailable) {
|
|
294
|
-
|
|
412
|
+
if (!acAvailable && !skipInstall) {
|
|
413
|
+
const acSpinner = spinner("Installing AgentChattr...");
|
|
295
414
|
const installResult = run("pip install agentchattr 2>&1");
|
|
296
415
|
if (installResult !== null) {
|
|
297
|
-
|
|
298
|
-
// Re-check that the binary is actually in PATH after install
|
|
416
|
+
acSpinner.stop(true);
|
|
299
417
|
acAvailable = which("agentchattr");
|
|
300
418
|
if (!acAvailable) warn("agentchattr binary not found in PATH after install");
|
|
301
419
|
} else {
|
|
302
|
-
|
|
420
|
+
acSpinner.stop(false);
|
|
421
|
+
warn("Install manually: pip install agentchattr");
|
|
303
422
|
}
|
|
304
423
|
}
|
|
305
424
|
|
|
@@ -316,8 +435,9 @@ function writeAgentChattrConfig(setup, configTomlPath) {
|
|
|
316
435
|
acProc.unref();
|
|
317
436
|
if (acProc.pid) {
|
|
318
437
|
ok(`AgentChattr started (PID: ${acProc.pid})`);
|
|
319
|
-
|
|
438
|
+
// Per-project PID file
|
|
320
439
|
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
440
|
+
const pidFile = path.join(CONFIG_DIR, `agentchattr-${setup.projectName}.pid`);
|
|
321
441
|
fs.writeFileSync(pidFile, String(acProc.pid));
|
|
322
442
|
} else {
|
|
323
443
|
warn("Could not start AgentChattr — start manually: agentchattr --config " + configTomlPath);
|
|
@@ -340,10 +460,10 @@ async function setupAddons(rl, setup, configTomlPath) {
|
|
|
340
460
|
if (wantTelegram) {
|
|
341
461
|
const telegramDir = path.join(path.dirname(setup.absDir), "agentchattr-telegram");
|
|
342
462
|
if (!fs.existsSync(telegramDir)) {
|
|
343
|
-
|
|
463
|
+
const cloneSpinner = spinner("Cloning agentchattr-telegram...");
|
|
344
464
|
const cloneResult = run(`git clone https://github.com/realproject7/agentchattr-telegram.git "${telegramDir}" 2>&1`);
|
|
345
|
-
|
|
346
|
-
|
|
465
|
+
cloneSpinner.stop(cloneResult !== null);
|
|
466
|
+
if (!cloneResult) warn("You can set it up manually later");
|
|
347
467
|
} else {
|
|
348
468
|
ok("agentchattr-telegram already present");
|
|
349
469
|
}
|
|
@@ -351,12 +471,20 @@ async function setupAddons(rl, setup, configTomlPath) {
|
|
|
351
471
|
if (fs.existsSync(telegramDir)) {
|
|
352
472
|
const reqFile = path.join(telegramDir, "requirements.txt");
|
|
353
473
|
if (fs.existsSync(reqFile)) {
|
|
354
|
-
|
|
355
|
-
|
|
474
|
+
const tgSpinner = spinner("Installing Telegram Bridge dependencies...");
|
|
475
|
+
const tgResult = run(`pip install -r "${reqFile}" 2>&1`);
|
|
476
|
+
tgSpinner.stop(tgResult !== null);
|
|
356
477
|
}
|
|
357
478
|
|
|
358
|
-
|
|
479
|
+
log("Create a bot via @BotFather on Telegram (https://t.me/BotFather), then copy the token.");
|
|
480
|
+
const botToken = await askSecret(rl, "Telegram bot token");
|
|
481
|
+
log("To find your chat ID:");
|
|
482
|
+
log(" 1. Open your bot on Telegram and send it any message (e.g., 'hi')");
|
|
483
|
+
log(" 2. Run: curl https://api.telegram.org/bot<TOKEN>/getUpdates");
|
|
484
|
+
log(" 3. Look for \"chat\":{\"id\":123456789,...} — the number is your chat ID");
|
|
485
|
+
log(" Note: Returns empty if no messages have been sent to the bot yet.");
|
|
359
486
|
const chatId = await ask(rl, "Telegram chat ID", "");
|
|
487
|
+
log("Need help? See https://github.com/realproject7/agentchattr-telegram#readme");
|
|
360
488
|
|
|
361
489
|
if (botToken && chatId) {
|
|
362
490
|
// Write bot token to ~/.quadwork/.env (never stored in config files)
|
|
@@ -373,7 +501,7 @@ async function setupAddons(rl, setup, configTomlPath) {
|
|
|
373
501
|
}
|
|
374
502
|
fs.writeFileSync(envPath, envContent, { mode: 0o600 });
|
|
375
503
|
fs.chmodSync(envPath, 0o600);
|
|
376
|
-
ok(`Saved bot token to ${envPath}`);
|
|
504
|
+
ok(`Saved bot token (${maskValue(botToken)}) to ${envPath}`);
|
|
377
505
|
|
|
378
506
|
// Persist telegram settings for writeQuadWorkConfig (env reference, not plaintext)
|
|
379
507
|
setup.telegram = {
|
|
@@ -382,12 +510,17 @@ async function setupAddons(rl, setup, configTomlPath) {
|
|
|
382
510
|
bridge_dir: telegramDir,
|
|
383
511
|
};
|
|
384
512
|
|
|
513
|
+
// Resolve per-project AgentChattr URL
|
|
514
|
+
const projectCfg = readConfig();
|
|
515
|
+
const projectEntry = projectCfg.projects?.find((p) => p.id === setup.projectName);
|
|
516
|
+
const projectChattrUrl = projectEntry?.agentchattr_url || "http://127.0.0.1:8300";
|
|
517
|
+
|
|
385
518
|
// Append telegram section to config.toml (token read from env at runtime)
|
|
386
519
|
const telegramSection = `
|
|
387
520
|
[telegram]
|
|
388
521
|
bot_token = "env:${envKey}"
|
|
389
522
|
chat_id = "${chatId}"
|
|
390
|
-
agentchattr_url = "
|
|
523
|
+
agentchattr_url = "${projectChattrUrl}"
|
|
391
524
|
poll_interval = 2
|
|
392
525
|
bridge_sender = "telegram-bridge"
|
|
393
526
|
`;
|
|
@@ -399,7 +532,7 @@ bridge_sender = "telegram-bridge"
|
|
|
399
532
|
if (fs.existsSync(bridgeScript)) {
|
|
400
533
|
log("Starting Telegram bridge...");
|
|
401
534
|
const bridgeToml = path.join(CONFIG_DIR, `telegram-${setup.projectName}.toml`);
|
|
402
|
-
const bridgeTomlContent = `[telegram]\nbot_token = "${botToken}"\nchat_id = "${chatId}"\n\n[agentchattr]\nurl = "
|
|
535
|
+
const bridgeTomlContent = `[telegram]\nbot_token = "${botToken}"\nchat_id = "${chatId}"\n\n[agentchattr]\nurl = "${projectChattrUrl}"\n`;
|
|
403
536
|
fs.writeFileSync(bridgeToml, bridgeTomlContent, { mode: 0o600 });
|
|
404
537
|
fs.chmodSync(bridgeToml, 0o600);
|
|
405
538
|
const bridgeProc = spawn("python3", [bridgeScript, "--config", bridgeToml], {
|
|
@@ -425,10 +558,10 @@ bridge_sender = "telegram-bridge"
|
|
|
425
558
|
if (wantMemory) {
|
|
426
559
|
const memoryDir = path.join(path.dirname(setup.absDir), "agent-memory");
|
|
427
560
|
if (!fs.existsSync(memoryDir)) {
|
|
428
|
-
|
|
561
|
+
const memSpinner = spinner("Cloning agent-memory...");
|
|
429
562
|
const cloneResult = run(`git clone https://github.com/realproject7/agent-memory.git "${memoryDir}" 2>&1`);
|
|
430
|
-
|
|
431
|
-
|
|
563
|
+
memSpinner.stop(cloneResult !== null);
|
|
564
|
+
if (!cloneResult) warn("You can set it up manually later");
|
|
432
565
|
} else {
|
|
433
566
|
ok("agent-memory already present");
|
|
434
567
|
}
|
|
@@ -504,9 +637,25 @@ function writeQuadWorkConfig(setup) {
|
|
|
504
637
|
};
|
|
505
638
|
}
|
|
506
639
|
|
|
640
|
+
// Auto-assign per-project AgentChattr and MCP ports (scan existing to avoid collisions)
|
|
641
|
+
const existingIdx = config.projects.findIndex((p) => p.id === setup.projectName);
|
|
642
|
+
const usedChattrPorts = new Set(config.projects.map((p) => {
|
|
643
|
+
try { return parseInt(new URL(p.agentchattr_url).port, 10); } catch { return 0; }
|
|
644
|
+
}).filter(Boolean));
|
|
645
|
+
const usedMcpPorts = new Set(config.projects.flatMap((p) => [p.mcp_http_port, p.mcp_sse_port]).filter(Boolean));
|
|
646
|
+
let chattrPort = 8300;
|
|
647
|
+
while (usedChattrPorts.has(chattrPort)) chattrPort++;
|
|
648
|
+
let mcp_http = 8200;
|
|
649
|
+
while (usedMcpPorts.has(mcp_http)) mcp_http++;
|
|
650
|
+
let mcp_sse = mcp_http + 1;
|
|
651
|
+
while (usedMcpPorts.has(mcp_sse)) mcp_sse++;
|
|
652
|
+
project.agentchattr_url = `http://127.0.0.1:${chattrPort}`;
|
|
653
|
+
project.agentchattr_token = require("crypto").randomBytes(16).toString("hex");
|
|
654
|
+
project.mcp_http_port = mcp_http;
|
|
655
|
+
project.mcp_sse_port = mcp_sse;
|
|
656
|
+
|
|
507
657
|
// Upsert project
|
|
508
|
-
|
|
509
|
-
if (idx >= 0) config.projects[idx] = project;
|
|
658
|
+
if (existingIdx >= 0) config.projects[existingIdx] = project;
|
|
510
659
|
else config.projects.push(project);
|
|
511
660
|
|
|
512
661
|
writeConfig(config);
|
|
@@ -516,49 +665,78 @@ function writeQuadWorkConfig(setup) {
|
|
|
516
665
|
// ─── Init Command ───────────────────────────────────────────────────────────
|
|
517
666
|
|
|
518
667
|
async function cmdInit() {
|
|
519
|
-
console.log("
|
|
668
|
+
console.log("");
|
|
669
|
+
console.log(` ${c.cyan}${c.bold}╔══════════════════════════════════════════╗${c.reset}`);
|
|
670
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.white}${c.bold}QuadWork Init${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
671
|
+
console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.dim}Global setup — projects via web UI${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
|
|
672
|
+
console.log(` ${c.cyan}${c.bold}╚══════════════════════════════════════════╝${c.reset}`);
|
|
673
|
+
console.log(`\n ${c.dim}Tip: Press Enter to accept defaults shown in [brackets].${c.reset}\n`);
|
|
520
674
|
|
|
521
675
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
522
676
|
|
|
523
677
|
try {
|
|
524
678
|
// Step 1: Prerequisites
|
|
679
|
+
header("Step 1: Prerequisites");
|
|
525
680
|
const prereqsOk = checkPrereqs();
|
|
526
681
|
if (!prereqsOk) {
|
|
527
682
|
const proceed = await askYN(rl, "Some prerequisites missing. Continue anyway?", false);
|
|
528
683
|
if (!proceed) { rl.close(); process.exit(1); }
|
|
529
684
|
}
|
|
530
685
|
|
|
531
|
-
// Step 2:
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
686
|
+
// Step 2: Global config
|
|
687
|
+
header("Step 2: Global Configuration");
|
|
688
|
+
const port = await ask(rl, "Dashboard port", "8400");
|
|
689
|
+
const defaultBackend = which("claude") ? "claude" : which("codex") ? "codex" : "claude";
|
|
690
|
+
const backend = await ask(rl, "Default CLI backend (claude/codex)", defaultBackend);
|
|
691
|
+
|
|
692
|
+
// Write global config
|
|
693
|
+
const config = readConfig();
|
|
694
|
+
config.port = parseInt(port, 10) || 8400;
|
|
695
|
+
config.default_backend = backend;
|
|
696
|
+
writeConfig(config);
|
|
697
|
+
ok(`Wrote ${CONFIG_PATH}`);
|
|
698
|
+
|
|
699
|
+
// Step 3: Start server
|
|
700
|
+
header("Step 3: Starting Dashboard");
|
|
701
|
+
const quadworkDir = path.join(__dirname, "..");
|
|
702
|
+
const serverDir = path.join(quadworkDir, "server");
|
|
703
|
+
if (fs.existsSync(path.join(serverDir, "index.js"))) {
|
|
704
|
+
const server = spawn("node", [serverDir], {
|
|
705
|
+
stdio: "ignore",
|
|
706
|
+
detached: true,
|
|
707
|
+
env: { ...process.env },
|
|
708
|
+
});
|
|
709
|
+
server.unref();
|
|
710
|
+
if (server.pid) {
|
|
711
|
+
ok(`Server started (PID: ${server.pid})`);
|
|
712
|
+
const pidFile = path.join(CONFIG_DIR, "server.pid");
|
|
713
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
714
|
+
fs.writeFileSync(pidFile, String(server.pid));
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
warn("Server not found — run from the quadwork directory");
|
|
718
|
+
}
|
|
548
719
|
|
|
549
720
|
// Done
|
|
721
|
+
const dashPort = parseInt(port, 10) || 8400;
|
|
722
|
+
const dashboardUrl = `http://127.0.0.1:${dashPort}`;
|
|
723
|
+
|
|
550
724
|
header("Setup Complete");
|
|
551
|
-
log(`
|
|
552
|
-
log(`
|
|
553
|
-
log(`
|
|
554
|
-
log(`Config: ${CONFIG_PATH}`);
|
|
555
|
-
log(`AgentChattr: ${configTomlPath}`);
|
|
725
|
+
log(`Config: ${CONFIG_PATH}`);
|
|
726
|
+
log(`Dashboard: ${dashboardUrl}`);
|
|
727
|
+
log(`Backend: ${backend}`);
|
|
556
728
|
log("");
|
|
557
729
|
log("Next steps:");
|
|
558
|
-
log(
|
|
730
|
+
log(` Open ${c.cyan}${dashboardUrl}/setup${c.reset} to create your first project`);
|
|
559
731
|
log(" npx quadwork stop — stop all processes");
|
|
560
732
|
log("");
|
|
561
733
|
|
|
734
|
+
// Open browser
|
|
735
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
736
|
+
setTimeout(() => {
|
|
737
|
+
try { execSync(`${openCmd} ${dashboardUrl}/setup`, { stdio: "ignore" }); } catch {}
|
|
738
|
+
}, 1500);
|
|
739
|
+
|
|
562
740
|
rl.close();
|
|
563
741
|
} catch (err) {
|
|
564
742
|
fail(err.message);
|
|
@@ -608,11 +786,12 @@ function cmdStart() {
|
|
|
608
786
|
const pidFile = path.join(CONFIG_DIR, "server.pid");
|
|
609
787
|
fs.writeFileSync(pidFile, String(server.pid));
|
|
610
788
|
|
|
611
|
-
// Start AgentChattr
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
789
|
+
// Start AgentChattr for each project that has a config.toml
|
|
790
|
+
if (which("agentchattr")) {
|
|
791
|
+
for (const project of config.projects) {
|
|
792
|
+
if (!project.working_dir) continue;
|
|
793
|
+
const configToml = path.join(project.working_dir, "agentchattr", "config.toml");
|
|
794
|
+
if (!fs.existsSync(configToml)) continue;
|
|
616
795
|
const acProc = spawn("agentchattr", ["--config", configToml], {
|
|
617
796
|
stdio: "ignore",
|
|
618
797
|
detached: true,
|
|
@@ -620,8 +799,8 @@ function cmdStart() {
|
|
|
620
799
|
acProc.on("error", () => {});
|
|
621
800
|
acProc.unref();
|
|
622
801
|
if (acProc.pid) {
|
|
623
|
-
ok(`AgentChattr started (PID: ${acProc.pid})`);
|
|
624
|
-
fs.writeFileSync(path.join(CONFIG_DIR,
|
|
802
|
+
ok(`AgentChattr started for ${project.id} (PID: ${acProc.pid})`);
|
|
803
|
+
fs.writeFileSync(path.join(CONFIG_DIR, `agentchattr-${project.id}.pid`), String(acProc.pid));
|
|
625
804
|
}
|
|
626
805
|
}
|
|
627
806
|
}
|
|
@@ -660,7 +839,15 @@ function cmdStop() {
|
|
|
660
839
|
|
|
661
840
|
let stopped = 0;
|
|
662
841
|
if (stopPid("Telegram bridge", "telegram-bridge.pid")) stopped++;
|
|
842
|
+
|
|
843
|
+
// Stop per-project AgentChattr instances
|
|
844
|
+
const config = readConfig();
|
|
845
|
+
for (const project of (config.projects || [])) {
|
|
846
|
+
if (stopPid(`AgentChattr (${project.id})`, `agentchattr-${project.id}.pid`)) stopped++;
|
|
847
|
+
}
|
|
848
|
+
// Also stop legacy single-instance PID if present
|
|
663
849
|
if (stopPid("AgentChattr", "agentchattr.pid")) stopped++;
|
|
850
|
+
|
|
664
851
|
if (stopPid("Server", "server.pid")) stopped++;
|
|
665
852
|
|
|
666
853
|
if (stopped === 0) warn("No running processes found");
|
|
@@ -682,11 +869,11 @@ async function cmdAddProject() {
|
|
|
682
869
|
const setup = await setupAgents(rl, repo);
|
|
683
870
|
if (!setup) { rl.close(); process.exit(1); }
|
|
684
871
|
|
|
685
|
-
const configTomlPath = path.join(setup.absDir, "config.toml");
|
|
686
|
-
writeAgentChattrConfig(setup, configTomlPath);
|
|
687
|
-
|
|
688
872
|
writeQuadWorkConfig(setup);
|
|
689
873
|
|
|
874
|
+
const configTomlPath = path.join(setup.absDir, "agentchattr", "config.toml");
|
|
875
|
+
writeAgentChattrConfig(setup, configTomlPath);
|
|
876
|
+
|
|
690
877
|
header("Project Added");
|
|
691
878
|
log(`Project: ${setup.projectName}`);
|
|
692
879
|
log(`Repo: ${setup.repo}`);
|
|
@@ -723,16 +910,20 @@ switch (command) {
|
|
|
723
910
|
Usage: quadwork <command>
|
|
724
911
|
|
|
725
912
|
Commands:
|
|
726
|
-
init
|
|
913
|
+
init Global setup (prereqs, port, backend) — then open web UI
|
|
727
914
|
start Start the QuadWork dashboard and backend
|
|
728
915
|
stop Stop all QuadWork processes
|
|
729
|
-
add-project Add a project to
|
|
916
|
+
add-project Add a project via CLI (alternative to web UI /setup)
|
|
917
|
+
|
|
918
|
+
Workflow:
|
|
919
|
+
1. npx quadwork init — one-time global setup, opens dashboard
|
|
920
|
+
2. Open /setup in browser — create projects with guided web UI
|
|
921
|
+
3. npx quadwork stop — stop everything when done
|
|
730
922
|
|
|
731
923
|
Examples:
|
|
732
924
|
npx quadwork init
|
|
733
925
|
npx quadwork start
|
|
734
926
|
npx quadwork stop
|
|
735
|
-
npx quadwork add-project
|
|
736
927
|
`);
|
|
737
928
|
if (command) process.exit(1);
|
|
738
929
|
}
|