pure-point-guard 0.1.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/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +7 -0
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +462 -0
- package/dist/cli.js +2896 -0
- package/dist/cli.js.map +1 -0
- package/package.json +66 -0
- package/skills/ppg/SKILL.md +16 -0
- package/skills/ppg-conductor/SKILL.md +52 -0
- package/skills/ppg-conductor/references/commands.md +248 -0
- package/skills/ppg-conductor/references/conductor.md +183 -0
- package/skills/ppg-conductor/references/modes.md +85 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2896 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/lib/errors.ts
|
|
13
|
+
var PgError, TmuxNotFoundError, NotGitRepoError, NotInitializedError, ManifestLockError, WorktreeNotFoundError, AgentNotFoundError, MergeFailedError;
|
|
14
|
+
var init_errors = __esm({
|
|
15
|
+
"src/lib/errors.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
PgError = class extends Error {
|
|
18
|
+
constructor(message, code, exitCode = 1) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.exitCode = exitCode;
|
|
22
|
+
this.name = "PgError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
TmuxNotFoundError = class extends PgError {
|
|
26
|
+
constructor() {
|
|
27
|
+
super(
|
|
28
|
+
"tmux is not installed or not in PATH. Install it with: brew install tmux",
|
|
29
|
+
"TMUX_NOT_FOUND"
|
|
30
|
+
);
|
|
31
|
+
this.name = "TmuxNotFoundError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
NotGitRepoError = class extends PgError {
|
|
35
|
+
constructor(dir) {
|
|
36
|
+
super(
|
|
37
|
+
`Not a git repository: ${dir}`,
|
|
38
|
+
"NOT_GIT_REPO"
|
|
39
|
+
);
|
|
40
|
+
this.name = "NotGitRepoError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
NotInitializedError = class extends PgError {
|
|
44
|
+
constructor(dir) {
|
|
45
|
+
super(
|
|
46
|
+
`Point Guard not initialized in ${dir}. Run 'ppg init' first.`,
|
|
47
|
+
"NOT_INITIALIZED"
|
|
48
|
+
);
|
|
49
|
+
this.name = "NotInitializedError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
ManifestLockError = class extends PgError {
|
|
53
|
+
constructor() {
|
|
54
|
+
super(
|
|
55
|
+
"Could not acquire manifest lock. Another ppg process may be running.",
|
|
56
|
+
"MANIFEST_LOCK"
|
|
57
|
+
);
|
|
58
|
+
this.name = "ManifestLockError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
WorktreeNotFoundError = class extends PgError {
|
|
62
|
+
constructor(id) {
|
|
63
|
+
super(
|
|
64
|
+
`Worktree not found: ${id}`,
|
|
65
|
+
"WORKTREE_NOT_FOUND"
|
|
66
|
+
);
|
|
67
|
+
this.name = "WorktreeNotFoundError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
AgentNotFoundError = class extends PgError {
|
|
71
|
+
constructor(id) {
|
|
72
|
+
super(
|
|
73
|
+
`Agent not found: ${id}`,
|
|
74
|
+
"AGENT_NOT_FOUND"
|
|
75
|
+
);
|
|
76
|
+
this.name = "AgentNotFoundError";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
MergeFailedError = class extends PgError {
|
|
80
|
+
constructor(message) {
|
|
81
|
+
super(message, "MERGE_FAILED");
|
|
82
|
+
this.name = "MergeFailedError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// src/lib/output.ts
|
|
89
|
+
function formatStatus(status) {
|
|
90
|
+
const color = STATUS_COLORS[status] ?? RESET;
|
|
91
|
+
return `${color}${status}${RESET}`;
|
|
92
|
+
}
|
|
93
|
+
function output(data, json) {
|
|
94
|
+
if (json) {
|
|
95
|
+
console.log(JSON.stringify(data, null, 2));
|
|
96
|
+
} else if (typeof data === "string") {
|
|
97
|
+
console.log(data);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(data);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function outputError(error, json) {
|
|
103
|
+
if (json) {
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
const code = error instanceof Error && "code" in error ? error.code : "UNKNOWN";
|
|
106
|
+
console.error(JSON.stringify({ error: message, code }));
|
|
107
|
+
} else {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
console.error(`${RED}Error:${RESET} ${message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function formatTable(rows, columns) {
|
|
113
|
+
if (rows.length === 0) return "No results.";
|
|
114
|
+
const widths = columns.map((col) => {
|
|
115
|
+
const headerLen = col.header.length;
|
|
116
|
+
const maxDataLen = rows.reduce((max, row) => {
|
|
117
|
+
const val = col.format ? col.format(row[col.key]) : String(row[col.key] ?? "");
|
|
118
|
+
const stripped = val.replace(/\x1b\[[0-9;]*m/g, "");
|
|
119
|
+
return Math.max(max, stripped.length);
|
|
120
|
+
}, 0);
|
|
121
|
+
return col.width ?? Math.max(headerLen, maxDataLen);
|
|
122
|
+
});
|
|
123
|
+
const header = columns.map((col, i) => `${BOLD}${col.header.padEnd(widths[i])}${RESET}`).join(" ");
|
|
124
|
+
const separator = widths.map((w) => DIM + "\u2500".repeat(w) + RESET).join(" ");
|
|
125
|
+
const body = rows.map(
|
|
126
|
+
(row) => columns.map((col, i) => {
|
|
127
|
+
const val = col.format ? col.format(row[col.key]) : String(row[col.key] ?? "");
|
|
128
|
+
const stripped = val.replace(/\x1b\[[0-9;]*m/g, "");
|
|
129
|
+
const padding = Math.max(0, widths[i] - stripped.length);
|
|
130
|
+
return val + " ".repeat(padding);
|
|
131
|
+
}).join(" ")
|
|
132
|
+
).join("\n");
|
|
133
|
+
return `${header}
|
|
134
|
+
${separator}
|
|
135
|
+
${body}`;
|
|
136
|
+
}
|
|
137
|
+
function info(message) {
|
|
138
|
+
console.log(`${CYAN}\u25B8${RESET} ${message}`);
|
|
139
|
+
}
|
|
140
|
+
function success(message) {
|
|
141
|
+
console.log(`${GREEN}\u2713${RESET} ${message}`);
|
|
142
|
+
}
|
|
143
|
+
function warn(message) {
|
|
144
|
+
console.log(`${YELLOW}\u26A0${RESET} ${message}`);
|
|
145
|
+
}
|
|
146
|
+
var RESET, BOLD, DIM, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, GRAY, STATUS_COLORS;
|
|
147
|
+
var init_output = __esm({
|
|
148
|
+
"src/lib/output.ts"() {
|
|
149
|
+
"use strict";
|
|
150
|
+
RESET = "\x1B[0m";
|
|
151
|
+
BOLD = "\x1B[1m";
|
|
152
|
+
DIM = "\x1B[2m";
|
|
153
|
+
RED = "\x1B[31m";
|
|
154
|
+
GREEN = "\x1B[32m";
|
|
155
|
+
YELLOW = "\x1B[33m";
|
|
156
|
+
BLUE = "\x1B[34m";
|
|
157
|
+
MAGENTA = "\x1B[35m";
|
|
158
|
+
CYAN = "\x1B[36m";
|
|
159
|
+
GRAY = "\x1B[90m";
|
|
160
|
+
STATUS_COLORS = {
|
|
161
|
+
spawning: YELLOW,
|
|
162
|
+
running: GREEN,
|
|
163
|
+
waiting: CYAN,
|
|
164
|
+
completed: BLUE,
|
|
165
|
+
failed: RED,
|
|
166
|
+
killed: MAGENTA,
|
|
167
|
+
lost: RED + BOLD,
|
|
168
|
+
active: GREEN,
|
|
169
|
+
merging: YELLOW,
|
|
170
|
+
merged: BLUE,
|
|
171
|
+
cleaned: GRAY
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// src/lib/paths.ts
|
|
177
|
+
import path from "path";
|
|
178
|
+
function pgDir(projectRoot) {
|
|
179
|
+
return path.join(projectRoot, PG_DIR);
|
|
180
|
+
}
|
|
181
|
+
function manifestPath(projectRoot) {
|
|
182
|
+
return path.join(pgDir(projectRoot), "manifest.json");
|
|
183
|
+
}
|
|
184
|
+
function configPath(projectRoot) {
|
|
185
|
+
return path.join(pgDir(projectRoot), "config.yaml");
|
|
186
|
+
}
|
|
187
|
+
function resultsDir(projectRoot) {
|
|
188
|
+
return path.join(pgDir(projectRoot), "results");
|
|
189
|
+
}
|
|
190
|
+
function resultFile(projectRoot, agentId2) {
|
|
191
|
+
return path.join(resultsDir(projectRoot), `${agentId2}.md`);
|
|
192
|
+
}
|
|
193
|
+
function templatesDir(projectRoot) {
|
|
194
|
+
return path.join(pgDir(projectRoot), "templates");
|
|
195
|
+
}
|
|
196
|
+
function logsDir(projectRoot) {
|
|
197
|
+
return path.join(pgDir(projectRoot), "logs");
|
|
198
|
+
}
|
|
199
|
+
function promptsDir(projectRoot) {
|
|
200
|
+
return path.join(pgDir(projectRoot), "prompts");
|
|
201
|
+
}
|
|
202
|
+
function promptFile(projectRoot, agentId2) {
|
|
203
|
+
return path.join(promptsDir(projectRoot), `${agentId2}.md`);
|
|
204
|
+
}
|
|
205
|
+
function worktreeBaseDir(projectRoot) {
|
|
206
|
+
return path.join(projectRoot, ".worktrees");
|
|
207
|
+
}
|
|
208
|
+
function worktreePath(projectRoot, id) {
|
|
209
|
+
return path.join(worktreeBaseDir(projectRoot), id);
|
|
210
|
+
}
|
|
211
|
+
var PG_DIR;
|
|
212
|
+
var init_paths = __esm({
|
|
213
|
+
"src/lib/paths.ts"() {
|
|
214
|
+
"use strict";
|
|
215
|
+
PG_DIR = ".pg";
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// src/core/config.ts
|
|
220
|
+
import fs from "fs/promises";
|
|
221
|
+
import YAML from "yaml";
|
|
222
|
+
async function loadConfig(projectRoot) {
|
|
223
|
+
const cfgPath = configPath(projectRoot);
|
|
224
|
+
try {
|
|
225
|
+
const raw = await fs.readFile(cfgPath, "utf-8");
|
|
226
|
+
const parsed = YAML.parse(raw);
|
|
227
|
+
return mergeConfig(DEFAULT_CONFIG, parsed);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (err.code === "ENOENT") {
|
|
230
|
+
return DEFAULT_CONFIG;
|
|
231
|
+
}
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function mergeConfig(defaults, overrides) {
|
|
236
|
+
return {
|
|
237
|
+
...defaults,
|
|
238
|
+
...overrides,
|
|
239
|
+
agents: {
|
|
240
|
+
...defaults.agents,
|
|
241
|
+
...overrides.agents ?? {}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function writeDefaultConfig(projectRoot) {
|
|
246
|
+
const cfgPath = configPath(projectRoot);
|
|
247
|
+
const content = YAML.stringify(DEFAULT_CONFIG, { indent: 2 });
|
|
248
|
+
await fs.writeFile(cfgPath, content, "utf-8");
|
|
249
|
+
}
|
|
250
|
+
function resolveAgentConfig(config, name) {
|
|
251
|
+
const agentName = name ?? config.defaultAgent;
|
|
252
|
+
const agent = config.agents[agentName];
|
|
253
|
+
if (!agent) {
|
|
254
|
+
throw new Error(`Unknown agent type: ${agentName}. Available: ${Object.keys(config.agents).join(", ")}`);
|
|
255
|
+
}
|
|
256
|
+
return agent;
|
|
257
|
+
}
|
|
258
|
+
var DEFAULT_CONFIG;
|
|
259
|
+
var init_config = __esm({
|
|
260
|
+
"src/core/config.ts"() {
|
|
261
|
+
"use strict";
|
|
262
|
+
init_paths();
|
|
263
|
+
DEFAULT_CONFIG = {
|
|
264
|
+
sessionName: "ppg",
|
|
265
|
+
defaultAgent: "claude",
|
|
266
|
+
agents: {
|
|
267
|
+
claude: {
|
|
268
|
+
name: "claude",
|
|
269
|
+
command: "claude --dangerously-skip-permissions",
|
|
270
|
+
interactive: true,
|
|
271
|
+
resultInstructions: [
|
|
272
|
+
"When you have completed the task, write a summary of what you did and any important notes",
|
|
273
|
+
"to the result file at: {{RESULT_FILE}}",
|
|
274
|
+
"Use this exact format:",
|
|
275
|
+
"",
|
|
276
|
+
"# Result: {{AGENT_ID}}",
|
|
277
|
+
"",
|
|
278
|
+
"## Summary",
|
|
279
|
+
"<what you accomplished>",
|
|
280
|
+
"",
|
|
281
|
+
"## Changes",
|
|
282
|
+
"<list of files changed>",
|
|
283
|
+
"",
|
|
284
|
+
"## Notes",
|
|
285
|
+
"<any important observations>"
|
|
286
|
+
].join("\n")
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
worktreeBase: ".worktrees",
|
|
290
|
+
templateDir: ".pg/templates",
|
|
291
|
+
resultDir: ".pg/results",
|
|
292
|
+
logDir: ".pg/logs",
|
|
293
|
+
envFiles: [".env", ".env.local"],
|
|
294
|
+
symlinkNodeModules: true
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// src/lib/cjs-compat.ts
|
|
300
|
+
async function getLockfile() {
|
|
301
|
+
if (!_lockfile) {
|
|
302
|
+
const mod = await import("proper-lockfile");
|
|
303
|
+
_lockfile = mod.default ?? mod;
|
|
304
|
+
}
|
|
305
|
+
return _lockfile;
|
|
306
|
+
}
|
|
307
|
+
async function getWriteFileAtomic() {
|
|
308
|
+
if (!_writeFileAtomic) {
|
|
309
|
+
const mod = await import("write-file-atomic");
|
|
310
|
+
_writeFileAtomic = mod.default ?? mod;
|
|
311
|
+
}
|
|
312
|
+
return _writeFileAtomic;
|
|
313
|
+
}
|
|
314
|
+
var _lockfile, _writeFileAtomic;
|
|
315
|
+
var init_cjs_compat = __esm({
|
|
316
|
+
"src/lib/cjs-compat.ts"() {
|
|
317
|
+
"use strict";
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// src/core/manifest.ts
|
|
322
|
+
import fs2 from "fs/promises";
|
|
323
|
+
function createEmptyManifest(projectRoot, sessionName) {
|
|
324
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
325
|
+
return {
|
|
326
|
+
version: 1,
|
|
327
|
+
projectRoot,
|
|
328
|
+
sessionName,
|
|
329
|
+
worktrees: {},
|
|
330
|
+
createdAt: now,
|
|
331
|
+
updatedAt: now
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
async function readManifest(projectRoot) {
|
|
335
|
+
const mPath = manifestPath(projectRoot);
|
|
336
|
+
const raw = await fs2.readFile(mPath, "utf-8");
|
|
337
|
+
return JSON.parse(raw);
|
|
338
|
+
}
|
|
339
|
+
async function writeManifest(projectRoot, manifest) {
|
|
340
|
+
const writeFileAtomic = await getWriteFileAtomic();
|
|
341
|
+
const mPath = manifestPath(projectRoot);
|
|
342
|
+
manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
343
|
+
await writeFileAtomic(mPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
344
|
+
}
|
|
345
|
+
async function updateManifest(projectRoot, updater) {
|
|
346
|
+
const lockfile = await getLockfile();
|
|
347
|
+
const mPath = manifestPath(projectRoot);
|
|
348
|
+
let release;
|
|
349
|
+
try {
|
|
350
|
+
release = await lockfile.lock(mPath, {
|
|
351
|
+
stale: 1e4,
|
|
352
|
+
retries: {
|
|
353
|
+
retries: 5,
|
|
354
|
+
minTimeout: 100,
|
|
355
|
+
maxTimeout: 1e3
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
} catch {
|
|
359
|
+
throw new ManifestLockError();
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const manifest = await readManifest(projectRoot);
|
|
363
|
+
const updated = await updater(manifest);
|
|
364
|
+
await writeManifest(projectRoot, updated);
|
|
365
|
+
return updated;
|
|
366
|
+
} finally {
|
|
367
|
+
if (release) {
|
|
368
|
+
await release();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function getWorktree(manifest, id) {
|
|
373
|
+
return manifest.worktrees[id];
|
|
374
|
+
}
|
|
375
|
+
function findWorktreeByName(manifest, name) {
|
|
376
|
+
return Object.values(manifest.worktrees).find(
|
|
377
|
+
(wt) => wt.name === name || wt.branch === name
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
function resolveWorktree(manifest, ref) {
|
|
381
|
+
return getWorktree(manifest, ref) ?? findWorktreeByName(manifest, ref);
|
|
382
|
+
}
|
|
383
|
+
function findAgent(manifest, agentId2) {
|
|
384
|
+
for (const wt of Object.values(manifest.worktrees)) {
|
|
385
|
+
const agent = wt.agents[agentId2];
|
|
386
|
+
if (agent) {
|
|
387
|
+
return { worktree: wt, agent };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return void 0;
|
|
391
|
+
}
|
|
392
|
+
var init_manifest = __esm({
|
|
393
|
+
"src/core/manifest.ts"() {
|
|
394
|
+
"use strict";
|
|
395
|
+
init_paths();
|
|
396
|
+
init_cjs_compat();
|
|
397
|
+
init_errors();
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// src/commands/init.ts
|
|
402
|
+
var init_exports = {};
|
|
403
|
+
__export(init_exports, {
|
|
404
|
+
initCommand: () => initCommand
|
|
405
|
+
});
|
|
406
|
+
import fs3 from "fs/promises";
|
|
407
|
+
import path2 from "path";
|
|
408
|
+
import { execa } from "execa";
|
|
409
|
+
async function initCommand(options) {
|
|
410
|
+
const cwd = process.cwd();
|
|
411
|
+
let projectRoot;
|
|
412
|
+
try {
|
|
413
|
+
const result = await execa("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
414
|
+
projectRoot = result.stdout.trim();
|
|
415
|
+
} catch {
|
|
416
|
+
throw new NotGitRepoError(cwd);
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
await execa("tmux", ["-V"]);
|
|
420
|
+
} catch {
|
|
421
|
+
throw new TmuxNotFoundError();
|
|
422
|
+
}
|
|
423
|
+
const dirs = [
|
|
424
|
+
pgDir(projectRoot),
|
|
425
|
+
resultsDir(projectRoot),
|
|
426
|
+
logsDir(projectRoot),
|
|
427
|
+
templatesDir(projectRoot),
|
|
428
|
+
promptsDir(projectRoot)
|
|
429
|
+
];
|
|
430
|
+
for (const dir of dirs) {
|
|
431
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
432
|
+
}
|
|
433
|
+
info("Created .pg/ directory structure");
|
|
434
|
+
await writeDefaultConfig(projectRoot);
|
|
435
|
+
info("Wrote default config.yaml");
|
|
436
|
+
const dirName = path2.basename(projectRoot);
|
|
437
|
+
const sessionName = `ppg-${dirName}`;
|
|
438
|
+
const manifest = createEmptyManifest(projectRoot, sessionName);
|
|
439
|
+
await writeManifest(projectRoot, manifest);
|
|
440
|
+
info("Wrote empty manifest.json");
|
|
441
|
+
await updateGitignore(projectRoot);
|
|
442
|
+
info("Updated .gitignore");
|
|
443
|
+
const templatePath = path2.join(templatesDir(projectRoot), "default.md");
|
|
444
|
+
try {
|
|
445
|
+
await fs3.access(templatePath);
|
|
446
|
+
} catch {
|
|
447
|
+
await fs3.writeFile(templatePath, DEFAULT_TEMPLATE, "utf-8");
|
|
448
|
+
info("Wrote sample template: default.md");
|
|
449
|
+
}
|
|
450
|
+
const conductorPath = path2.join(pgDir(projectRoot), "conductor-context.md");
|
|
451
|
+
await fs3.writeFile(conductorPath, CONDUCTOR_CONTEXT, "utf-8");
|
|
452
|
+
info("Wrote conductor-context.md");
|
|
453
|
+
const pluginRegistered = await registerClaudePlugin();
|
|
454
|
+
if (pluginRegistered) {
|
|
455
|
+
info("Registered ppg Claude Code plugin");
|
|
456
|
+
}
|
|
457
|
+
if (options.json) {
|
|
458
|
+
console.log(JSON.stringify({
|
|
459
|
+
success: true,
|
|
460
|
+
projectRoot,
|
|
461
|
+
sessionName,
|
|
462
|
+
pgDir: pgDir(projectRoot),
|
|
463
|
+
pluginRegistered
|
|
464
|
+
}));
|
|
465
|
+
} else {
|
|
466
|
+
success(`Point Guard initialized in ${projectRoot}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function registerClaudePlugin() {
|
|
470
|
+
try {
|
|
471
|
+
const home = process.env.HOME;
|
|
472
|
+
if (!home) return false;
|
|
473
|
+
const skillsDir = path2.join(home, ".claude", "skills");
|
|
474
|
+
const pkgRoot = new URL("../../", import.meta.url);
|
|
475
|
+
const srcSkillsDir = new URL("skills/", pkgRoot).pathname;
|
|
476
|
+
try {
|
|
477
|
+
await fs3.access(srcSkillsDir);
|
|
478
|
+
} catch {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
const skillFolders = ["ppg", "ppg-conductor"];
|
|
482
|
+
let copied = false;
|
|
483
|
+
for (const folder of skillFolders) {
|
|
484
|
+
const srcDir = path2.join(srcSkillsDir, folder);
|
|
485
|
+
const destDir = path2.join(skillsDir, folder);
|
|
486
|
+
try {
|
|
487
|
+
await fs3.access(srcDir);
|
|
488
|
+
} catch {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
await fs3.cp(srcDir, destDir, { recursive: true, force: true });
|
|
492
|
+
copied = true;
|
|
493
|
+
}
|
|
494
|
+
return copied;
|
|
495
|
+
} catch {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function updateGitignore(projectRoot) {
|
|
500
|
+
const gitignorePath = path2.join(projectRoot, ".gitignore");
|
|
501
|
+
const entriesToAdd = [
|
|
502
|
+
".pg/results/",
|
|
503
|
+
".pg/logs/",
|
|
504
|
+
".pg/manifest.json",
|
|
505
|
+
".pg/prompts/",
|
|
506
|
+
".pg/conductor-context.md"
|
|
507
|
+
];
|
|
508
|
+
let content = "";
|
|
509
|
+
try {
|
|
510
|
+
content = await fs3.readFile(gitignorePath, "utf-8");
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
const lines = content.split("\n");
|
|
514
|
+
const toAdd = entriesToAdd.filter((entry) => !lines.includes(entry));
|
|
515
|
+
if (toAdd.length > 0) {
|
|
516
|
+
const addition = (content.endsWith("\n") || content === "" ? "" : "\n") + "\n# Point Guard\n" + toAdd.join("\n") + "\n";
|
|
517
|
+
await fs3.appendFile(gitignorePath, addition, "utf-8");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
var CONDUCTOR_CONTEXT, DEFAULT_TEMPLATE;
|
|
521
|
+
var init_init = __esm({
|
|
522
|
+
"src/commands/init.ts"() {
|
|
523
|
+
"use strict";
|
|
524
|
+
init_paths();
|
|
525
|
+
init_errors();
|
|
526
|
+
init_output();
|
|
527
|
+
init_config();
|
|
528
|
+
init_manifest();
|
|
529
|
+
CONDUCTOR_CONTEXT = `# PPG Conductor Context
|
|
530
|
+
|
|
531
|
+
You are operating on the master branch of a ppg-managed project.
|
|
532
|
+
|
|
533
|
+
## Critical Rule
|
|
534
|
+
**NEVER make code changes directly on the master branch.** Use \`ppg spawn\` to create worktrees.
|
|
535
|
+
|
|
536
|
+
## Quick Reference
|
|
537
|
+
- \`ppg spawn --name <name> --prompt "<task>" --json --no-open\` \u2014 Spawn worktree + agent
|
|
538
|
+
- \`ppg status --json\` \u2014 Check statuses
|
|
539
|
+
- \`ppg aggregate --all --json\` \u2014 Collect results
|
|
540
|
+
- \`ppg merge <wt-id> --json\` \u2014 Merge completed work
|
|
541
|
+
- \`ppg kill --agent <id> --json\` \u2014 Kill agent
|
|
542
|
+
|
|
543
|
+
## Workflow
|
|
544
|
+
1. Break request into parallelizable tasks
|
|
545
|
+
2. Spawn each: \`ppg spawn --name <name> --prompt "<self-contained prompt>" --json --no-open\`
|
|
546
|
+
3. Poll: \`ppg status --json\` every 5s
|
|
547
|
+
4. Aggregate: \`ppg aggregate --all --json\`
|
|
548
|
+
5. Present results, then merge confirmed worktrees
|
|
549
|
+
|
|
550
|
+
Each agent prompt must be self-contained \u2014 agents have no memory of this conversation.
|
|
551
|
+
Always use \`--json --no-open\`.
|
|
552
|
+
`;
|
|
553
|
+
DEFAULT_TEMPLATE = `# Task: {{TASK_NAME}}
|
|
554
|
+
|
|
555
|
+
## Context
|
|
556
|
+
You are working in a git worktree at: {{WORKTREE_PATH}}
|
|
557
|
+
Branch: {{BRANCH}}
|
|
558
|
+
Project root: {{PROJECT_ROOT}}
|
|
559
|
+
|
|
560
|
+
## Instructions
|
|
561
|
+
{{PROMPT}}
|
|
562
|
+
|
|
563
|
+
## Result Reporting
|
|
564
|
+
When you have completed the task, write your results to:
|
|
565
|
+
{{RESULT_FILE}}
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// src/core/worktree.ts
|
|
571
|
+
import { execa as execa2 } from "execa";
|
|
572
|
+
async function getRepoRoot(cwd) {
|
|
573
|
+
try {
|
|
574
|
+
const result = await execa2("git", ["rev-parse", "--show-toplevel"], {
|
|
575
|
+
cwd: cwd ?? process.cwd()
|
|
576
|
+
});
|
|
577
|
+
return result.stdout.trim();
|
|
578
|
+
} catch {
|
|
579
|
+
throw new NotGitRepoError(cwd ?? process.cwd());
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async function getCurrentBranch(cwd) {
|
|
583
|
+
const result = await execa2("git", ["branch", "--show-current"], {
|
|
584
|
+
cwd: cwd ?? process.cwd()
|
|
585
|
+
});
|
|
586
|
+
return result.stdout.trim();
|
|
587
|
+
}
|
|
588
|
+
async function createWorktree(repoRoot, id, options) {
|
|
589
|
+
const wtPath = worktreePath(repoRoot, id);
|
|
590
|
+
const args = ["worktree", "add", wtPath, "-b", options.branch];
|
|
591
|
+
if (options.base) {
|
|
592
|
+
args.push(options.base);
|
|
593
|
+
}
|
|
594
|
+
await execa2("git", args, { cwd: repoRoot });
|
|
595
|
+
return wtPath;
|
|
596
|
+
}
|
|
597
|
+
async function removeWorktree(repoRoot, wtPath, options) {
|
|
598
|
+
const args = ["worktree", "remove", wtPath];
|
|
599
|
+
if (options?.force) {
|
|
600
|
+
args.push("--force");
|
|
601
|
+
}
|
|
602
|
+
await execa2("git", args, { cwd: repoRoot });
|
|
603
|
+
if (options?.deleteBranch && options.branchName) {
|
|
604
|
+
try {
|
|
605
|
+
await execa2("git", ["branch", "-D", options.branchName], { cwd: repoRoot });
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function pruneWorktrees(repoRoot) {
|
|
611
|
+
await execa2("git", ["worktree", "prune"], { cwd: repoRoot });
|
|
612
|
+
}
|
|
613
|
+
var init_worktree = __esm({
|
|
614
|
+
"src/core/worktree.ts"() {
|
|
615
|
+
"use strict";
|
|
616
|
+
init_paths();
|
|
617
|
+
init_errors();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// src/core/env.ts
|
|
622
|
+
import fs4 from "fs/promises";
|
|
623
|
+
import path3 from "path";
|
|
624
|
+
async function setupWorktreeEnv(projectRoot, wtPath, config) {
|
|
625
|
+
for (const envFile of config.envFiles) {
|
|
626
|
+
const src = path3.join(projectRoot, envFile);
|
|
627
|
+
const dest = path3.join(wtPath, envFile);
|
|
628
|
+
try {
|
|
629
|
+
await fs4.access(src);
|
|
630
|
+
await fs4.copyFile(src, dest);
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (config.symlinkNodeModules) {
|
|
635
|
+
const src = path3.join(projectRoot, "node_modules");
|
|
636
|
+
const dest = path3.join(wtPath, "node_modules");
|
|
637
|
+
try {
|
|
638
|
+
await fs4.access(src);
|
|
639
|
+
try {
|
|
640
|
+
await fs4.lstat(dest);
|
|
641
|
+
} catch {
|
|
642
|
+
await fs4.symlink(src, dest, "dir");
|
|
643
|
+
}
|
|
644
|
+
} catch {
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async function teardownWorktreeEnv(wtPath) {
|
|
649
|
+
const nmPath = path3.join(wtPath, "node_modules");
|
|
650
|
+
try {
|
|
651
|
+
const stat = await fs4.lstat(nmPath);
|
|
652
|
+
if (stat.isSymbolicLink()) {
|
|
653
|
+
await fs4.unlink(nmPath);
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
var init_env = __esm({
|
|
659
|
+
"src/core/env.ts"() {
|
|
660
|
+
"use strict";
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// src/core/template.ts
|
|
665
|
+
import fs5 from "fs/promises";
|
|
666
|
+
import path4 from "path";
|
|
667
|
+
async function listTemplates(projectRoot) {
|
|
668
|
+
const dir = templatesDir(projectRoot);
|
|
669
|
+
try {
|
|
670
|
+
const files = await fs5.readdir(dir);
|
|
671
|
+
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
672
|
+
} catch {
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function loadTemplate(projectRoot, name) {
|
|
677
|
+
const dir = templatesDir(projectRoot);
|
|
678
|
+
const filePath = path4.join(dir, `${name}.md`);
|
|
679
|
+
return fs5.readFile(filePath, "utf-8");
|
|
680
|
+
}
|
|
681
|
+
function renderTemplate(content, context) {
|
|
682
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
683
|
+
return context[key] ?? `{{${key}}}`;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
var init_template = __esm({
|
|
687
|
+
"src/core/template.ts"() {
|
|
688
|
+
"use strict";
|
|
689
|
+
init_paths();
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// src/core/tmux.ts
|
|
694
|
+
var tmux_exports = {};
|
|
695
|
+
__export(tmux_exports, {
|
|
696
|
+
attachSession: () => attachSession,
|
|
697
|
+
capturePane: () => capturePane,
|
|
698
|
+
checkTmux: () => checkTmux,
|
|
699
|
+
createWindow: () => createWindow,
|
|
700
|
+
ensureSession: () => ensureSession,
|
|
701
|
+
getPaneInfo: () => getPaneInfo,
|
|
702
|
+
isInsideTmux: () => isInsideTmux,
|
|
703
|
+
killPane: () => killPane,
|
|
704
|
+
killWindow: () => killWindow,
|
|
705
|
+
listPanes: () => listPanes,
|
|
706
|
+
listSessionPanes: () => listSessionPanes,
|
|
707
|
+
selectPane: () => selectPane,
|
|
708
|
+
selectWindow: () => selectWindow,
|
|
709
|
+
sendCtrlC: () => sendCtrlC,
|
|
710
|
+
sendKeys: () => sendKeys,
|
|
711
|
+
sendLiteral: () => sendLiteral,
|
|
712
|
+
sendRawKeys: () => sendRawKeys,
|
|
713
|
+
sessionExists: () => sessionExists,
|
|
714
|
+
splitPane: () => splitPane
|
|
715
|
+
});
|
|
716
|
+
import { execa as execa3 } from "execa";
|
|
717
|
+
async function checkTmux() {
|
|
718
|
+
try {
|
|
719
|
+
await execa3("tmux", ["-V"]);
|
|
720
|
+
} catch {
|
|
721
|
+
throw new TmuxNotFoundError();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function sessionExists(name) {
|
|
725
|
+
try {
|
|
726
|
+
await execa3("tmux", ["has-session", "-t", name]);
|
|
727
|
+
return true;
|
|
728
|
+
} catch {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async function ensureSession(name) {
|
|
733
|
+
if (!await sessionExists(name)) {
|
|
734
|
+
await execa3("tmux", ["new-session", "-d", "-s", name, "-x", "220", "-y", "50"]);
|
|
735
|
+
await execa3("tmux", ["set-option", "-t", name, "mouse", "on"]);
|
|
736
|
+
await execa3("tmux", ["set-option", "-t", name, "history-limit", "50000"]);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function createWindow(session, name, cwd) {
|
|
740
|
+
const result = await execa3("tmux", [
|
|
741
|
+
"new-window",
|
|
742
|
+
"-t",
|
|
743
|
+
session,
|
|
744
|
+
"-n",
|
|
745
|
+
name,
|
|
746
|
+
"-c",
|
|
747
|
+
cwd,
|
|
748
|
+
"-P",
|
|
749
|
+
"-F",
|
|
750
|
+
"#{window_index}"
|
|
751
|
+
]);
|
|
752
|
+
const windowIndex = result.stdout.trim();
|
|
753
|
+
return `${session}:${windowIndex}`;
|
|
754
|
+
}
|
|
755
|
+
async function splitPane(target, direction, cwd) {
|
|
756
|
+
const flag = direction === "horizontal" ? "-h" : "-v";
|
|
757
|
+
const result = await execa3("tmux", [
|
|
758
|
+
"split-window",
|
|
759
|
+
flag,
|
|
760
|
+
"-t",
|
|
761
|
+
target,
|
|
762
|
+
"-c",
|
|
763
|
+
cwd,
|
|
764
|
+
"-P",
|
|
765
|
+
"-F",
|
|
766
|
+
"#{session_name}:#{window_index}.#{pane_index}|#{pane_id}"
|
|
767
|
+
]);
|
|
768
|
+
const [canonicalTarget, paneId] = result.stdout.trim().split("|");
|
|
769
|
+
return { paneId, target: canonicalTarget };
|
|
770
|
+
}
|
|
771
|
+
async function sendKeys(target, command) {
|
|
772
|
+
await execa3("tmux", ["send-keys", "-t", target, "-l", command + "\n"]);
|
|
773
|
+
}
|
|
774
|
+
async function sendLiteral(target, text) {
|
|
775
|
+
await execa3("tmux", ["send-keys", "-t", target, "-l", text]);
|
|
776
|
+
}
|
|
777
|
+
async function sendRawKeys(target, keys) {
|
|
778
|
+
await execa3("tmux", ["send-keys", "-t", target, keys]);
|
|
779
|
+
}
|
|
780
|
+
async function capturePane(target, lines) {
|
|
781
|
+
const args = ["capture-pane", "-t", target, "-p"];
|
|
782
|
+
if (lines) {
|
|
783
|
+
args.push("-S", `-${lines}`);
|
|
784
|
+
}
|
|
785
|
+
const result = await execa3("tmux", args);
|
|
786
|
+
return result.stdout;
|
|
787
|
+
}
|
|
788
|
+
async function killPane(target) {
|
|
789
|
+
try {
|
|
790
|
+
await execa3("tmux", ["kill-pane", "-t", target]);
|
|
791
|
+
} catch {
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async function killWindow(target) {
|
|
795
|
+
try {
|
|
796
|
+
await execa3("tmux", ["kill-window", "-t", target]);
|
|
797
|
+
} catch {
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function listPanes(target) {
|
|
801
|
+
try {
|
|
802
|
+
const result = await execa3("tmux", [
|
|
803
|
+
"list-panes",
|
|
804
|
+
"-t",
|
|
805
|
+
target,
|
|
806
|
+
"-F",
|
|
807
|
+
"#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_dead}|#{pane_dead_status}"
|
|
808
|
+
]);
|
|
809
|
+
return result.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
810
|
+
const [paneId, panePid, currentCommand, dead, deadStatus] = line.split("|");
|
|
811
|
+
return {
|
|
812
|
+
paneId,
|
|
813
|
+
panePid,
|
|
814
|
+
currentCommand,
|
|
815
|
+
isDead: dead === "1",
|
|
816
|
+
deadStatus: deadStatus ? parseInt(deadStatus, 10) : void 0
|
|
817
|
+
};
|
|
818
|
+
});
|
|
819
|
+
} catch {
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async function getPaneInfo(target) {
|
|
824
|
+
try {
|
|
825
|
+
const result = await execa3("tmux", [
|
|
826
|
+
"display-message",
|
|
827
|
+
"-t",
|
|
828
|
+
target,
|
|
829
|
+
"-p",
|
|
830
|
+
"#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_dead}|#{pane_dead_status}"
|
|
831
|
+
]);
|
|
832
|
+
const [paneId, panePid, currentCommand, dead, deadStatus] = result.stdout.trim().split("|");
|
|
833
|
+
return {
|
|
834
|
+
paneId,
|
|
835
|
+
panePid,
|
|
836
|
+
currentCommand,
|
|
837
|
+
isDead: dead === "1",
|
|
838
|
+
deadStatus: deadStatus ? parseInt(deadStatus, 10) : void 0
|
|
839
|
+
};
|
|
840
|
+
} catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
async function listSessionPanes(session) {
|
|
845
|
+
const map = /* @__PURE__ */ new Map();
|
|
846
|
+
try {
|
|
847
|
+
const result = await execa3("tmux", [
|
|
848
|
+
"list-panes",
|
|
849
|
+
"-s",
|
|
850
|
+
"-t",
|
|
851
|
+
session,
|
|
852
|
+
"-F",
|
|
853
|
+
"#{session_name}:#{window_index}.#{pane_index}|#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_dead}|#{pane_dead_status}"
|
|
854
|
+
]);
|
|
855
|
+
for (const line of result.stdout.trim().split("\n").filter(Boolean)) {
|
|
856
|
+
const [target, paneId, panePid, currentCommand, dead, deadStatus] = line.split("|");
|
|
857
|
+
const info2 = {
|
|
858
|
+
paneId,
|
|
859
|
+
panePid,
|
|
860
|
+
currentCommand,
|
|
861
|
+
isDead: dead === "1",
|
|
862
|
+
deadStatus: deadStatus ? parseInt(deadStatus, 10) : void 0
|
|
863
|
+
};
|
|
864
|
+
map.set(target, info2);
|
|
865
|
+
map.set(paneId, info2);
|
|
866
|
+
const dotIdx = target.lastIndexOf(".");
|
|
867
|
+
if (dotIdx !== -1) {
|
|
868
|
+
map.set(target.slice(0, dotIdx), info2);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
return map;
|
|
874
|
+
}
|
|
875
|
+
async function selectPane(target) {
|
|
876
|
+
await execa3("tmux", ["select-pane", "-t", target]);
|
|
877
|
+
}
|
|
878
|
+
async function selectWindow(target) {
|
|
879
|
+
await execa3("tmux", ["select-window", "-t", target]);
|
|
880
|
+
}
|
|
881
|
+
async function attachSession(session) {
|
|
882
|
+
await execa3("tmux", ["attach-session", "-t", session], { stdio: "inherit" });
|
|
883
|
+
}
|
|
884
|
+
async function isInsideTmux() {
|
|
885
|
+
return !!process.env.TMUX;
|
|
886
|
+
}
|
|
887
|
+
async function sendCtrlC(target) {
|
|
888
|
+
await execa3("tmux", ["send-keys", "-t", target, "C-c"]);
|
|
889
|
+
}
|
|
890
|
+
var init_tmux = __esm({
|
|
891
|
+
"src/core/tmux.ts"() {
|
|
892
|
+
"use strict";
|
|
893
|
+
init_errors();
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// src/core/agent.ts
|
|
898
|
+
import fs6 from "fs/promises";
|
|
899
|
+
async function spawnAgent(options) {
|
|
900
|
+
const {
|
|
901
|
+
agentId: agentId2,
|
|
902
|
+
agentConfig,
|
|
903
|
+
prompt,
|
|
904
|
+
worktreePath: worktreePath2,
|
|
905
|
+
tmuxTarget,
|
|
906
|
+
projectRoot,
|
|
907
|
+
branch
|
|
908
|
+
} = options;
|
|
909
|
+
const resFile = resultFile(projectRoot, agentId2);
|
|
910
|
+
let fullPrompt = prompt;
|
|
911
|
+
if (agentConfig.resultInstructions) {
|
|
912
|
+
const ctx = {
|
|
913
|
+
WORKTREE_PATH: worktreePath2,
|
|
914
|
+
BRANCH: branch,
|
|
915
|
+
AGENT_ID: agentId2,
|
|
916
|
+
RESULT_FILE: resFile,
|
|
917
|
+
PROJECT_ROOT: projectRoot
|
|
918
|
+
};
|
|
919
|
+
const instructions = renderTemplate(agentConfig.resultInstructions, ctx);
|
|
920
|
+
fullPrompt += `
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
924
|
+
${instructions}`;
|
|
925
|
+
}
|
|
926
|
+
const pFile = promptFile(projectRoot, agentId2);
|
|
927
|
+
await fs6.writeFile(pFile, fullPrompt, "utf-8");
|
|
928
|
+
const command = buildAgentCommand(agentConfig, pFile, options.sessionId);
|
|
929
|
+
await sendKeys(tmuxTarget, command);
|
|
930
|
+
return {
|
|
931
|
+
id: agentId2,
|
|
932
|
+
name: agentConfig.name,
|
|
933
|
+
agentType: agentConfig.name,
|
|
934
|
+
status: "running",
|
|
935
|
+
tmuxTarget,
|
|
936
|
+
prompt: prompt.slice(0, 500),
|
|
937
|
+
// Truncate for manifest storage
|
|
938
|
+
resultFile: resFile,
|
|
939
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
940
|
+
...options.sessionId ? { sessionId: options.sessionId } : {}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function buildAgentCommand(agentConfig, promptFilePath, sessionId2) {
|
|
944
|
+
const envPrefix = "unset CLAUDECODE;";
|
|
945
|
+
const { command, promptFlag } = agentConfig;
|
|
946
|
+
const sessionFlag = sessionId2 && command.includes("claude") ? ` --session-id ${sessionId2}` : "";
|
|
947
|
+
if (promptFlag) {
|
|
948
|
+
return `${envPrefix} ${command}${sessionFlag} ${promptFlag} "$(cat ${promptFilePath})"`;
|
|
949
|
+
}
|
|
950
|
+
return `${envPrefix} ${command}${sessionFlag} "$(cat ${promptFilePath})"`;
|
|
951
|
+
}
|
|
952
|
+
async function checkAgentStatus(agent, projectRoot, paneMap) {
|
|
953
|
+
if (["completed", "failed", "killed", "lost"].includes(agent.status)) {
|
|
954
|
+
return { status: agent.status, exitCode: agent.exitCode };
|
|
955
|
+
}
|
|
956
|
+
const hasResult = await fileExists(agent.resultFile);
|
|
957
|
+
if (hasResult) {
|
|
958
|
+
return { status: "completed" };
|
|
959
|
+
}
|
|
960
|
+
const paneInfo = paneMap ? paneMap.get(agent.tmuxTarget) ?? null : await getPaneInfo(agent.tmuxTarget);
|
|
961
|
+
if (!paneInfo) {
|
|
962
|
+
return { status: "lost" };
|
|
963
|
+
}
|
|
964
|
+
if (paneInfo.isDead) {
|
|
965
|
+
const exitCode = paneInfo.deadStatus;
|
|
966
|
+
const hasResultNow = await fileExists(agent.resultFile);
|
|
967
|
+
if (hasResultNow || exitCode === 0) {
|
|
968
|
+
return { status: "completed", exitCode: exitCode ?? 0 };
|
|
969
|
+
}
|
|
970
|
+
return { status: "failed", exitCode };
|
|
971
|
+
}
|
|
972
|
+
if (SHELL_COMMANDS.has(paneInfo.currentCommand)) {
|
|
973
|
+
const hasResultNow = await fileExists(agent.resultFile);
|
|
974
|
+
if (hasResultNow) {
|
|
975
|
+
return { status: "completed", exitCode: 0 };
|
|
976
|
+
}
|
|
977
|
+
return { status: "failed", exitCode: void 0 };
|
|
978
|
+
}
|
|
979
|
+
return { status: "running" };
|
|
980
|
+
}
|
|
981
|
+
async function refreshAllAgentStatuses(manifest, projectRoot) {
|
|
982
|
+
const paneMap = await listSessionPanes(manifest.sessionName);
|
|
983
|
+
const checks = [];
|
|
984
|
+
for (const wt of Object.values(manifest.worktrees)) {
|
|
985
|
+
for (const agent of Object.values(wt.agents)) {
|
|
986
|
+
checks.push({
|
|
987
|
+
agent,
|
|
988
|
+
promise: checkAgentStatus(agent, projectRoot, paneMap)
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const results = await Promise.all(checks.map((c) => c.promise));
|
|
993
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
994
|
+
for (let i = 0; i < checks.length; i++) {
|
|
995
|
+
const { agent } = checks[i];
|
|
996
|
+
const { status, exitCode } = results[i];
|
|
997
|
+
if (status !== agent.status) {
|
|
998
|
+
agent.status = status;
|
|
999
|
+
if (exitCode !== void 0) agent.exitCode = exitCode;
|
|
1000
|
+
if (["completed", "failed", "lost"].includes(status) && !agent.completedAt) {
|
|
1001
|
+
agent.completedAt = now;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const wtChecks = Object.values(manifest.worktrees).filter((wt) => wt.status === "active").map(async (wt) => {
|
|
1006
|
+
const exists = await fileExists(wt.path);
|
|
1007
|
+
if (!exists) {
|
|
1008
|
+
wt.status = "cleaned";
|
|
1009
|
+
for (const agent of Object.values(wt.agents)) {
|
|
1010
|
+
if (!["completed", "failed", "killed"].includes(agent.status)) {
|
|
1011
|
+
agent.status = "lost";
|
|
1012
|
+
if (!agent.completedAt) agent.completedAt = now;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
await Promise.all(wtChecks);
|
|
1018
|
+
return manifest;
|
|
1019
|
+
}
|
|
1020
|
+
async function resumeAgent(options) {
|
|
1021
|
+
const { agent, worktreeId: worktreeId2, sessionName, cwd, windowName, projectRoot } = options;
|
|
1022
|
+
if (!agent.sessionId) {
|
|
1023
|
+
throw new PgError(
|
|
1024
|
+
`Agent ${agent.id} has no session ID. Cannot resume agents spawned before session tracking was added.`,
|
|
1025
|
+
"NO_SESSION_ID"
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
await ensureSession(sessionName);
|
|
1029
|
+
const newTarget = await createWindow(sessionName, windowName, cwd);
|
|
1030
|
+
await sendKeys(newTarget, `unset CLAUDECODE; claude --resume ${agent.sessionId}`);
|
|
1031
|
+
await updateManifest(projectRoot, (m) => {
|
|
1032
|
+
const mAgent = m.worktrees[worktreeId2]?.agents[agent.id];
|
|
1033
|
+
if (mAgent) {
|
|
1034
|
+
mAgent.tmuxTarget = newTarget;
|
|
1035
|
+
mAgent.status = "running";
|
|
1036
|
+
}
|
|
1037
|
+
return m;
|
|
1038
|
+
});
|
|
1039
|
+
return newTarget;
|
|
1040
|
+
}
|
|
1041
|
+
async function killAgent(agent) {
|
|
1042
|
+
await sendCtrlC(agent.tmuxTarget);
|
|
1043
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1044
|
+
const paneInfo = await getPaneInfo(agent.tmuxTarget);
|
|
1045
|
+
if (paneInfo && !paneInfo.isDead) {
|
|
1046
|
+
await killPane(agent.tmuxTarget);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
async function killAgents(agents) {
|
|
1050
|
+
if (agents.length === 0) return;
|
|
1051
|
+
await Promise.all(agents.map((a) => sendCtrlC(a.tmuxTarget).catch(() => {
|
|
1052
|
+
})));
|
|
1053
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1054
|
+
await Promise.all(agents.map(async (a) => {
|
|
1055
|
+
const paneInfo = await getPaneInfo(a.tmuxTarget);
|
|
1056
|
+
if (paneInfo && !paneInfo.isDead) {
|
|
1057
|
+
await killPane(a.tmuxTarget);
|
|
1058
|
+
}
|
|
1059
|
+
}));
|
|
1060
|
+
}
|
|
1061
|
+
async function fileExists(filePath) {
|
|
1062
|
+
try {
|
|
1063
|
+
await fs6.access(filePath);
|
|
1064
|
+
return true;
|
|
1065
|
+
} catch {
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
var SHELL_COMMANDS;
|
|
1070
|
+
var init_agent = __esm({
|
|
1071
|
+
"src/core/agent.ts"() {
|
|
1072
|
+
"use strict";
|
|
1073
|
+
init_paths();
|
|
1074
|
+
init_tmux();
|
|
1075
|
+
init_manifest();
|
|
1076
|
+
init_errors();
|
|
1077
|
+
init_template();
|
|
1078
|
+
init_tmux();
|
|
1079
|
+
SHELL_COMMANDS = /* @__PURE__ */ new Set(["bash", "zsh", "sh", "fish", "dash", "tcsh", "csh"]);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// src/core/terminal.ts
|
|
1084
|
+
import { execa as execa4 } from "execa";
|
|
1085
|
+
async function openTerminalWindow(sessionName, windowTarget, title) {
|
|
1086
|
+
const tmuxCmd = `tmux attach-session -t ${sessionName} \\\\; select-window -t ${windowTarget}`;
|
|
1087
|
+
const script = `
|
|
1088
|
+
tell application "Terminal"
|
|
1089
|
+
activate
|
|
1090
|
+
set newTab to do script "${tmuxCmd}"
|
|
1091
|
+
set custom title of newTab to "${title}"
|
|
1092
|
+
end tell
|
|
1093
|
+
`;
|
|
1094
|
+
try {
|
|
1095
|
+
await execa4("osascript", ["-e", script]);
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
warn(`Could not open Terminal window for "${title}": ${err instanceof Error ? err.message : err}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
var init_terminal = __esm({
|
|
1101
|
+
"src/core/terminal.ts"() {
|
|
1102
|
+
"use strict";
|
|
1103
|
+
init_output();
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// src/lib/id.ts
|
|
1108
|
+
import { randomUUID } from "crypto";
|
|
1109
|
+
import { customAlphabet } from "nanoid";
|
|
1110
|
+
function worktreeId() {
|
|
1111
|
+
return `wt-${shortId()}`;
|
|
1112
|
+
}
|
|
1113
|
+
function agentId() {
|
|
1114
|
+
return `ag-${longId()}`;
|
|
1115
|
+
}
|
|
1116
|
+
function sessionId() {
|
|
1117
|
+
return randomUUID();
|
|
1118
|
+
}
|
|
1119
|
+
var alphabet, shortId, longId;
|
|
1120
|
+
var init_id = __esm({
|
|
1121
|
+
"src/lib/id.ts"() {
|
|
1122
|
+
"use strict";
|
|
1123
|
+
alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1124
|
+
shortId = customAlphabet(alphabet, 6);
|
|
1125
|
+
longId = customAlphabet(alphabet, 8);
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
// src/commands/spawn.ts
|
|
1130
|
+
var spawn_exports = {};
|
|
1131
|
+
__export(spawn_exports, {
|
|
1132
|
+
spawnCommand: () => spawnCommand
|
|
1133
|
+
});
|
|
1134
|
+
import fs7 from "fs/promises";
|
|
1135
|
+
async function spawnCommand(options) {
|
|
1136
|
+
const projectRoot = await getRepoRoot();
|
|
1137
|
+
const config = await loadConfig(projectRoot);
|
|
1138
|
+
try {
|
|
1139
|
+
await fs7.access(manifestPath(projectRoot));
|
|
1140
|
+
} catch {
|
|
1141
|
+
throw new NotInitializedError(projectRoot);
|
|
1142
|
+
}
|
|
1143
|
+
const agentConfig = resolveAgentConfig(config, options.agent);
|
|
1144
|
+
const count = options.count ?? 1;
|
|
1145
|
+
const promptText = await resolvePrompt(options, projectRoot);
|
|
1146
|
+
if (options.worktree) {
|
|
1147
|
+
await spawnIntoExistingWorktree(
|
|
1148
|
+
projectRoot,
|
|
1149
|
+
config,
|
|
1150
|
+
agentConfig,
|
|
1151
|
+
options.worktree,
|
|
1152
|
+
promptText,
|
|
1153
|
+
count,
|
|
1154
|
+
options
|
|
1155
|
+
);
|
|
1156
|
+
} else {
|
|
1157
|
+
await spawnNewWorktree(
|
|
1158
|
+
projectRoot,
|
|
1159
|
+
config,
|
|
1160
|
+
agentConfig,
|
|
1161
|
+
promptText,
|
|
1162
|
+
count,
|
|
1163
|
+
options
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
async function resolvePrompt(options, projectRoot) {
|
|
1168
|
+
if (options.prompt) return options.prompt;
|
|
1169
|
+
if (options.promptFile) {
|
|
1170
|
+
return fs7.readFile(options.promptFile, "utf-8");
|
|
1171
|
+
}
|
|
1172
|
+
if (options.template) {
|
|
1173
|
+
const templateContent = await loadTemplate(projectRoot, options.template);
|
|
1174
|
+
const vars = {};
|
|
1175
|
+
for (const v of options.var ?? []) {
|
|
1176
|
+
const [key, ...rest] = v.split("=");
|
|
1177
|
+
vars[key] = rest.join("=");
|
|
1178
|
+
}
|
|
1179
|
+
return templateContent;
|
|
1180
|
+
}
|
|
1181
|
+
throw new PgError("One of --prompt, --prompt-file, or --template is required", "INVALID_ARGS");
|
|
1182
|
+
}
|
|
1183
|
+
async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, count, options) {
|
|
1184
|
+
const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
|
|
1185
|
+
const wtId = worktreeId();
|
|
1186
|
+
const name = options.name ?? wtId;
|
|
1187
|
+
const branchName = `ppg/${name}`;
|
|
1188
|
+
info(`Creating worktree ${wtId} on branch ${branchName}`);
|
|
1189
|
+
const wtPath = await createWorktree(projectRoot, wtId, {
|
|
1190
|
+
branch: branchName,
|
|
1191
|
+
base: baseBranch
|
|
1192
|
+
});
|
|
1193
|
+
await setupWorktreeEnv(projectRoot, wtPath, config);
|
|
1194
|
+
const sessionName = config.sessionName;
|
|
1195
|
+
await ensureSession(sessionName);
|
|
1196
|
+
const windowTarget = await createWindow(sessionName, name, wtPath);
|
|
1197
|
+
const agents = [];
|
|
1198
|
+
for (let i = 0; i < count; i++) {
|
|
1199
|
+
const aId = agentId();
|
|
1200
|
+
let target;
|
|
1201
|
+
if (i === 0) {
|
|
1202
|
+
target = windowTarget;
|
|
1203
|
+
} else if (options.split) {
|
|
1204
|
+
const direction = i % 2 === 1 ? "horizontal" : "vertical";
|
|
1205
|
+
const pane = await splitPane(windowTarget, direction, wtPath);
|
|
1206
|
+
target = pane.target;
|
|
1207
|
+
} else {
|
|
1208
|
+
target = await createWindow(sessionName, `${name}-${i}`, wtPath);
|
|
1209
|
+
}
|
|
1210
|
+
const ctx = {
|
|
1211
|
+
WORKTREE_PATH: wtPath,
|
|
1212
|
+
BRANCH: branchName,
|
|
1213
|
+
AGENT_ID: aId,
|
|
1214
|
+
RESULT_FILE: resultFile(projectRoot, aId),
|
|
1215
|
+
PROJECT_ROOT: projectRoot,
|
|
1216
|
+
TASK_NAME: name,
|
|
1217
|
+
PROMPT: promptText
|
|
1218
|
+
};
|
|
1219
|
+
for (const v of options.var ?? []) {
|
|
1220
|
+
const [key, ...rest] = v.split("=");
|
|
1221
|
+
ctx[key] = rest.join("=");
|
|
1222
|
+
}
|
|
1223
|
+
const renderedPrompt = renderTemplate(promptText, ctx);
|
|
1224
|
+
const agentEntry = await spawnAgent({
|
|
1225
|
+
agentId: aId,
|
|
1226
|
+
agentConfig,
|
|
1227
|
+
prompt: renderedPrompt,
|
|
1228
|
+
worktreePath: wtPath,
|
|
1229
|
+
tmuxTarget: target,
|
|
1230
|
+
projectRoot,
|
|
1231
|
+
branch: branchName,
|
|
1232
|
+
sessionId: sessionId()
|
|
1233
|
+
});
|
|
1234
|
+
agents.push(agentEntry);
|
|
1235
|
+
}
|
|
1236
|
+
const worktreeEntry = {
|
|
1237
|
+
id: wtId,
|
|
1238
|
+
name,
|
|
1239
|
+
path: wtPath,
|
|
1240
|
+
branch: branchName,
|
|
1241
|
+
baseBranch,
|
|
1242
|
+
status: "active",
|
|
1243
|
+
tmuxWindow: windowTarget,
|
|
1244
|
+
agents: Object.fromEntries(agents.map((a) => [a.id, a])),
|
|
1245
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1246
|
+
};
|
|
1247
|
+
await updateManifest(projectRoot, (m) => {
|
|
1248
|
+
m.worktrees[wtId] = worktreeEntry;
|
|
1249
|
+
return m;
|
|
1250
|
+
});
|
|
1251
|
+
if (options.open !== false) {
|
|
1252
|
+
openTerminalWindow(sessionName, windowTarget, name).catch(() => {
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
if (options.json) {
|
|
1256
|
+
output({
|
|
1257
|
+
success: true,
|
|
1258
|
+
worktree: {
|
|
1259
|
+
id: wtId,
|
|
1260
|
+
name,
|
|
1261
|
+
branch: branchName,
|
|
1262
|
+
path: wtPath,
|
|
1263
|
+
tmuxWindow: windowTarget
|
|
1264
|
+
},
|
|
1265
|
+
agents: agents.map((a) => ({
|
|
1266
|
+
id: a.id,
|
|
1267
|
+
tmuxTarget: a.tmuxTarget,
|
|
1268
|
+
sessionId: a.sessionId
|
|
1269
|
+
}))
|
|
1270
|
+
}, true);
|
|
1271
|
+
} else {
|
|
1272
|
+
success(`Spawned worktree ${wtId} with ${agents.length} agent(s)`);
|
|
1273
|
+
for (const a of agents) {
|
|
1274
|
+
info(` Agent ${a.id} \u2192 ${a.tmuxTarget}`);
|
|
1275
|
+
}
|
|
1276
|
+
info(`Attach: ppg attach ${wtId}`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
async function spawnIntoExistingWorktree(projectRoot, config, agentConfig, worktreeRef, promptText, count, options) {
|
|
1280
|
+
const manifest = await readManifest(projectRoot);
|
|
1281
|
+
const wt = resolveWorktree(manifest, worktreeRef);
|
|
1282
|
+
if (!wt) throw new WorktreeNotFoundError(worktreeRef);
|
|
1283
|
+
let windowTarget = wt.tmuxWindow;
|
|
1284
|
+
if (!windowTarget) {
|
|
1285
|
+
await ensureSession(manifest.sessionName);
|
|
1286
|
+
windowTarget = await createWindow(manifest.sessionName, wt.name, wt.path);
|
|
1287
|
+
}
|
|
1288
|
+
const agents = [];
|
|
1289
|
+
for (let i = 0; i < count; i++) {
|
|
1290
|
+
const aId = agentId();
|
|
1291
|
+
let target;
|
|
1292
|
+
if (i === 0 && options.split) {
|
|
1293
|
+
target = windowTarget;
|
|
1294
|
+
} else if (options.split) {
|
|
1295
|
+
const direction = i % 2 === 1 ? "horizontal" : "vertical";
|
|
1296
|
+
const pane = await splitPane(windowTarget, direction, wt.path);
|
|
1297
|
+
target = pane.target;
|
|
1298
|
+
} else {
|
|
1299
|
+
target = await createWindow(manifest.sessionName, `${wt.name}-agent-${i}`, wt.path);
|
|
1300
|
+
}
|
|
1301
|
+
const ctx = {
|
|
1302
|
+
WORKTREE_PATH: wt.path,
|
|
1303
|
+
BRANCH: wt.branch,
|
|
1304
|
+
AGENT_ID: aId,
|
|
1305
|
+
RESULT_FILE: resultFile(projectRoot, aId),
|
|
1306
|
+
PROJECT_ROOT: projectRoot,
|
|
1307
|
+
TASK_NAME: wt.name,
|
|
1308
|
+
PROMPT: promptText
|
|
1309
|
+
};
|
|
1310
|
+
for (const v of options.var ?? []) {
|
|
1311
|
+
const [key, ...rest] = v.split("=");
|
|
1312
|
+
ctx[key] = rest.join("=");
|
|
1313
|
+
}
|
|
1314
|
+
const renderedPrompt = renderTemplate(promptText, ctx);
|
|
1315
|
+
const agentEntry = await spawnAgent({
|
|
1316
|
+
agentId: aId,
|
|
1317
|
+
agentConfig,
|
|
1318
|
+
prompt: renderedPrompt,
|
|
1319
|
+
worktreePath: wt.path,
|
|
1320
|
+
tmuxTarget: target,
|
|
1321
|
+
projectRoot,
|
|
1322
|
+
branch: wt.branch,
|
|
1323
|
+
sessionId: sessionId()
|
|
1324
|
+
});
|
|
1325
|
+
agents.push(agentEntry);
|
|
1326
|
+
}
|
|
1327
|
+
await updateManifest(projectRoot, (m) => {
|
|
1328
|
+
const mWt = m.worktrees[wt.id];
|
|
1329
|
+
if (!mWt.tmuxWindow) {
|
|
1330
|
+
mWt.tmuxWindow = windowTarget;
|
|
1331
|
+
}
|
|
1332
|
+
for (const a of agents) {
|
|
1333
|
+
mWt.agents[a.id] = a;
|
|
1334
|
+
}
|
|
1335
|
+
return m;
|
|
1336
|
+
});
|
|
1337
|
+
if (options.open !== false) {
|
|
1338
|
+
openTerminalWindow(manifest.sessionName, windowTarget, wt.name).catch(() => {
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
if (options.json) {
|
|
1342
|
+
output({
|
|
1343
|
+
success: true,
|
|
1344
|
+
worktree: {
|
|
1345
|
+
id: wt.id,
|
|
1346
|
+
name: wt.name,
|
|
1347
|
+
branch: wt.branch,
|
|
1348
|
+
path: wt.path,
|
|
1349
|
+
tmuxWindow: windowTarget
|
|
1350
|
+
},
|
|
1351
|
+
agents: agents.map((a) => ({ id: a.id, tmuxTarget: a.tmuxTarget, sessionId: a.sessionId }))
|
|
1352
|
+
}, true);
|
|
1353
|
+
} else {
|
|
1354
|
+
success(`Added ${agents.length} agent(s) to worktree ${wt.id}`);
|
|
1355
|
+
for (const a of agents) {
|
|
1356
|
+
info(` Agent ${a.id} \u2192 ${a.tmuxTarget}`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
var init_spawn = __esm({
|
|
1361
|
+
"src/commands/spawn.ts"() {
|
|
1362
|
+
"use strict";
|
|
1363
|
+
init_config();
|
|
1364
|
+
init_manifest();
|
|
1365
|
+
init_worktree();
|
|
1366
|
+
init_env();
|
|
1367
|
+
init_template();
|
|
1368
|
+
init_agent();
|
|
1369
|
+
init_tmux();
|
|
1370
|
+
init_terminal();
|
|
1371
|
+
init_id();
|
|
1372
|
+
init_paths();
|
|
1373
|
+
init_errors();
|
|
1374
|
+
init_output();
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
// src/commands/status.ts
|
|
1379
|
+
var status_exports = {};
|
|
1380
|
+
__export(status_exports, {
|
|
1381
|
+
statusCommand: () => statusCommand
|
|
1382
|
+
});
|
|
1383
|
+
async function statusCommand(worktreeFilter, options) {
|
|
1384
|
+
const projectRoot = await getRepoRoot();
|
|
1385
|
+
let manifest;
|
|
1386
|
+
try {
|
|
1387
|
+
manifest = await updateManifest(projectRoot, async (m) => {
|
|
1388
|
+
return refreshAllAgentStatuses(m, projectRoot);
|
|
1389
|
+
});
|
|
1390
|
+
} catch {
|
|
1391
|
+
throw new NotInitializedError(projectRoot);
|
|
1392
|
+
}
|
|
1393
|
+
const filter = worktreeFilter ?? options?.worktree;
|
|
1394
|
+
let worktrees = Object.values(manifest.worktrees);
|
|
1395
|
+
if (filter) {
|
|
1396
|
+
worktrees = worktrees.filter(
|
|
1397
|
+
(wt) => wt.id === filter || wt.name === filter || wt.branch === filter
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
if (options?.json) {
|
|
1401
|
+
output({
|
|
1402
|
+
session: manifest.sessionName,
|
|
1403
|
+
worktrees: Object.fromEntries(worktrees.map((wt) => [wt.id, wt]))
|
|
1404
|
+
}, true);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (worktrees.length === 0) {
|
|
1408
|
+
console.log("No active worktrees. Use `ppg spawn` to create one.");
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
for (const wt of worktrees) {
|
|
1412
|
+
printWorktreeStatus(wt);
|
|
1413
|
+
}
|
|
1414
|
+
if (options?.watch) {
|
|
1415
|
+
const interval = setInterval(async () => {
|
|
1416
|
+
console.clear();
|
|
1417
|
+
try {
|
|
1418
|
+
const m = await updateManifest(projectRoot, async (m2) => {
|
|
1419
|
+
return refreshAllAgentStatuses(m2, projectRoot);
|
|
1420
|
+
});
|
|
1421
|
+
let wts = Object.values(m.worktrees);
|
|
1422
|
+
if (filter) {
|
|
1423
|
+
wts = wts.filter(
|
|
1424
|
+
(wt) => wt.id === filter || wt.name === filter || wt.branch === filter
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
for (const wt of wts) {
|
|
1428
|
+
printWorktreeStatus(wt);
|
|
1429
|
+
}
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
console.error("Error refreshing status:", err);
|
|
1432
|
+
}
|
|
1433
|
+
}, 2e3);
|
|
1434
|
+
process.on("SIGINT", () => {
|
|
1435
|
+
clearInterval(interval);
|
|
1436
|
+
process.exit(0);
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
function printWorktreeStatus(wt) {
|
|
1441
|
+
const agents = Object.values(wt.agents);
|
|
1442
|
+
const statusCounts = {
|
|
1443
|
+
running: agents.filter((a) => a.status === "running").length,
|
|
1444
|
+
completed: agents.filter((a) => a.status === "completed").length,
|
|
1445
|
+
failed: agents.filter((a) => a.status === "failed").length,
|
|
1446
|
+
lost: agents.filter((a) => a.status === "lost").length
|
|
1447
|
+
};
|
|
1448
|
+
console.log(
|
|
1449
|
+
`
|
|
1450
|
+
${wt.name} (${wt.id}) [${formatStatus(wt.status)}] branch:${wt.branch}`
|
|
1451
|
+
);
|
|
1452
|
+
if (agents.length === 0) {
|
|
1453
|
+
console.log(" No agents");
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const columns = [
|
|
1457
|
+
{ header: "Agent", key: "id", width: 14 },
|
|
1458
|
+
{ header: "Type", key: "agentType", width: 10 },
|
|
1459
|
+
{
|
|
1460
|
+
header: "Status",
|
|
1461
|
+
key: "status",
|
|
1462
|
+
width: 12,
|
|
1463
|
+
format: (v) => formatStatus(v)
|
|
1464
|
+
},
|
|
1465
|
+
{ header: "Started", key: "startedAt", width: 20, format: (v) => formatTime(v) },
|
|
1466
|
+
{ header: "Pane", key: "tmuxTarget", width: 20 }
|
|
1467
|
+
];
|
|
1468
|
+
const table = formatTable(agents, columns);
|
|
1469
|
+
console.log(table.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
1470
|
+
}
|
|
1471
|
+
function formatTime(iso) {
|
|
1472
|
+
if (!iso) return "\u2014";
|
|
1473
|
+
const d = new Date(iso);
|
|
1474
|
+
const now = /* @__PURE__ */ new Date();
|
|
1475
|
+
const diffMs = now.getTime() - d.getTime();
|
|
1476
|
+
const diffMin = Math.floor(diffMs / 6e4);
|
|
1477
|
+
if (diffMin < 1) return "just now";
|
|
1478
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
1479
|
+
const diffHr = Math.floor(diffMin / 60);
|
|
1480
|
+
return `${diffHr}h ${diffMin % 60}m ago`;
|
|
1481
|
+
}
|
|
1482
|
+
var init_status = __esm({
|
|
1483
|
+
"src/commands/status.ts"() {
|
|
1484
|
+
"use strict";
|
|
1485
|
+
init_manifest();
|
|
1486
|
+
init_agent();
|
|
1487
|
+
init_worktree();
|
|
1488
|
+
init_errors();
|
|
1489
|
+
init_output();
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
// src/core/cleanup.ts
|
|
1494
|
+
async function cleanupWorktree(projectRoot, wt) {
|
|
1495
|
+
const windowKills = [];
|
|
1496
|
+
for (const agent of Object.values(wt.agents)) {
|
|
1497
|
+
if (agent.tmuxTarget) {
|
|
1498
|
+
windowKills.push(killWindow(agent.tmuxTarget).catch(() => {
|
|
1499
|
+
}));
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
if (wt.tmuxWindow) {
|
|
1503
|
+
windowKills.push(killWindow(wt.tmuxWindow).catch(() => {
|
|
1504
|
+
}));
|
|
1505
|
+
}
|
|
1506
|
+
await Promise.all(windowKills);
|
|
1507
|
+
try {
|
|
1508
|
+
await teardownWorktreeEnv(wt.path);
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1511
|
+
try {
|
|
1512
|
+
await removeWorktree(projectRoot, wt.path, {
|
|
1513
|
+
force: true,
|
|
1514
|
+
deleteBranch: true,
|
|
1515
|
+
branchName: wt.branch
|
|
1516
|
+
});
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
warn(`Could not fully remove worktree ${wt.id}: ${err instanceof Error ? err.message : err}`);
|
|
1519
|
+
}
|
|
1520
|
+
await updateManifest(projectRoot, (m) => {
|
|
1521
|
+
if (m.worktrees[wt.id]) {
|
|
1522
|
+
m.worktrees[wt.id].status = "cleaned";
|
|
1523
|
+
}
|
|
1524
|
+
return m;
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
var init_cleanup = __esm({
|
|
1528
|
+
"src/core/cleanup.ts"() {
|
|
1529
|
+
"use strict";
|
|
1530
|
+
init_manifest();
|
|
1531
|
+
init_worktree();
|
|
1532
|
+
init_env();
|
|
1533
|
+
init_tmux();
|
|
1534
|
+
init_output();
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
// src/commands/kill.ts
|
|
1539
|
+
var kill_exports = {};
|
|
1540
|
+
__export(kill_exports, {
|
|
1541
|
+
killCommand: () => killCommand
|
|
1542
|
+
});
|
|
1543
|
+
async function killCommand(options) {
|
|
1544
|
+
const projectRoot = await getRepoRoot();
|
|
1545
|
+
if (!options.agent && !options.worktree && !options.all) {
|
|
1546
|
+
throw new PgError("One of --agent, --worktree, or --all is required", "INVALID_ARGS");
|
|
1547
|
+
}
|
|
1548
|
+
if (options.agent) {
|
|
1549
|
+
await killSingleAgent(projectRoot, options.agent, options);
|
|
1550
|
+
} else if (options.worktree) {
|
|
1551
|
+
await killWorktreeAgents(projectRoot, options.worktree, options);
|
|
1552
|
+
} else if (options.all) {
|
|
1553
|
+
await killAllAgents(projectRoot, options);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
async function killSingleAgent(projectRoot, agentId2, options) {
|
|
1557
|
+
const manifest = await readManifest(projectRoot);
|
|
1558
|
+
const found = findAgent(manifest, agentId2);
|
|
1559
|
+
if (!found) throw new AgentNotFoundError(agentId2);
|
|
1560
|
+
const { agent } = found;
|
|
1561
|
+
const isTerminal = ["completed", "failed", "killed", "lost"].includes(agent.status);
|
|
1562
|
+
if (options.delete) {
|
|
1563
|
+
if (!isTerminal) {
|
|
1564
|
+
info(`Killing agent ${agentId2}`);
|
|
1565
|
+
await killAgent(agent);
|
|
1566
|
+
}
|
|
1567
|
+
await Promise.resolve().then(() => (init_tmux(), tmux_exports)).then((tmux) => tmux.killPane(agent.tmuxTarget));
|
|
1568
|
+
await updateManifest(projectRoot, (m) => {
|
|
1569
|
+
const f = findAgent(m, agentId2);
|
|
1570
|
+
if (f) {
|
|
1571
|
+
delete f.worktree.agents[agentId2];
|
|
1572
|
+
}
|
|
1573
|
+
return m;
|
|
1574
|
+
});
|
|
1575
|
+
if (options.json) {
|
|
1576
|
+
output({ success: true, killed: [agentId2], deleted: [agentId2] }, true);
|
|
1577
|
+
} else {
|
|
1578
|
+
success(`Deleted agent ${agentId2}`);
|
|
1579
|
+
}
|
|
1580
|
+
} else {
|
|
1581
|
+
info(`Killing agent ${agentId2}`);
|
|
1582
|
+
await killAgent(agent);
|
|
1583
|
+
await updateManifest(projectRoot, (m) => {
|
|
1584
|
+
const f = findAgent(m, agentId2);
|
|
1585
|
+
if (f) {
|
|
1586
|
+
f.agent.status = "killed";
|
|
1587
|
+
f.agent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1588
|
+
}
|
|
1589
|
+
return m;
|
|
1590
|
+
});
|
|
1591
|
+
if (options.json) {
|
|
1592
|
+
output({ success: true, killed: [agentId2] }, true);
|
|
1593
|
+
} else {
|
|
1594
|
+
success(`Killed agent ${agentId2}`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
async function killWorktreeAgents(projectRoot, worktreeRef, options) {
|
|
1599
|
+
const manifest = await readManifest(projectRoot);
|
|
1600
|
+
const wt = resolveWorktree(manifest, worktreeRef);
|
|
1601
|
+
if (!wt) throw new WorktreeNotFoundError(worktreeRef);
|
|
1602
|
+
const toKill = Object.values(wt.agents).filter((a) => ["running", "spawning", "waiting"].includes(a.status));
|
|
1603
|
+
const killedIds = toKill.map((a) => a.id);
|
|
1604
|
+
for (const a of toKill) info(`Killing agent ${a.id}`);
|
|
1605
|
+
await killAgents(toKill);
|
|
1606
|
+
await updateManifest(projectRoot, (m) => {
|
|
1607
|
+
const mWt = m.worktrees[wt.id];
|
|
1608
|
+
if (mWt) {
|
|
1609
|
+
for (const agent of Object.values(mWt.agents)) {
|
|
1610
|
+
if (killedIds.includes(agent.id)) {
|
|
1611
|
+
agent.status = "killed";
|
|
1612
|
+
agent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return m;
|
|
1617
|
+
});
|
|
1618
|
+
const shouldRemove = options.remove || options.delete;
|
|
1619
|
+
if (shouldRemove) {
|
|
1620
|
+
await removeWorktreeCleanup(projectRoot, wt.id);
|
|
1621
|
+
}
|
|
1622
|
+
if (options.delete) {
|
|
1623
|
+
await updateManifest(projectRoot, (m) => {
|
|
1624
|
+
delete m.worktrees[wt.id];
|
|
1625
|
+
return m;
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
if (options.json) {
|
|
1629
|
+
output({
|
|
1630
|
+
success: true,
|
|
1631
|
+
killed: killedIds,
|
|
1632
|
+
removed: shouldRemove ? [wt.id] : [],
|
|
1633
|
+
deleted: options.delete ? [wt.id] : []
|
|
1634
|
+
}, true);
|
|
1635
|
+
} else {
|
|
1636
|
+
success(`Killed ${killedIds.length} agent(s) in worktree ${wt.id}`);
|
|
1637
|
+
if (options.delete) {
|
|
1638
|
+
success(`Deleted worktree ${wt.id}`);
|
|
1639
|
+
} else if (options.remove) {
|
|
1640
|
+
success(`Removed worktree ${wt.id}`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function killAllAgents(projectRoot, options) {
|
|
1645
|
+
const manifest = await readManifest(projectRoot);
|
|
1646
|
+
const toKill = [];
|
|
1647
|
+
for (const wt of Object.values(manifest.worktrees)) {
|
|
1648
|
+
for (const agent of Object.values(wt.agents)) {
|
|
1649
|
+
if (["running", "spawning", "waiting"].includes(agent.status)) {
|
|
1650
|
+
toKill.push(agent);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const killedIds = toKill.map((a) => a.id);
|
|
1655
|
+
for (const a of toKill) info(`Killing agent ${a.id}`);
|
|
1656
|
+
await killAgents(toKill);
|
|
1657
|
+
const activeWorktreeIds = Object.values(manifest.worktrees).filter((wt) => wt.status === "active").map((wt) => wt.id);
|
|
1658
|
+
await updateManifest(projectRoot, (m) => {
|
|
1659
|
+
for (const wt of Object.values(m.worktrees)) {
|
|
1660
|
+
for (const agent of Object.values(wt.agents)) {
|
|
1661
|
+
if (killedIds.includes(agent.id)) {
|
|
1662
|
+
agent.status = "killed";
|
|
1663
|
+
agent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return m;
|
|
1668
|
+
});
|
|
1669
|
+
const shouldRemove = options.remove || options.delete;
|
|
1670
|
+
if (shouldRemove) {
|
|
1671
|
+
for (const wtId of activeWorktreeIds) {
|
|
1672
|
+
await removeWorktreeCleanup(projectRoot, wtId);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
if (options.delete) {
|
|
1676
|
+
await updateManifest(projectRoot, (m) => {
|
|
1677
|
+
for (const wtId of activeWorktreeIds) {
|
|
1678
|
+
delete m.worktrees[wtId];
|
|
1679
|
+
}
|
|
1680
|
+
return m;
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
if (options.json) {
|
|
1684
|
+
output({
|
|
1685
|
+
success: true,
|
|
1686
|
+
killed: killedIds,
|
|
1687
|
+
removed: shouldRemove ? activeWorktreeIds : [],
|
|
1688
|
+
deleted: options.delete ? activeWorktreeIds : []
|
|
1689
|
+
}, true);
|
|
1690
|
+
} else {
|
|
1691
|
+
success(`Killed ${killedIds.length} agent(s) across ${activeWorktreeIds.length} worktree(s)`);
|
|
1692
|
+
if (options.delete) {
|
|
1693
|
+
success(`Deleted ${activeWorktreeIds.length} worktree(s)`);
|
|
1694
|
+
} else if (options.remove) {
|
|
1695
|
+
success(`Removed ${activeWorktreeIds.length} worktree(s)`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
async function removeWorktreeCleanup(projectRoot, wtId) {
|
|
1700
|
+
const manifest = await readManifest(projectRoot);
|
|
1701
|
+
const wt = resolveWorktree(manifest, wtId);
|
|
1702
|
+
if (!wt) return;
|
|
1703
|
+
await cleanupWorktree(projectRoot, wt);
|
|
1704
|
+
}
|
|
1705
|
+
var init_kill = __esm({
|
|
1706
|
+
"src/commands/kill.ts"() {
|
|
1707
|
+
"use strict";
|
|
1708
|
+
init_manifest();
|
|
1709
|
+
init_agent();
|
|
1710
|
+
init_worktree();
|
|
1711
|
+
init_cleanup();
|
|
1712
|
+
init_errors();
|
|
1713
|
+
init_output();
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
// src/commands/attach.ts
|
|
1718
|
+
var attach_exports = {};
|
|
1719
|
+
__export(attach_exports, {
|
|
1720
|
+
attachCommand: () => attachCommand
|
|
1721
|
+
});
|
|
1722
|
+
async function attachCommand(target) {
|
|
1723
|
+
const projectRoot = await getRepoRoot();
|
|
1724
|
+
let manifest;
|
|
1725
|
+
try {
|
|
1726
|
+
manifest = await readManifest(projectRoot);
|
|
1727
|
+
} catch {
|
|
1728
|
+
throw new NotInitializedError(projectRoot);
|
|
1729
|
+
}
|
|
1730
|
+
let tmuxTarget;
|
|
1731
|
+
const sessionName = manifest.sessionName;
|
|
1732
|
+
let agent;
|
|
1733
|
+
let worktreeId2;
|
|
1734
|
+
const wt = resolveWorktree(manifest, target);
|
|
1735
|
+
if (wt) {
|
|
1736
|
+
if (!wt.tmuxWindow) {
|
|
1737
|
+
throw new PgError("Worktree has no tmux window. Spawn agents first with: ppg spawn --worktree " + wt.id + ' --prompt "your task"', "NO_TMUX_WINDOW");
|
|
1738
|
+
}
|
|
1739
|
+
tmuxTarget = wt.tmuxWindow;
|
|
1740
|
+
} else {
|
|
1741
|
+
const found = findAgent(manifest, target);
|
|
1742
|
+
if (found) {
|
|
1743
|
+
agent = found.agent;
|
|
1744
|
+
worktreeId2 = found.worktree.id;
|
|
1745
|
+
tmuxTarget = found.agent.tmuxTarget;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (!tmuxTarget) {
|
|
1749
|
+
throw new PgError(`Could not resolve target: ${target}. Try a worktree ID, name, or agent ID.`, "TARGET_NOT_FOUND");
|
|
1750
|
+
}
|
|
1751
|
+
if (agent?.sessionId && worktreeId2) {
|
|
1752
|
+
const paneInfo = await getPaneInfo(tmuxTarget);
|
|
1753
|
+
if (!paneInfo || paneInfo.isDead) {
|
|
1754
|
+
info(`Pane is dead. Resuming session ${agent.sessionId}...`);
|
|
1755
|
+
const resumeWt = manifest.worktrees[worktreeId2];
|
|
1756
|
+
const cwd = resumeWt?.path ?? projectRoot;
|
|
1757
|
+
const windowName = resumeWt?.name ?? agent.name ?? target;
|
|
1758
|
+
tmuxTarget = await resumeAgent({
|
|
1759
|
+
agent,
|
|
1760
|
+
worktreeId: worktreeId2,
|
|
1761
|
+
sessionName,
|
|
1762
|
+
cwd,
|
|
1763
|
+
windowName,
|
|
1764
|
+
projectRoot
|
|
1765
|
+
});
|
|
1766
|
+
success(`Resumed agent ${agent.id} in ${tmuxTarget}`);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
const insideTmux = await isInsideTmux();
|
|
1770
|
+
if (insideTmux) {
|
|
1771
|
+
await selectWindow(tmuxTarget);
|
|
1772
|
+
info(`Switched to ${tmuxTarget}`);
|
|
1773
|
+
} else {
|
|
1774
|
+
const title = wt ? wt.name : target;
|
|
1775
|
+
info(`Opening Terminal window for ${title}`);
|
|
1776
|
+
await openTerminalWindow(sessionName, tmuxTarget, title);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
var init_attach = __esm({
|
|
1780
|
+
"src/commands/attach.ts"() {
|
|
1781
|
+
"use strict";
|
|
1782
|
+
init_manifest();
|
|
1783
|
+
init_worktree();
|
|
1784
|
+
init_tmux();
|
|
1785
|
+
init_agent();
|
|
1786
|
+
init_tmux();
|
|
1787
|
+
init_terminal();
|
|
1788
|
+
init_errors();
|
|
1789
|
+
init_output();
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
// src/commands/logs.ts
|
|
1794
|
+
var logs_exports = {};
|
|
1795
|
+
__export(logs_exports, {
|
|
1796
|
+
logsCommand: () => logsCommand
|
|
1797
|
+
});
|
|
1798
|
+
async function logsCommand(agentId2, options) {
|
|
1799
|
+
const projectRoot = await getRepoRoot();
|
|
1800
|
+
let manifest;
|
|
1801
|
+
try {
|
|
1802
|
+
manifest = await readManifest(projectRoot);
|
|
1803
|
+
} catch {
|
|
1804
|
+
throw new NotInitializedError(projectRoot);
|
|
1805
|
+
}
|
|
1806
|
+
const found = findAgent(manifest, agentId2);
|
|
1807
|
+
if (!found) throw new AgentNotFoundError(agentId2);
|
|
1808
|
+
const { agent } = found;
|
|
1809
|
+
const lines = options.full ? void 0 : options.lines ?? 100;
|
|
1810
|
+
if (options.follow) {
|
|
1811
|
+
let lastOutput = "";
|
|
1812
|
+
const interval = setInterval(async () => {
|
|
1813
|
+
try {
|
|
1814
|
+
const content = await capturePane(agent.tmuxTarget, lines);
|
|
1815
|
+
if (content !== lastOutput) {
|
|
1816
|
+
if (lastOutput) {
|
|
1817
|
+
const oldLines = lastOutput.split("\n");
|
|
1818
|
+
const newLines = content.split("\n");
|
|
1819
|
+
const diff = newLines.slice(oldLines.length);
|
|
1820
|
+
if (diff.length > 0) {
|
|
1821
|
+
process.stdout.write(diff.join("\n") + "\n");
|
|
1822
|
+
}
|
|
1823
|
+
} else {
|
|
1824
|
+
process.stdout.write(content + "\n");
|
|
1825
|
+
}
|
|
1826
|
+
lastOutput = content;
|
|
1827
|
+
}
|
|
1828
|
+
} catch {
|
|
1829
|
+
clearInterval(interval);
|
|
1830
|
+
outputError(new Error("Pane no longer available"), options.json ?? false);
|
|
1831
|
+
process.exit(1);
|
|
1832
|
+
}
|
|
1833
|
+
}, 1e3);
|
|
1834
|
+
process.on("SIGINT", () => {
|
|
1835
|
+
clearInterval(interval);
|
|
1836
|
+
process.exit(0);
|
|
1837
|
+
});
|
|
1838
|
+
} else {
|
|
1839
|
+
try {
|
|
1840
|
+
const content = await capturePane(agent.tmuxTarget, lines);
|
|
1841
|
+
if (options.json) {
|
|
1842
|
+
output({
|
|
1843
|
+
agentId: agent.id,
|
|
1844
|
+
status: agent.status,
|
|
1845
|
+
tmuxTarget: agent.tmuxTarget,
|
|
1846
|
+
output: content
|
|
1847
|
+
}, true);
|
|
1848
|
+
} else {
|
|
1849
|
+
console.log(content);
|
|
1850
|
+
}
|
|
1851
|
+
} catch {
|
|
1852
|
+
throw new PgError(`Could not capture pane for agent ${agentId2}. Pane may no longer exist.`, "PANE_NOT_FOUND");
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
var init_logs = __esm({
|
|
1857
|
+
"src/commands/logs.ts"() {
|
|
1858
|
+
"use strict";
|
|
1859
|
+
init_manifest();
|
|
1860
|
+
init_worktree();
|
|
1861
|
+
init_tmux();
|
|
1862
|
+
init_errors();
|
|
1863
|
+
init_output();
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
// src/commands/aggregate.ts
|
|
1868
|
+
var aggregate_exports = {};
|
|
1869
|
+
__export(aggregate_exports, {
|
|
1870
|
+
aggregateCommand: () => aggregateCommand
|
|
1871
|
+
});
|
|
1872
|
+
import fs8 from "fs/promises";
|
|
1873
|
+
async function aggregateCommand(worktreeId2, options) {
|
|
1874
|
+
const projectRoot = await getRepoRoot();
|
|
1875
|
+
let manifest;
|
|
1876
|
+
try {
|
|
1877
|
+
manifest = await updateManifest(projectRoot, async (m) => {
|
|
1878
|
+
return refreshAllAgentStatuses(m, projectRoot);
|
|
1879
|
+
});
|
|
1880
|
+
} catch {
|
|
1881
|
+
throw new NotInitializedError(projectRoot);
|
|
1882
|
+
}
|
|
1883
|
+
let worktrees;
|
|
1884
|
+
if (options?.all) {
|
|
1885
|
+
worktrees = Object.values(manifest.worktrees);
|
|
1886
|
+
} else if (worktreeId2) {
|
|
1887
|
+
const wt = resolveWorktree(manifest, worktreeId2);
|
|
1888
|
+
if (!wt) throw new WorktreeNotFoundError(worktreeId2);
|
|
1889
|
+
worktrees = [wt];
|
|
1890
|
+
} else {
|
|
1891
|
+
worktrees = Object.values(manifest.worktrees).filter(
|
|
1892
|
+
(wt) => Object.values(wt.agents).some((a) => a.status === "completed")
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
const results = [];
|
|
1896
|
+
for (const wt of worktrees) {
|
|
1897
|
+
for (const agent of Object.values(wt.agents)) {
|
|
1898
|
+
if (agent.status !== "completed" && agent.status !== "failed") continue;
|
|
1899
|
+
const result = await collectAgentResult(agent, projectRoot);
|
|
1900
|
+
results.push({
|
|
1901
|
+
agentId: agent.id,
|
|
1902
|
+
worktreeId: wt.id,
|
|
1903
|
+
worktreeName: wt.name,
|
|
1904
|
+
branch: wt.branch,
|
|
1905
|
+
status: agent.status,
|
|
1906
|
+
content: result
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
if (results.length === 0) {
|
|
1911
|
+
if (options?.json) {
|
|
1912
|
+
output({ results: [] }, true);
|
|
1913
|
+
} else {
|
|
1914
|
+
console.log("No completed agent results to aggregate.");
|
|
1915
|
+
}
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
if (options?.json) {
|
|
1919
|
+
output({ results }, true);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
const combined = results.map((r) => {
|
|
1923
|
+
return [
|
|
1924
|
+
`# Agent: ${r.agentId}`,
|
|
1925
|
+
`**Worktree:** ${r.worktreeName} (${r.worktreeId})`,
|
|
1926
|
+
`**Branch:** ${r.branch}`,
|
|
1927
|
+
`**Status:** ${r.status}`,
|
|
1928
|
+
"",
|
|
1929
|
+
r.content,
|
|
1930
|
+
"",
|
|
1931
|
+
"---",
|
|
1932
|
+
""
|
|
1933
|
+
].join("\n");
|
|
1934
|
+
}).join("\n");
|
|
1935
|
+
if (options?.output) {
|
|
1936
|
+
await fs8.writeFile(options.output, combined, "utf-8");
|
|
1937
|
+
success(`Wrote ${results.length} result(s) to ${options.output}`);
|
|
1938
|
+
} else {
|
|
1939
|
+
console.log(combined);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
async function collectAgentResult(agent, projectRoot) {
|
|
1943
|
+
try {
|
|
1944
|
+
const content = await fs8.readFile(agent.resultFile, "utf-8");
|
|
1945
|
+
return content;
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
try {
|
|
1949
|
+
const paneContent = await capturePane(agent.tmuxTarget, 500);
|
|
1950
|
+
return `*[No result file \u2014 pane capture fallback]*
|
|
1951
|
+
|
|
1952
|
+
\`\`\`
|
|
1953
|
+
${paneContent}
|
|
1954
|
+
\`\`\``;
|
|
1955
|
+
} catch {
|
|
1956
|
+
return "*[No result file and pane not available]*";
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
var init_aggregate = __esm({
|
|
1960
|
+
"src/commands/aggregate.ts"() {
|
|
1961
|
+
"use strict";
|
|
1962
|
+
init_manifest();
|
|
1963
|
+
init_agent();
|
|
1964
|
+
init_worktree();
|
|
1965
|
+
init_tmux();
|
|
1966
|
+
init_errors();
|
|
1967
|
+
init_output();
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// src/commands/merge.ts
|
|
1972
|
+
var merge_exports = {};
|
|
1973
|
+
__export(merge_exports, {
|
|
1974
|
+
mergeCommand: () => mergeCommand
|
|
1975
|
+
});
|
|
1976
|
+
import { execa as execa5 } from "execa";
|
|
1977
|
+
async function mergeCommand(worktreeId2, options) {
|
|
1978
|
+
const projectRoot = await getRepoRoot();
|
|
1979
|
+
let manifest;
|
|
1980
|
+
try {
|
|
1981
|
+
manifest = await updateManifest(projectRoot, async (m) => {
|
|
1982
|
+
return refreshAllAgentStatuses(m, projectRoot);
|
|
1983
|
+
});
|
|
1984
|
+
} catch {
|
|
1985
|
+
throw new NotInitializedError(projectRoot);
|
|
1986
|
+
}
|
|
1987
|
+
const wt = resolveWorktree(manifest, worktreeId2);
|
|
1988
|
+
if (!wt) throw new WorktreeNotFoundError(worktreeId2);
|
|
1989
|
+
const agents = Object.values(wt.agents);
|
|
1990
|
+
const incomplete = agents.filter((a) => !["completed", "failed", "killed"].includes(a.status));
|
|
1991
|
+
if (incomplete.length > 0 && !options.force) {
|
|
1992
|
+
const ids = incomplete.map((a) => a.id).join(", ");
|
|
1993
|
+
throw new PgError(
|
|
1994
|
+
`${incomplete.length} agent(s) still running: ${ids}. Use --force to merge anyway.`,
|
|
1995
|
+
"AGENTS_RUNNING"
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
if (options.dryRun) {
|
|
1999
|
+
info("Dry run \u2014 no changes will be made");
|
|
2000
|
+
info(`Would merge branch ${wt.branch} into ${wt.baseBranch} using ${options.strategy ?? "squash"} strategy`);
|
|
2001
|
+
if (options.cleanup !== false) {
|
|
2002
|
+
info(`Would remove worktree ${wt.id} and delete branch ${wt.branch}`);
|
|
2003
|
+
}
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
await updateManifest(projectRoot, (m) => {
|
|
2007
|
+
if (m.worktrees[wt.id]) {
|
|
2008
|
+
m.worktrees[wt.id].status = "merging";
|
|
2009
|
+
}
|
|
2010
|
+
return m;
|
|
2011
|
+
});
|
|
2012
|
+
const strategy = options.strategy ?? "squash";
|
|
2013
|
+
try {
|
|
2014
|
+
info(`Merging ${wt.branch} into ${wt.baseBranch} (${strategy})`);
|
|
2015
|
+
if (strategy === "squash") {
|
|
2016
|
+
await execa5("git", ["merge", "--squash", wt.branch], { cwd: projectRoot });
|
|
2017
|
+
await execa5("git", ["commit", "-m", `ppg: merge ${wt.name} (${wt.branch})`], {
|
|
2018
|
+
cwd: projectRoot
|
|
2019
|
+
});
|
|
2020
|
+
} else {
|
|
2021
|
+
await execa5("git", ["merge", "--no-ff", wt.branch, "-m", `ppg: merge ${wt.name} (${wt.branch})`], {
|
|
2022
|
+
cwd: projectRoot
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
success(`Merged ${wt.branch} into ${wt.baseBranch}`);
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
await updateManifest(projectRoot, (m) => {
|
|
2028
|
+
if (m.worktrees[wt.id]) {
|
|
2029
|
+
m.worktrees[wt.id].status = "failed";
|
|
2030
|
+
}
|
|
2031
|
+
return m;
|
|
2032
|
+
});
|
|
2033
|
+
throw new MergeFailedError(
|
|
2034
|
+
`Merge failed: ${err instanceof Error ? err.message : err}`
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
await updateManifest(projectRoot, (m) => {
|
|
2038
|
+
if (m.worktrees[wt.id]) {
|
|
2039
|
+
m.worktrees[wt.id].status = "merged";
|
|
2040
|
+
m.worktrees[wt.id].mergedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2041
|
+
}
|
|
2042
|
+
return m;
|
|
2043
|
+
});
|
|
2044
|
+
if (options.cleanup !== false) {
|
|
2045
|
+
info("Cleaning up...");
|
|
2046
|
+
await cleanupWorktree(projectRoot, wt);
|
|
2047
|
+
success(`Cleaned up worktree ${wt.id}`);
|
|
2048
|
+
}
|
|
2049
|
+
if (options.json) {
|
|
2050
|
+
output({
|
|
2051
|
+
success: true,
|
|
2052
|
+
worktreeId: wt.id,
|
|
2053
|
+
branch: wt.branch,
|
|
2054
|
+
baseBranch: wt.baseBranch,
|
|
2055
|
+
strategy,
|
|
2056
|
+
cleaned: options.cleanup !== false
|
|
2057
|
+
}, true);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
var init_merge = __esm({
|
|
2061
|
+
"src/commands/merge.ts"() {
|
|
2062
|
+
"use strict";
|
|
2063
|
+
init_manifest();
|
|
2064
|
+
init_agent();
|
|
2065
|
+
init_worktree();
|
|
2066
|
+
init_cleanup();
|
|
2067
|
+
init_errors();
|
|
2068
|
+
init_output();
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
|
|
2072
|
+
// src/commands/list.ts
|
|
2073
|
+
var list_exports = {};
|
|
2074
|
+
__export(list_exports, {
|
|
2075
|
+
listCommand: () => listCommand
|
|
2076
|
+
});
|
|
2077
|
+
import fs9 from "fs/promises";
|
|
2078
|
+
import path5 from "path";
|
|
2079
|
+
async function listCommand(type, options) {
|
|
2080
|
+
if (type !== "templates") {
|
|
2081
|
+
throw new PgError(`Unknown list type: ${type}. Available: templates`, "INVALID_ARGS");
|
|
2082
|
+
}
|
|
2083
|
+
const projectRoot = await getRepoRoot();
|
|
2084
|
+
const templateNames = await listTemplates(projectRoot);
|
|
2085
|
+
if (templateNames.length === 0) {
|
|
2086
|
+
console.log("No templates found in .pg/templates/");
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
const templates = await Promise.all(
|
|
2090
|
+
templateNames.map(async (name) => {
|
|
2091
|
+
const filePath = path5.join(templatesDir(projectRoot), `${name}.md`);
|
|
2092
|
+
const content = await fs9.readFile(filePath, "utf-8");
|
|
2093
|
+
const firstLine = content.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
2094
|
+
const description = firstLine.replace(/^#+\s*/, "").trim();
|
|
2095
|
+
const vars = [...content.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]);
|
|
2096
|
+
const uniqueVars = [...new Set(vars)];
|
|
2097
|
+
return { name, description, variables: uniqueVars };
|
|
2098
|
+
})
|
|
2099
|
+
);
|
|
2100
|
+
if (options.json) {
|
|
2101
|
+
output({ templates }, true);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
const columns = [
|
|
2105
|
+
{ header: "Name", key: "name", width: 20 },
|
|
2106
|
+
{ header: "Description", key: "description", width: 40 },
|
|
2107
|
+
{
|
|
2108
|
+
header: "Variables",
|
|
2109
|
+
key: "variables",
|
|
2110
|
+
width: 30,
|
|
2111
|
+
format: (v) => v.join(", ")
|
|
2112
|
+
}
|
|
2113
|
+
];
|
|
2114
|
+
console.log(formatTable(templates, columns));
|
|
2115
|
+
}
|
|
2116
|
+
var init_list = __esm({
|
|
2117
|
+
"src/commands/list.ts"() {
|
|
2118
|
+
"use strict";
|
|
2119
|
+
init_worktree();
|
|
2120
|
+
init_template();
|
|
2121
|
+
init_paths();
|
|
2122
|
+
init_errors();
|
|
2123
|
+
init_output();
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
// src/commands/restart.ts
|
|
2128
|
+
var restart_exports = {};
|
|
2129
|
+
__export(restart_exports, {
|
|
2130
|
+
restartCommand: () => restartCommand
|
|
2131
|
+
});
|
|
2132
|
+
import fs10 from "fs/promises";
|
|
2133
|
+
async function restartCommand(agentRef, options) {
|
|
2134
|
+
const projectRoot = await getRepoRoot();
|
|
2135
|
+
const config = await loadConfig(projectRoot);
|
|
2136
|
+
let manifest;
|
|
2137
|
+
try {
|
|
2138
|
+
manifest = await readManifest(projectRoot);
|
|
2139
|
+
} catch {
|
|
2140
|
+
throw new NotInitializedError(projectRoot);
|
|
2141
|
+
}
|
|
2142
|
+
const found = findAgent(manifest, agentRef);
|
|
2143
|
+
if (!found) throw new AgentNotFoundError(agentRef);
|
|
2144
|
+
const { worktree: wt, agent: oldAgent } = found;
|
|
2145
|
+
if (["running", "spawning", "waiting"].includes(oldAgent.status)) {
|
|
2146
|
+
info(`Killing existing agent ${oldAgent.id}`);
|
|
2147
|
+
await killAgent(oldAgent);
|
|
2148
|
+
}
|
|
2149
|
+
let promptText;
|
|
2150
|
+
if (options.prompt) {
|
|
2151
|
+
promptText = options.prompt;
|
|
2152
|
+
} else {
|
|
2153
|
+
const pFile = promptFile(projectRoot, oldAgent.id);
|
|
2154
|
+
try {
|
|
2155
|
+
promptText = await fs10.readFile(pFile, "utf-8");
|
|
2156
|
+
} catch {
|
|
2157
|
+
throw new PgError(
|
|
2158
|
+
`Could not read original prompt for agent ${oldAgent.id}. Use --prompt to provide one.`,
|
|
2159
|
+
"PROMPT_NOT_FOUND"
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
const agentConfig = resolveAgentConfig(config, options.agent ?? oldAgent.agentType);
|
|
2164
|
+
await ensureSession(manifest.sessionName);
|
|
2165
|
+
const newAgentId = agentId();
|
|
2166
|
+
const windowTarget = await createWindow(manifest.sessionName, `${wt.name}-restart`, wt.path);
|
|
2167
|
+
const ctx = {
|
|
2168
|
+
WORKTREE_PATH: wt.path,
|
|
2169
|
+
BRANCH: wt.branch,
|
|
2170
|
+
AGENT_ID: newAgentId,
|
|
2171
|
+
RESULT_FILE: resultFile(projectRoot, newAgentId),
|
|
2172
|
+
PROJECT_ROOT: projectRoot,
|
|
2173
|
+
TASK_NAME: wt.name,
|
|
2174
|
+
PROMPT: promptText
|
|
2175
|
+
};
|
|
2176
|
+
const renderedPrompt = renderTemplate(promptText, ctx);
|
|
2177
|
+
const newSessionId = sessionId();
|
|
2178
|
+
const agentEntry = await spawnAgent({
|
|
2179
|
+
agentId: newAgentId,
|
|
2180
|
+
agentConfig,
|
|
2181
|
+
prompt: renderedPrompt,
|
|
2182
|
+
worktreePath: wt.path,
|
|
2183
|
+
tmuxTarget: windowTarget,
|
|
2184
|
+
projectRoot,
|
|
2185
|
+
branch: wt.branch,
|
|
2186
|
+
sessionId: newSessionId
|
|
2187
|
+
});
|
|
2188
|
+
await updateManifest(projectRoot, (m) => {
|
|
2189
|
+
const mWt = m.worktrees[wt.id];
|
|
2190
|
+
if (mWt) {
|
|
2191
|
+
const mOldAgent = mWt.agents[oldAgent.id];
|
|
2192
|
+
if (mOldAgent && !["completed", "failed", "killed", "lost"].includes(mOldAgent.status)) {
|
|
2193
|
+
mOldAgent.status = "killed";
|
|
2194
|
+
mOldAgent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2195
|
+
}
|
|
2196
|
+
mWt.agents[newAgentId] = agentEntry;
|
|
2197
|
+
}
|
|
2198
|
+
return m;
|
|
2199
|
+
});
|
|
2200
|
+
if (options.open !== false) {
|
|
2201
|
+
openTerminalWindow(manifest.sessionName, windowTarget, `${wt.name}-restart`).catch(() => {
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
if (options.json) {
|
|
2205
|
+
output({
|
|
2206
|
+
success: true,
|
|
2207
|
+
oldAgentId: oldAgent.id,
|
|
2208
|
+
newAgent: {
|
|
2209
|
+
id: newAgentId,
|
|
2210
|
+
tmuxTarget: windowTarget,
|
|
2211
|
+
sessionId: newSessionId,
|
|
2212
|
+
worktreeId: wt.id,
|
|
2213
|
+
worktreeName: wt.name,
|
|
2214
|
+
branch: wt.branch,
|
|
2215
|
+
path: wt.path
|
|
2216
|
+
}
|
|
2217
|
+
}, true);
|
|
2218
|
+
} else {
|
|
2219
|
+
success(`Restarted agent ${oldAgent.id} \u2192 ${newAgentId} in worktree ${wt.name}`);
|
|
2220
|
+
info(` New agent ${newAgentId} \u2192 ${windowTarget}`);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
var init_restart = __esm({
|
|
2224
|
+
"src/commands/restart.ts"() {
|
|
2225
|
+
"use strict";
|
|
2226
|
+
init_manifest();
|
|
2227
|
+
init_config();
|
|
2228
|
+
init_agent();
|
|
2229
|
+
init_worktree();
|
|
2230
|
+
init_tmux();
|
|
2231
|
+
init_terminal();
|
|
2232
|
+
init_id();
|
|
2233
|
+
init_paths();
|
|
2234
|
+
init_errors();
|
|
2235
|
+
init_output();
|
|
2236
|
+
init_template();
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
// src/commands/diff.ts
|
|
2241
|
+
var diff_exports = {};
|
|
2242
|
+
__export(diff_exports, {
|
|
2243
|
+
diffCommand: () => diffCommand
|
|
2244
|
+
});
|
|
2245
|
+
import { execa as execa6 } from "execa";
|
|
2246
|
+
async function diffCommand(worktreeRef, options) {
|
|
2247
|
+
const projectRoot = await getRepoRoot();
|
|
2248
|
+
let manifest;
|
|
2249
|
+
try {
|
|
2250
|
+
manifest = await readManifest(projectRoot);
|
|
2251
|
+
} catch {
|
|
2252
|
+
throw new NotInitializedError(projectRoot);
|
|
2253
|
+
}
|
|
2254
|
+
const wt = resolveWorktree(manifest, worktreeRef);
|
|
2255
|
+
if (!wt) throw new WorktreeNotFoundError(worktreeRef);
|
|
2256
|
+
const diffRange = `${wt.baseBranch}...${wt.branch}`;
|
|
2257
|
+
if (options.json) {
|
|
2258
|
+
const result = await execa6("git", ["diff", "--numstat", diffRange], { cwd: projectRoot });
|
|
2259
|
+
const files = result.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
2260
|
+
const [added, removed, file] = line.split(" ");
|
|
2261
|
+
return {
|
|
2262
|
+
file,
|
|
2263
|
+
added: added === "-" ? 0 : parseInt(added, 10),
|
|
2264
|
+
removed: removed === "-" ? 0 : parseInt(removed, 10)
|
|
2265
|
+
};
|
|
2266
|
+
});
|
|
2267
|
+
output({
|
|
2268
|
+
worktreeId: wt.id,
|
|
2269
|
+
branch: wt.branch,
|
|
2270
|
+
baseBranch: wt.baseBranch,
|
|
2271
|
+
files
|
|
2272
|
+
}, true);
|
|
2273
|
+
} else if (options.stat) {
|
|
2274
|
+
const result = await execa6("git", ["diff", "--stat", diffRange], { cwd: projectRoot });
|
|
2275
|
+
console.log(result.stdout);
|
|
2276
|
+
} else if (options.nameOnly) {
|
|
2277
|
+
const result = await execa6("git", ["diff", "--name-only", diffRange], { cwd: projectRoot });
|
|
2278
|
+
console.log(result.stdout);
|
|
2279
|
+
} else {
|
|
2280
|
+
const result = await execa6("git", ["diff", diffRange], { cwd: projectRoot });
|
|
2281
|
+
console.log(result.stdout);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
var init_diff = __esm({
|
|
2285
|
+
"src/commands/diff.ts"() {
|
|
2286
|
+
"use strict";
|
|
2287
|
+
init_manifest();
|
|
2288
|
+
init_worktree();
|
|
2289
|
+
init_errors();
|
|
2290
|
+
init_output();
|
|
2291
|
+
}
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2294
|
+
// src/commands/clean.ts
|
|
2295
|
+
var clean_exports = {};
|
|
2296
|
+
__export(clean_exports, {
|
|
2297
|
+
cleanCommand: () => cleanCommand
|
|
2298
|
+
});
|
|
2299
|
+
async function cleanCommand(options) {
|
|
2300
|
+
const projectRoot = await getRepoRoot();
|
|
2301
|
+
let manifest;
|
|
2302
|
+
try {
|
|
2303
|
+
manifest = await readManifest(projectRoot);
|
|
2304
|
+
} catch {
|
|
2305
|
+
throw new NotInitializedError(projectRoot);
|
|
2306
|
+
}
|
|
2307
|
+
const terminalStatuses = ["merged", "cleaned"];
|
|
2308
|
+
if (options.all) {
|
|
2309
|
+
terminalStatuses.push("failed");
|
|
2310
|
+
}
|
|
2311
|
+
const toClean = Object.values(manifest.worktrees).filter((wt) => terminalStatuses.includes(wt.status));
|
|
2312
|
+
const toRemoveFromManifest = Object.values(manifest.worktrees).filter((wt) => wt.status === "cleaned");
|
|
2313
|
+
if (options.dryRun) {
|
|
2314
|
+
if (toClean.length === 0 && toRemoveFromManifest.length === 0) {
|
|
2315
|
+
info("Nothing to clean");
|
|
2316
|
+
} else {
|
|
2317
|
+
info("Dry run \u2014 would clean:");
|
|
2318
|
+
for (const wt of toClean) {
|
|
2319
|
+
if (wt.status !== "cleaned") {
|
|
2320
|
+
info(` ${wt.id} (${wt.name}) \u2014 ${wt.status}`);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
for (const wt of toRemoveFromManifest) {
|
|
2324
|
+
info(` ${wt.id} (${wt.name}) \u2014 remove from manifest`);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
if (options.json) {
|
|
2328
|
+
output({
|
|
2329
|
+
dryRun: true,
|
|
2330
|
+
wouldClean: toClean.filter((wt) => wt.status !== "cleaned").map((wt) => ({
|
|
2331
|
+
id: wt.id,
|
|
2332
|
+
name: wt.name,
|
|
2333
|
+
status: wt.status
|
|
2334
|
+
})),
|
|
2335
|
+
wouldRemoveFromManifest: toRemoveFromManifest.map((wt) => ({
|
|
2336
|
+
id: wt.id,
|
|
2337
|
+
name: wt.name
|
|
2338
|
+
}))
|
|
2339
|
+
}, true);
|
|
2340
|
+
}
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
const cleaned = [];
|
|
2344
|
+
const removed = [];
|
|
2345
|
+
for (const wt of toClean) {
|
|
2346
|
+
if (wt.status !== "cleaned") {
|
|
2347
|
+
info(`Cleaning worktree ${wt.id} (${wt.name})`);
|
|
2348
|
+
await cleanupWorktree(projectRoot, wt);
|
|
2349
|
+
cleaned.push(wt.id);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
const allCleanedIds = [.../* @__PURE__ */ new Set([
|
|
2353
|
+
...toRemoveFromManifest.map((wt) => wt.id),
|
|
2354
|
+
...cleaned
|
|
2355
|
+
])];
|
|
2356
|
+
if (allCleanedIds.length > 0) {
|
|
2357
|
+
await updateManifest(projectRoot, (m) => {
|
|
2358
|
+
for (const id of allCleanedIds) {
|
|
2359
|
+
delete m.worktrees[id];
|
|
2360
|
+
}
|
|
2361
|
+
return m;
|
|
2362
|
+
});
|
|
2363
|
+
removed.push(...allCleanedIds);
|
|
2364
|
+
}
|
|
2365
|
+
if (options.prune) {
|
|
2366
|
+
info("Pruning stale git worktrees");
|
|
2367
|
+
await pruneWorktrees(projectRoot);
|
|
2368
|
+
}
|
|
2369
|
+
if (options.json) {
|
|
2370
|
+
output({
|
|
2371
|
+
success: true,
|
|
2372
|
+
cleaned,
|
|
2373
|
+
removedFromManifest: removed,
|
|
2374
|
+
pruned: options.prune ?? false
|
|
2375
|
+
}, true);
|
|
2376
|
+
} else {
|
|
2377
|
+
if (cleaned.length > 0) {
|
|
2378
|
+
success(`Cleaned ${cleaned.length} worktree(s)`);
|
|
2379
|
+
}
|
|
2380
|
+
if (removed.length > 0) {
|
|
2381
|
+
success(`Removed ${removed.length} worktree(s) from manifest`);
|
|
2382
|
+
}
|
|
2383
|
+
if (cleaned.length === 0 && removed.length === 0) {
|
|
2384
|
+
info("Nothing to clean");
|
|
2385
|
+
}
|
|
2386
|
+
if (options.prune) {
|
|
2387
|
+
success("Pruned stale git worktrees");
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
var init_clean = __esm({
|
|
2392
|
+
"src/commands/clean.ts"() {
|
|
2393
|
+
"use strict";
|
|
2394
|
+
init_manifest();
|
|
2395
|
+
init_worktree();
|
|
2396
|
+
init_cleanup();
|
|
2397
|
+
init_errors();
|
|
2398
|
+
init_output();
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
// src/commands/send.ts
|
|
2403
|
+
var send_exports = {};
|
|
2404
|
+
__export(send_exports, {
|
|
2405
|
+
sendCommand: () => sendCommand
|
|
2406
|
+
});
|
|
2407
|
+
async function sendCommand(agentId2, text, options) {
|
|
2408
|
+
const projectRoot = await getRepoRoot();
|
|
2409
|
+
let manifest;
|
|
2410
|
+
try {
|
|
2411
|
+
manifest = await readManifest(projectRoot);
|
|
2412
|
+
} catch {
|
|
2413
|
+
throw new NotInitializedError(projectRoot);
|
|
2414
|
+
}
|
|
2415
|
+
const found = findAgent(manifest, agentId2);
|
|
2416
|
+
if (!found) throw new AgentNotFoundError(agentId2);
|
|
2417
|
+
const { agent } = found;
|
|
2418
|
+
if (options.keys) {
|
|
2419
|
+
await sendRawKeys(agent.tmuxTarget, text);
|
|
2420
|
+
} else if (options.enter === false) {
|
|
2421
|
+
await sendLiteral(agent.tmuxTarget, text);
|
|
2422
|
+
} else {
|
|
2423
|
+
await sendKeys(agent.tmuxTarget, text);
|
|
2424
|
+
}
|
|
2425
|
+
if (options.json) {
|
|
2426
|
+
output({
|
|
2427
|
+
success: true,
|
|
2428
|
+
agentId: agent.id,
|
|
2429
|
+
tmuxTarget: agent.tmuxTarget,
|
|
2430
|
+
text
|
|
2431
|
+
}, true);
|
|
2432
|
+
} else {
|
|
2433
|
+
success(`Sent to agent ${agent.id}`);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
var init_send = __esm({
|
|
2437
|
+
"src/commands/send.ts"() {
|
|
2438
|
+
"use strict";
|
|
2439
|
+
init_manifest();
|
|
2440
|
+
init_worktree();
|
|
2441
|
+
init_tmux();
|
|
2442
|
+
init_errors();
|
|
2443
|
+
init_output();
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
// src/commands/wait.ts
|
|
2448
|
+
var wait_exports = {};
|
|
2449
|
+
__export(wait_exports, {
|
|
2450
|
+
waitCommand: () => waitCommand
|
|
2451
|
+
});
|
|
2452
|
+
async function waitCommand(worktreeRef, options) {
|
|
2453
|
+
const projectRoot = await getRepoRoot();
|
|
2454
|
+
const interval = (options.interval ?? 5) * 1e3;
|
|
2455
|
+
const timeout = options.timeout ? options.timeout * 1e3 : void 0;
|
|
2456
|
+
const startTime = Date.now();
|
|
2457
|
+
if (!worktreeRef && !options.all) {
|
|
2458
|
+
throw new PgError("Specify a worktree ID or use --all", "INVALID_ARGS");
|
|
2459
|
+
}
|
|
2460
|
+
if (!options.json) {
|
|
2461
|
+
info("Waiting for agents to complete...");
|
|
2462
|
+
}
|
|
2463
|
+
while (true) {
|
|
2464
|
+
if (timeout && Date.now() - startTime >= timeout) {
|
|
2465
|
+
const manifest2 = await refreshAndGet(projectRoot);
|
|
2466
|
+
const agents2 = collectAgents(manifest2, worktreeRef, options.all);
|
|
2467
|
+
if (options.json) {
|
|
2468
|
+
output({
|
|
2469
|
+
timedOut: true,
|
|
2470
|
+
agents: agents2.map(formatAgent)
|
|
2471
|
+
}, true);
|
|
2472
|
+
}
|
|
2473
|
+
throw new PgError("Timed out waiting for agents", "WAIT_TIMEOUT", 2);
|
|
2474
|
+
}
|
|
2475
|
+
const manifest = await refreshAndGet(projectRoot);
|
|
2476
|
+
const agents = collectAgents(manifest, worktreeRef, options.all);
|
|
2477
|
+
const allTerminal = agents.every((a) => TERMINAL_STATUSES.includes(a.status));
|
|
2478
|
+
if (allTerminal) {
|
|
2479
|
+
const anyFailed = agents.some((a) => ["failed", "lost"].includes(a.status));
|
|
2480
|
+
if (options.json) {
|
|
2481
|
+
output({
|
|
2482
|
+
timedOut: false,
|
|
2483
|
+
agents: agents.map(formatAgent)
|
|
2484
|
+
}, true);
|
|
2485
|
+
} else {
|
|
2486
|
+
for (const a of agents) {
|
|
2487
|
+
info(` ${a.id}: ${a.status}`);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
if (anyFailed) {
|
|
2491
|
+
throw new PgError("Some agents failed", "AGENTS_FAILED", 1);
|
|
2492
|
+
}
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
async function refreshAndGet(projectRoot) {
|
|
2499
|
+
try {
|
|
2500
|
+
return await updateManifest(projectRoot, async (m) => {
|
|
2501
|
+
return refreshAllAgentStatuses(m, projectRoot);
|
|
2502
|
+
});
|
|
2503
|
+
} catch {
|
|
2504
|
+
throw new NotInitializedError(projectRoot);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
function collectAgents(manifest, worktreeRef, all) {
|
|
2508
|
+
if (all) {
|
|
2509
|
+
const agents = [];
|
|
2510
|
+
for (const wt2 of Object.values(manifest.worktrees)) {
|
|
2511
|
+
agents.push(...Object.values(wt2.agents));
|
|
2512
|
+
}
|
|
2513
|
+
return agents;
|
|
2514
|
+
}
|
|
2515
|
+
const wt = resolveWorktree(manifest, worktreeRef);
|
|
2516
|
+
if (!wt) throw new WorktreeNotFoundError(worktreeRef);
|
|
2517
|
+
return Object.values(wt.agents);
|
|
2518
|
+
}
|
|
2519
|
+
function formatAgent(a) {
|
|
2520
|
+
return {
|
|
2521
|
+
id: a.id,
|
|
2522
|
+
status: a.status,
|
|
2523
|
+
agentType: a.agentType,
|
|
2524
|
+
exitCode: a.exitCode,
|
|
2525
|
+
startedAt: a.startedAt,
|
|
2526
|
+
completedAt: a.completedAt
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
var TERMINAL_STATUSES;
|
|
2530
|
+
var init_wait = __esm({
|
|
2531
|
+
"src/commands/wait.ts"() {
|
|
2532
|
+
"use strict";
|
|
2533
|
+
init_manifest();
|
|
2534
|
+
init_agent();
|
|
2535
|
+
init_worktree();
|
|
2536
|
+
init_errors();
|
|
2537
|
+
init_output();
|
|
2538
|
+
TERMINAL_STATUSES = ["completed", "failed", "killed", "lost"];
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
|
|
2542
|
+
// src/commands/worktree.ts
|
|
2543
|
+
var worktree_exports = {};
|
|
2544
|
+
__export(worktree_exports, {
|
|
2545
|
+
worktreeCreateCommand: () => worktreeCreateCommand
|
|
2546
|
+
});
|
|
2547
|
+
async function worktreeCreateCommand(options) {
|
|
2548
|
+
const projectRoot = await getRepoRoot();
|
|
2549
|
+
const config = await loadConfig(projectRoot);
|
|
2550
|
+
try {
|
|
2551
|
+
await readManifest(projectRoot);
|
|
2552
|
+
} catch {
|
|
2553
|
+
throw new NotInitializedError(projectRoot);
|
|
2554
|
+
}
|
|
2555
|
+
const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
|
|
2556
|
+
const wtId = worktreeId();
|
|
2557
|
+
const name = options.name ?? wtId;
|
|
2558
|
+
const branchName = `ppg/${name}`;
|
|
2559
|
+
info(`Creating worktree ${wtId} on branch ${branchName}`);
|
|
2560
|
+
const wtPath = await createWorktree(projectRoot, wtId, {
|
|
2561
|
+
branch: branchName,
|
|
2562
|
+
base: baseBranch
|
|
2563
|
+
});
|
|
2564
|
+
await setupWorktreeEnv(projectRoot, wtPath, config);
|
|
2565
|
+
const worktreeEntry = {
|
|
2566
|
+
id: wtId,
|
|
2567
|
+
name,
|
|
2568
|
+
path: wtPath,
|
|
2569
|
+
branch: branchName,
|
|
2570
|
+
baseBranch,
|
|
2571
|
+
status: "active",
|
|
2572
|
+
tmuxWindow: "",
|
|
2573
|
+
agents: {},
|
|
2574
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2575
|
+
};
|
|
2576
|
+
await updateManifest(projectRoot, (m) => {
|
|
2577
|
+
m.worktrees[wtId] = worktreeEntry;
|
|
2578
|
+
return m;
|
|
2579
|
+
});
|
|
2580
|
+
if (options.json) {
|
|
2581
|
+
output({
|
|
2582
|
+
success: true,
|
|
2583
|
+
worktree: {
|
|
2584
|
+
id: wtId,
|
|
2585
|
+
name,
|
|
2586
|
+
branch: branchName,
|
|
2587
|
+
baseBranch,
|
|
2588
|
+
path: wtPath
|
|
2589
|
+
}
|
|
2590
|
+
}, true);
|
|
2591
|
+
} else {
|
|
2592
|
+
success(`Created worktree ${wtId} (${name}) on branch ${branchName}`);
|
|
2593
|
+
info(`Path: ${wtPath}`);
|
|
2594
|
+
info(`Spawn agents: ppg spawn --worktree ${wtId} --prompt "your task"`);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
var init_worktree2 = __esm({
|
|
2598
|
+
"src/commands/worktree.ts"() {
|
|
2599
|
+
"use strict";
|
|
2600
|
+
init_config();
|
|
2601
|
+
init_manifest();
|
|
2602
|
+
init_worktree();
|
|
2603
|
+
init_env();
|
|
2604
|
+
init_id();
|
|
2605
|
+
init_errors();
|
|
2606
|
+
init_output();
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
|
|
2610
|
+
// src/commands/ui.ts
|
|
2611
|
+
var ui_exports = {};
|
|
2612
|
+
__export(ui_exports, {
|
|
2613
|
+
findDashboardBinary: () => findDashboardBinary,
|
|
2614
|
+
uiCommand: () => uiCommand
|
|
2615
|
+
});
|
|
2616
|
+
import { access } from "fs/promises";
|
|
2617
|
+
import path6 from "path";
|
|
2618
|
+
import { execa as execa7 } from "execa";
|
|
2619
|
+
async function findDashboardBinary(projectRoot) {
|
|
2620
|
+
const localBuild = path6.join(
|
|
2621
|
+
projectRoot,
|
|
2622
|
+
"PPG CLI",
|
|
2623
|
+
"build",
|
|
2624
|
+
"Build",
|
|
2625
|
+
"Products",
|
|
2626
|
+
"Release",
|
|
2627
|
+
"PPG CLI.app",
|
|
2628
|
+
"Contents",
|
|
2629
|
+
"MacOS",
|
|
2630
|
+
"PPG CLI"
|
|
2631
|
+
);
|
|
2632
|
+
try {
|
|
2633
|
+
await access(localBuild);
|
|
2634
|
+
return localBuild;
|
|
2635
|
+
} catch {
|
|
2636
|
+
}
|
|
2637
|
+
const appsBuild = "/Applications/PPG CLI.app/Contents/MacOS/PPG CLI";
|
|
2638
|
+
try {
|
|
2639
|
+
await access(appsBuild);
|
|
2640
|
+
return appsBuild;
|
|
2641
|
+
} catch {
|
|
2642
|
+
}
|
|
2643
|
+
try {
|
|
2644
|
+
const result = await execa7("mdfind", [
|
|
2645
|
+
'kMDItemCFBundleIdentifier == "com.2wit.PPG-CLI"'
|
|
2646
|
+
]);
|
|
2647
|
+
const appPath = result.stdout.trim().split("\n")[0];
|
|
2648
|
+
if (appPath) {
|
|
2649
|
+
const binaryPath = path6.join(appPath, "Contents", "MacOS", "PPG CLI");
|
|
2650
|
+
try {
|
|
2651
|
+
await access(binaryPath);
|
|
2652
|
+
return binaryPath;
|
|
2653
|
+
} catch {
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
} catch {
|
|
2657
|
+
}
|
|
2658
|
+
return null;
|
|
2659
|
+
}
|
|
2660
|
+
async function uiCommand() {
|
|
2661
|
+
const projectRoot = await getRepoRoot();
|
|
2662
|
+
let manifest;
|
|
2663
|
+
try {
|
|
2664
|
+
manifest = await readManifest(projectRoot);
|
|
2665
|
+
} catch {
|
|
2666
|
+
throw new NotInitializedError(projectRoot);
|
|
2667
|
+
}
|
|
2668
|
+
const binaryPath = await findDashboardBinary(projectRoot);
|
|
2669
|
+
if (!binaryPath) {
|
|
2670
|
+
throw new PgError(
|
|
2671
|
+
`Dashboard app not found. Install it with:
|
|
2672
|
+
ppg install-dashboard
|
|
2673
|
+
|
|
2674
|
+
Or build from source:
|
|
2675
|
+
cd "PPG CLI" && xcodebuild -scheme "PPG CLI" -configuration Release -derivedDataPath build build`,
|
|
2676
|
+
"DASHBOARD_NOT_FOUND"
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
const mPath = manifestPath(projectRoot);
|
|
2680
|
+
const proc = execa7(binaryPath, [
|
|
2681
|
+
"--manifest-path",
|
|
2682
|
+
mPath,
|
|
2683
|
+
"--session-name",
|
|
2684
|
+
manifest.sessionName,
|
|
2685
|
+
"--project-root",
|
|
2686
|
+
projectRoot
|
|
2687
|
+
], {
|
|
2688
|
+
detached: true,
|
|
2689
|
+
stdio: "ignore"
|
|
2690
|
+
});
|
|
2691
|
+
proc.unref();
|
|
2692
|
+
info(`Dashboard launched for ${manifest.sessionName}`);
|
|
2693
|
+
}
|
|
2694
|
+
var init_ui = __esm({
|
|
2695
|
+
"src/commands/ui.ts"() {
|
|
2696
|
+
"use strict";
|
|
2697
|
+
init_worktree();
|
|
2698
|
+
init_manifest();
|
|
2699
|
+
init_paths();
|
|
2700
|
+
init_errors();
|
|
2701
|
+
init_output();
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
|
|
2705
|
+
// src/commands/install-dashboard.ts
|
|
2706
|
+
var install_dashboard_exports = {};
|
|
2707
|
+
__export(install_dashboard_exports, {
|
|
2708
|
+
installDashboardCommand: () => installDashboardCommand
|
|
2709
|
+
});
|
|
2710
|
+
import { createWriteStream } from "fs";
|
|
2711
|
+
import { mkdir, cp, rm, readFile } from "fs/promises";
|
|
2712
|
+
import { tmpdir } from "os";
|
|
2713
|
+
import path7 from "path";
|
|
2714
|
+
import { pipeline } from "stream/promises";
|
|
2715
|
+
import { Readable } from "stream";
|
|
2716
|
+
import { execa as execa8 } from "execa";
|
|
2717
|
+
async function getVersion() {
|
|
2718
|
+
const pkgPath = new URL("../../package.json", import.meta.url);
|
|
2719
|
+
const raw = await readFile(pkgPath, "utf-8");
|
|
2720
|
+
const pkg = JSON.parse(raw);
|
|
2721
|
+
return pkg.version;
|
|
2722
|
+
}
|
|
2723
|
+
async function installDashboardCommand(options) {
|
|
2724
|
+
const { dir, json } = options;
|
|
2725
|
+
try {
|
|
2726
|
+
const version = await getVersion();
|
|
2727
|
+
const tag = `v${version}`;
|
|
2728
|
+
const url = `https://github.com/${REPO}/releases/download/${tag}/${ASSET_NAME}`;
|
|
2729
|
+
if (!json) info(`Downloading dashboard ${tag} from GitHub Releases\u2026`);
|
|
2730
|
+
const res = await fetch(url);
|
|
2731
|
+
if (!res.ok) {
|
|
2732
|
+
if (res.status === 404) {
|
|
2733
|
+
throw new PgError(
|
|
2734
|
+
`Dashboard release not found for ${tag}. The dashboard may not be available for this version yet.
|
|
2735
|
+
Check: https://github.com/${REPO}/releases/tag/${tag}`,
|
|
2736
|
+
"DASHBOARD_NOT_FOUND"
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
throw new PgError(
|
|
2740
|
+
`Failed to download dashboard: HTTP ${res.status} ${res.statusText}`,
|
|
2741
|
+
"DOWNLOAD_FAILED"
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
const tmp = path7.join(tmpdir(), `ppg-dashboard-${Date.now()}`);
|
|
2745
|
+
await mkdir(tmp, { recursive: true });
|
|
2746
|
+
const dmgPath = path7.join(tmp, ASSET_NAME);
|
|
2747
|
+
const body = res.body;
|
|
2748
|
+
if (!body) throw new PgError("Empty response body", "DOWNLOAD_FAILED");
|
|
2749
|
+
await pipeline(
|
|
2750
|
+
Readable.fromWeb(body),
|
|
2751
|
+
createWriteStream(dmgPath)
|
|
2752
|
+
);
|
|
2753
|
+
if (!json) info("Mounting\u2026");
|
|
2754
|
+
const mountResult = await execa8("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet"]);
|
|
2755
|
+
const mountLine = mountResult.stdout.trim().split("\n").pop() ?? "";
|
|
2756
|
+
const mountPoint = mountLine.split(" ").pop()?.trim();
|
|
2757
|
+
if (!mountPoint) {
|
|
2758
|
+
throw new PgError("Failed to mount DMG \u2014 could not determine mount point", "INSTALL_FAILED");
|
|
2759
|
+
}
|
|
2760
|
+
try {
|
|
2761
|
+
const srcApp = path7.join(mountPoint, APP_NAME);
|
|
2762
|
+
const destApp = path7.join(dir, APP_NAME);
|
|
2763
|
+
if (!json) info("Installing\u2026");
|
|
2764
|
+
await rm(destApp, { recursive: true, force: true });
|
|
2765
|
+
await cp(srcApp, destApp, { recursive: true });
|
|
2766
|
+
try {
|
|
2767
|
+
await execa8("xattr", ["-dr", "com.apple.quarantine", destApp]);
|
|
2768
|
+
} catch {
|
|
2769
|
+
}
|
|
2770
|
+
if (json) {
|
|
2771
|
+
output({ success: true, version, path: destApp }, true);
|
|
2772
|
+
} else {
|
|
2773
|
+
success(`Dashboard ${tag} installed to ${destApp}`);
|
|
2774
|
+
}
|
|
2775
|
+
} finally {
|
|
2776
|
+
await execa8("hdiutil", ["detach", mountPoint, "-quiet"]).catch(() => {
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
await rm(tmp, { recursive: true, force: true });
|
|
2780
|
+
} catch (err) {
|
|
2781
|
+
if (err instanceof PgError) throw err;
|
|
2782
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2783
|
+
throw new PgError(`Dashboard installation failed: ${message}`, "INSTALL_FAILED");
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
var REPO, ASSET_NAME, APP_NAME;
|
|
2787
|
+
var init_install_dashboard = __esm({
|
|
2788
|
+
"src/commands/install-dashboard.ts"() {
|
|
2789
|
+
"use strict";
|
|
2790
|
+
init_output();
|
|
2791
|
+
init_errors();
|
|
2792
|
+
REPO = "2witstudios/ppg-cli";
|
|
2793
|
+
ASSET_NAME = "PPG-CLI-Dashboard.dmg";
|
|
2794
|
+
APP_NAME = "PPG CLI.app";
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
|
|
2798
|
+
// src/cli.ts
|
|
2799
|
+
init_errors();
|
|
2800
|
+
init_output();
|
|
2801
|
+
import { Command } from "commander";
|
|
2802
|
+
var program = new Command();
|
|
2803
|
+
program.name("ppg").description("Pure Point Guard \u2014 local orchestration runtime for parallel CLI coding agents").version("0.1.0");
|
|
2804
|
+
program.command("init").description("Initialize Point Guard in the current git repository").option("--json", "Output as JSON").action(async (options) => {
|
|
2805
|
+
const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
2806
|
+
await initCommand2(options);
|
|
2807
|
+
});
|
|
2808
|
+
program.command("spawn").description("Spawn a new worktree and agent(s), or add agents to an existing worktree").option("-n, --name <name>", "Name for the worktree/task").option("-a, --agent <type>", "Agent type to use (default: claude)").option("-p, --prompt <text>", "Prompt text for the agent").option("-f, --prompt-file <path>", "File containing the prompt").option("-t, --template <name>", "Template name from .pg/templates/").option("--var <key=value...>", "Template variables", collectVars, []).option("-b, --base <branch>", "Base branch for the worktree").option("-w, --worktree <id>", "Add agent to existing worktree").option("-c, --count <n>", "Number of agents to spawn", (v) => Number(v), 1).option("--split", "Put all agents in one window as split panes").option("--no-open", "Do not open a Terminal window for the spawned agents").option("--json", "Output as JSON").action(async (options) => {
|
|
2809
|
+
const { spawnCommand: spawnCommand2 } = await Promise.resolve().then(() => (init_spawn(), spawn_exports));
|
|
2810
|
+
await spawnCommand2(options);
|
|
2811
|
+
});
|
|
2812
|
+
program.command("status").description("Show status of worktrees and agents").argument("[worktree]", "Filter by worktree ID or name").option("--json", "Output as JSON").option("-w, --watch", "Watch for status changes").action(async (worktree, options) => {
|
|
2813
|
+
const { statusCommand: statusCommand2 } = await Promise.resolve().then(() => (init_status(), status_exports));
|
|
2814
|
+
await statusCommand2(worktree, options);
|
|
2815
|
+
});
|
|
2816
|
+
program.command("kill").description("Kill agents or worktrees").option("-a, --agent <id>", "Kill a specific agent").option("-w, --worktree <id>", "Kill all agents in a worktree").option("--all", "Kill all agents in all worktrees").option("-r, --remove", "Also remove the worktree after killing").option("-d, --delete", "Delete agent/worktree entry from manifest after killing").option("--json", "Output as JSON").action(async (options) => {
|
|
2817
|
+
const { killCommand: killCommand2 } = await Promise.resolve().then(() => (init_kill(), kill_exports));
|
|
2818
|
+
await killCommand2(options);
|
|
2819
|
+
});
|
|
2820
|
+
program.command("attach").description("Attach to a worktree or agent tmux pane").argument("<target>", "Worktree ID, agent ID, or name").action(async (target) => {
|
|
2821
|
+
const { attachCommand: attachCommand2 } = await Promise.resolve().then(() => (init_attach(), attach_exports));
|
|
2822
|
+
await attachCommand2(target);
|
|
2823
|
+
});
|
|
2824
|
+
program.command("logs").description("View agent pane output").argument("<agent-id>", "Agent ID").option("-l, --lines <n>", "Number of lines to show", (v) => Number(v), 100).option("-f, --follow", "Follow output (poll every 1s)").option("--full", "Show full pane history").option("--json", "Output as JSON").action(async (agentId2, options) => {
|
|
2825
|
+
const { logsCommand: logsCommand2 } = await Promise.resolve().then(() => (init_logs(), logs_exports));
|
|
2826
|
+
await logsCommand2(agentId2, options);
|
|
2827
|
+
});
|
|
2828
|
+
program.command("aggregate").description("Aggregate results from completed agents").argument("[worktree-id]", "Worktree ID to aggregate results from").option("--all", "Aggregate from all worktrees").option("-o, --output <file>", "Write output to file").option("--json", "Output as JSON").action(async (worktreeId2, options) => {
|
|
2829
|
+
const { aggregateCommand: aggregateCommand2 } = await Promise.resolve().then(() => (init_aggregate(), aggregate_exports));
|
|
2830
|
+
await aggregateCommand2(worktreeId2, options);
|
|
2831
|
+
});
|
|
2832
|
+
program.command("merge").description("Merge a worktree branch back into base").argument("<worktree-id>", "Worktree ID to merge").option("-s, --strategy <strategy>", "Merge strategy: squash or no-ff", "squash").option("--no-cleanup", "Do not remove worktree after merge").option("--dry-run", "Show what would be done without doing it").option("--force", "Merge even if agents are not completed").option("--json", "Output as JSON").action(async (worktreeId2, options) => {
|
|
2833
|
+
const { mergeCommand: mergeCommand2 } = await Promise.resolve().then(() => (init_merge(), merge_exports));
|
|
2834
|
+
await mergeCommand2(worktreeId2, options);
|
|
2835
|
+
});
|
|
2836
|
+
program.command("list").description("List available templates").argument("<type>", "What to list: templates").option("--json", "Output as JSON").action(async (type, options) => {
|
|
2837
|
+
const { listCommand: listCommand2 } = await Promise.resolve().then(() => (init_list(), list_exports));
|
|
2838
|
+
await listCommand2(type, options);
|
|
2839
|
+
});
|
|
2840
|
+
program.command("restart").description("Restart a failed/killed agent in the same worktree").argument("<agent-id>", "Agent ID to restart").option("-p, --prompt <text>", "Override the original prompt").option("-a, --agent <type>", "Override the agent type").option("--no-open", "Do not open a Terminal window").option("--json", "Output as JSON").action(async (agentId2, options) => {
|
|
2841
|
+
const { restartCommand: restartCommand2 } = await Promise.resolve().then(() => (init_restart(), restart_exports));
|
|
2842
|
+
await restartCommand2(agentId2, options);
|
|
2843
|
+
});
|
|
2844
|
+
program.command("diff").description("Show changes made in a worktree branch").argument("<worktree-id>", "Worktree ID or name").option("--stat", "Show diffstat summary").option("--name-only", "Show only changed file names").option("--json", "Output as JSON").action(async (worktreeId2, options) => {
|
|
2845
|
+
const { diffCommand: diffCommand2 } = await Promise.resolve().then(() => (init_diff(), diff_exports));
|
|
2846
|
+
await diffCommand2(worktreeId2, options);
|
|
2847
|
+
});
|
|
2848
|
+
program.command("clean").description("Remove worktrees in terminal states (merged/cleaned/failed)").option("--all", "Also clean failed worktrees").option("--dry-run", "Show what would be done without doing it").option("--prune", "Also run git worktree prune").option("--json", "Output as JSON").action(async (options) => {
|
|
2849
|
+
const { cleanCommand: cleanCommand2 } = await Promise.resolve().then(() => (init_clean(), clean_exports));
|
|
2850
|
+
await cleanCommand2(options);
|
|
2851
|
+
});
|
|
2852
|
+
program.command("send").description("Send text to an agent's tmux pane").argument("<agent-id>", "Agent ID").argument("<text>", "Text to send").option("--keys", "Send raw tmux key names (e.g., C-c, Enter)").option("--no-enter", "Do not append Enter after the text").option("--json", "Output as JSON").action(async (agentId2, text, options) => {
|
|
2853
|
+
const { sendCommand: sendCommand2 } = await Promise.resolve().then(() => (init_send(), send_exports));
|
|
2854
|
+
await sendCommand2(agentId2, text, options);
|
|
2855
|
+
});
|
|
2856
|
+
program.command("wait").description("Wait for agents to reach terminal state").argument("[worktree-id]", "Worktree ID or name").option("--all", "Wait for all agents across all worktrees").option("--timeout <seconds>", "Timeout in seconds", parseInt).option("--interval <seconds>", "Poll interval in seconds", parseInt).option("--json", "Output as JSON").action(async (worktreeId2, options) => {
|
|
2857
|
+
const { waitCommand: waitCommand2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
|
|
2858
|
+
await waitCommand2(worktreeId2, options);
|
|
2859
|
+
});
|
|
2860
|
+
var worktreeCmd = program.command("worktree").description("Manage worktrees");
|
|
2861
|
+
worktreeCmd.command("create").description("Create a standalone worktree without spawning agents").option("-n, --name <name>", "Name for the worktree").option("-b, --base <branch>", "Base branch for the worktree").option("--json", "Output as JSON").action(async (options) => {
|
|
2862
|
+
const { worktreeCreateCommand: worktreeCreateCommand2 } = await Promise.resolve().then(() => (init_worktree2(), worktree_exports));
|
|
2863
|
+
await worktreeCreateCommand2(options);
|
|
2864
|
+
});
|
|
2865
|
+
program.command("ui").alias("dashboard").description("Open the native dashboard").action(async () => {
|
|
2866
|
+
const { uiCommand: uiCommand2 } = await Promise.resolve().then(() => (init_ui(), ui_exports));
|
|
2867
|
+
await uiCommand2();
|
|
2868
|
+
});
|
|
2869
|
+
program.command("install-dashboard").description("Download and install the macOS dashboard app").option("--dir <path>", "Install directory", "/Applications").option("--json", "JSON output").action(async (options) => {
|
|
2870
|
+
const { installDashboardCommand: installDashboardCommand2 } = await Promise.resolve().then(() => (init_install_dashboard(), install_dashboard_exports));
|
|
2871
|
+
await installDashboardCommand2(options);
|
|
2872
|
+
});
|
|
2873
|
+
program.exitOverride();
|
|
2874
|
+
function collectVars(value, previous) {
|
|
2875
|
+
return previous.concat([value]);
|
|
2876
|
+
}
|
|
2877
|
+
async function main() {
|
|
2878
|
+
try {
|
|
2879
|
+
await program.parseAsync(process.argv);
|
|
2880
|
+
} catch (err) {
|
|
2881
|
+
if (err instanceof PgError) {
|
|
2882
|
+
outputError(err, program.opts().json ?? false);
|
|
2883
|
+
process.exit(err.exitCode);
|
|
2884
|
+
}
|
|
2885
|
+
if (err instanceof Error && "code" in err) {
|
|
2886
|
+
const code = err.code;
|
|
2887
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
2888
|
+
process.exit(0);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
outputError(err, false);
|
|
2892
|
+
process.exit(1);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
main();
|
|
2896
|
+
//# sourceMappingURL=cli.js.map
|