@matthugh1/conductor-cli 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.
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ assertCanQueueMerge,
4
+ getBranchOverview,
5
+ getConflictBranchesForWorkQueue,
6
+ getMergeStats,
7
+ isBranchFullyOnMain
8
+ } from "./chunk-IHARLSA6.js";
9
+ import "./chunk-FAZ7FCZQ.js";
10
+ import "./chunk-PANC6BTV.js";
11
+ import "./chunk-4YEHSYVN.js";
12
+ export {
13
+ assertCanQueueMerge,
14
+ getBranchOverview,
15
+ getConflictBranchesForWorkQueue,
16
+ getMergeStats,
17
+ isBranchFullyOnMain
18
+ };
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../../src/core/errors.ts
4
+ var ConductorError = class extends Error {
5
+ code;
6
+ retryable;
7
+ constructor(code, message, retryable = false) {
8
+ super(message);
9
+ this.name = "ConductorError";
10
+ this.code = code;
11
+ this.retryable = retryable;
12
+ }
13
+ };
14
+
15
+ export {
16
+ ConductorError
17
+ };
@@ -0,0 +1,534 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConductorError
4
+ } from "./chunk-4YEHSYVN.js";
5
+
6
+ // ../../src/core/git-wrapper.ts
7
+ import { execFile } from "child_process";
8
+ import { randomBytes } from "crypto";
9
+ import { promisify } from "util";
10
+ var execFileAsync = promisify(execFile);
11
+ var MAX_BUFFER = 10 * 1024 * 1024;
12
+ var READ_TIMEOUT_MS = 3e4;
13
+ var WRITE_TIMEOUT_MS = 6e4;
14
+ var READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
15
+ "status",
16
+ "log",
17
+ "diff",
18
+ "show",
19
+ "branch",
20
+ "tag",
21
+ "remote",
22
+ "config",
23
+ "ls-files",
24
+ "ls-tree",
25
+ "ls-remote",
26
+ "rev-parse",
27
+ "rev-list",
28
+ "describe",
29
+ "shortlog",
30
+ "blame",
31
+ "reflog",
32
+ "stash",
33
+ // stash list is read-only; stash push is write but rare
34
+ "name-rev",
35
+ "cat-file",
36
+ "count-objects",
37
+ "verify-pack"
38
+ ]);
39
+ function gitTimeoutForArgs(args) {
40
+ const subcommand = args.find((a) => !a.startsWith("-"))?.toLowerCase();
41
+ if (subcommand && READ_ONLY_SUBCOMMANDS.has(subcommand)) {
42
+ return READ_TIMEOUT_MS;
43
+ }
44
+ return WRITE_TIMEOUT_MS;
45
+ }
46
+ async function runGit(projectRoot, args, options) {
47
+ const timeout = options?.timeoutMs ?? gitTimeoutForArgs(args);
48
+ try {
49
+ const { stdout } = await execFileAsync("git", ["-C", projectRoot, ...args], {
50
+ maxBuffer: MAX_BUFFER,
51
+ timeout
52
+ });
53
+ return stdout;
54
+ } catch (err) {
55
+ const e = err;
56
+ if (e.killed || e.signal === "SIGTERM") {
57
+ throw new ConductorError(
58
+ "GIT_TIMEOUT" /* GIT_TIMEOUT */,
59
+ `Git command timed out after ${Math.round(timeout / 1e3)}s. The command was killed. If this is a network operation (push, pull, fetch), check your connection.`,
60
+ true
61
+ );
62
+ }
63
+ if (e.code === "ENOENT") {
64
+ throw new ConductorError("GIT_NOT_FOUND" /* GIT_NOT_FOUND */, "Git is not installed or not on your PATH");
65
+ }
66
+ const stderr = typeof e.stderr === "string" ? e.stderr : "";
67
+ const firstLine = stderr.split("\n").find((line) => line.trim().length > 0) ?? "";
68
+ if (firstLine.includes("not a git repository")) {
69
+ throw new ConductorError("GIT_NOT_A_REPO" /* GIT_NOT_A_REPO */, "This folder is not a git repository");
70
+ }
71
+ if (e.status === 128 && firstLine.length > 0) {
72
+ throw new ConductorError("GIT_FAILED" /* GIT_FAILED */, firstLine);
73
+ }
74
+ if (firstLine.length > 0) {
75
+ throw new ConductorError("GIT_FAILED" /* GIT_FAILED */, firstLine);
76
+ }
77
+ throw new ConductorError("GIT_FAILED" /* GIT_FAILED */, "Git exited with an error");
78
+ }
79
+ }
80
+ async function getGitBranchInfo(projectRoot) {
81
+ try {
82
+ await runGit(projectRoot, ["rev-parse", "--is-inside-work-tree"]);
83
+ } catch {
84
+ return { isNotGitRepo: true, hasNoCommits: false, branchName: null };
85
+ }
86
+ try {
87
+ await runGit(projectRoot, ["rev-parse", "HEAD"]);
88
+ } catch {
89
+ return { isNotGitRepo: false, hasNoCommits: true, branchName: null };
90
+ }
91
+ try {
92
+ const out = await runGit(projectRoot, [
93
+ "rev-parse",
94
+ "--abbrev-ref",
95
+ "HEAD"
96
+ ]);
97
+ const name = out.trim();
98
+ if (name === "HEAD") {
99
+ const short = await runGit(projectRoot, ["rev-parse", "--short", "HEAD"]);
100
+ return {
101
+ isNotGitRepo: false,
102
+ hasNoCommits: false,
103
+ branchName: `detached (${short.trim()})`
104
+ };
105
+ }
106
+ return { isNotGitRepo: false, hasNoCommits: false, branchName: name };
107
+ } catch {
108
+ return { isNotGitRepo: false, hasNoCommits: false, branchName: null };
109
+ }
110
+ }
111
+ function formatGitBranchLine(info) {
112
+ if (info.isNotGitRepo) {
113
+ return "Not a git repository";
114
+ }
115
+ if (info.hasNoCommits) {
116
+ return "No commits yet \u2014 Git will name a branch after the first commit";
117
+ }
118
+ if (info.branchName !== null && info.branchName.length > 0) {
119
+ return info.branchName;
120
+ }
121
+ return "Branch unknown";
122
+ }
123
+ async function getCurrentBranch(projectRoot) {
124
+ const info = await getGitBranchInfo(projectRoot);
125
+ if (info.isNotGitRepo || info.hasNoCommits) {
126
+ return null;
127
+ }
128
+ if (info.branchName !== null && info.branchName.startsWith("detached (")) {
129
+ return null;
130
+ }
131
+ return info.branchName;
132
+ }
133
+ async function getStatusSummary(projectRoot) {
134
+ try {
135
+ const out = await runGit(projectRoot, ["status", "--porcelain"]);
136
+ const lines = out.split("\n").filter((l) => l.length > 0);
137
+ if (lines.length === 0) {
138
+ return "Clean \u2014 no uncommitted changes";
139
+ }
140
+ let modified = 0;
141
+ let added = 0;
142
+ let deleted = 0;
143
+ let untracked = 0;
144
+ for (const line of lines) {
145
+ const code = line.slice(0, 2);
146
+ if (code.includes("?")) {
147
+ untracked += 1;
148
+ continue;
149
+ }
150
+ if (code.includes("A") || code.includes("N")) {
151
+ added += 1;
152
+ }
153
+ if (code.includes("M")) {
154
+ modified += 1;
155
+ }
156
+ if (code.includes("D")) {
157
+ deleted += 1;
158
+ }
159
+ }
160
+ const parts = [];
161
+ if (modified > 0) {
162
+ parts.push(`${modified} file${modified === 1 ? "" : "s"} changed`);
163
+ }
164
+ if (added > 0) {
165
+ parts.push(`${added} new file${added === 1 ? "" : "s"}`);
166
+ }
167
+ if (deleted > 0) {
168
+ parts.push(`${deleted} deleted file${deleted === 1 ? "" : "s"}`);
169
+ }
170
+ if (untracked > 0) {
171
+ parts.push(`${untracked} untracked file${untracked === 1 ? "" : "s"}`);
172
+ }
173
+ if (parts.length === 0) {
174
+ return `${lines.length} change${lines.length === 1 ? "" : "s"} in the working tree`;
175
+ }
176
+ return parts.join(", ");
177
+ } catch (err) {
178
+ const msg = err instanceof Error ? err.message : "Could not read git status.";
179
+ return msg;
180
+ }
181
+ }
182
+ async function getRecentCommits(projectRoot) {
183
+ const out = await runGit(projectRoot, ["log", "--oneline", "-5"]);
184
+ const rows = [];
185
+ for (const line of out.split("\n")) {
186
+ if (line.trim().length === 0) {
187
+ continue;
188
+ }
189
+ const space = line.indexOf(" ");
190
+ if (space === -1) {
191
+ rows.push({ hash: line, message: "" });
192
+ } else {
193
+ rows.push({
194
+ hash: line.slice(0, space),
195
+ message: line.slice(space + 1).trim()
196
+ });
197
+ }
198
+ }
199
+ return rows;
200
+ }
201
+ function commandSummary(gitArgs) {
202
+ if (gitArgs.length === 0) {
203
+ return "git";
204
+ }
205
+ return `git ${gitArgs.join(" ")}`;
206
+ }
207
+ var lower = (a) => a.map((x) => x.toLowerCase());
208
+ function tierAndCopy(gitArgs) {
209
+ const args = lower(gitArgs);
210
+ const joined = args.join(" ");
211
+ if (args.length === 0) {
212
+ return {
213
+ tier: "yellow",
214
+ plainEnglish: "Runs git with no subcommand \u2014 unusual and may show help or do nothing.",
215
+ whatChanges: "Depends on your git version.",
216
+ whatCouldGoWrong: "Use a clear subcommand (for example status or commit) so the intent is obvious."
217
+ };
218
+ }
219
+ if (joined.includes("push") && (args.includes("--force") || args.includes("-f"))) {
220
+ return {
221
+ tier: "red",
222
+ plainEnglish: "Overwrite the remote branch with your local history (force push).",
223
+ whatChanges: "The remote branch may lose commits that exist only on the server.",
224
+ whatCouldGoWrong: "If others use this branch, their work can be overwritten or confused \u2014 coordinate before force pushing."
225
+ };
226
+ }
227
+ if (args.includes("reset") && args.includes("--hard")) {
228
+ return {
229
+ tier: "red",
230
+ plainEnglish: "Discard uncommitted changes and move your branch pointer.",
231
+ whatChanges: "Uncommitted edits in your working tree can be permanently removed.",
232
+ whatCouldGoWrong: "Anything not saved in a commit may be gone for good."
233
+ };
234
+ }
235
+ if (args.includes("clean")) {
236
+ const isDryRun = args.includes("-n") || args.includes("--dry-run");
237
+ const forcesClean = args.some(
238
+ (x) => x === "-f" || x === "-fd" || x === "-xffd" || x.startsWith("-f")
239
+ );
240
+ if (!isDryRun && forcesClean) {
241
+ return {
242
+ tier: "red",
243
+ plainEnglish: "Delete untracked files (and sometimes directories) from your project folder.",
244
+ whatChanges: "Files that are not tracked by git can be removed from disk.",
245
+ whatCouldGoWrong: "You may delete files you still need \u2014 there is no undo from git."
246
+ };
247
+ }
248
+ }
249
+ if (args.includes("rebase")) {
250
+ return {
251
+ tier: "red",
252
+ plainEnglish: "Replay commits on top of another branch (rewrite history locally).",
253
+ whatChanges: "Commit hashes and history can change; conflicts may need resolving.",
254
+ whatCouldGoWrong: "Mistakes during rebase can be hard to untangle \u2014 snapshot or branch first."
255
+ };
256
+ }
257
+ if (args.includes("branch") && gitArgs.includes("-D")) {
258
+ return {
259
+ tier: "red",
260
+ plainEnglish: "Delete a branch even if it is not fully merged.",
261
+ whatChanges: "The branch ref is removed from your repo.",
262
+ whatCouldGoWrong: "Commits may become harder to find if you still needed that branch."
263
+ };
264
+ }
265
+ if (args.includes("stash") && (args.includes("drop") || args.includes("clear"))) {
266
+ return {
267
+ tier: "red",
268
+ plainEnglish: "Permanently remove one or all stash entries.",
269
+ whatChanges: "Stashed work can be deleted.",
270
+ whatCouldGoWrong: "You can lose saved work that only lived in the stash."
271
+ };
272
+ }
273
+ if (args.includes("checkout") && args.includes("--force")) {
274
+ return {
275
+ tier: "red",
276
+ plainEnglish: "Force checkout, potentially overwriting local changes.",
277
+ whatChanges: "Your working tree may match another branch or commit aggressively.",
278
+ whatCouldGoWrong: "Local changes can be overwritten without a prompt."
279
+ };
280
+ }
281
+ const g0 = args[0];
282
+ const greenVerbs = /* @__PURE__ */ new Set([
283
+ "status",
284
+ "diff",
285
+ "log",
286
+ "show",
287
+ "ls-files",
288
+ "rev-parse",
289
+ "blame",
290
+ "grep"
291
+ ]);
292
+ if (greenVerbs.has(g0)) {
293
+ return {
294
+ tier: "green",
295
+ plainEnglish: describeGreen(g0, args),
296
+ whatChanges: "Only reads repository state; it does not change commits or the index by itself.",
297
+ whatCouldGoWrong: "Low risk \u2014 output may be large in big repos."
298
+ };
299
+ }
300
+ if (g0 === "remote" && !args.includes("add") && !args.includes("remove") && !args.includes("rm") && !args.includes("set-url")) {
301
+ return {
302
+ tier: "green",
303
+ plainEnglish: "Show or list configured remotes.",
304
+ whatChanges: "No files or commits are modified.",
305
+ whatCouldGoWrong: "Very low risk."
306
+ };
307
+ }
308
+ if (g0 === "branch") {
309
+ const listsBranches = !args.includes("-D") && !args.includes("-d") && !args.includes("-m") && !args.includes("--set-upstream") && (args.length === 1 || args.includes("-a") || args.includes("-r") || args.includes("-v") || args.includes("-vv") || args.includes("--list"));
310
+ if (listsBranches) {
311
+ return {
312
+ tier: "green",
313
+ plainEnglish: "List branches (local and/or remote).",
314
+ whatChanges: "Does not create, delete, or switch branches.",
315
+ whatCouldGoWrong: "Very low risk."
316
+ };
317
+ }
318
+ }
319
+ if (g0 === "tag") {
320
+ if (args.length === 1 || args.includes("-l") || args.includes("--list")) {
321
+ return {
322
+ tier: "green",
323
+ plainEnglish: "List tags (read-only).",
324
+ whatChanges: "Does not create or delete tags.",
325
+ whatCouldGoWrong: "Low risk."
326
+ };
327
+ }
328
+ }
329
+ if (g0 === "stash" && (args[1] === "list" || args[1] === "show")) {
330
+ return {
331
+ tier: "green",
332
+ plainEnglish: "Inspect saved stash entries.",
333
+ whatChanges: "Does not apply or drop stash entries.",
334
+ whatCouldGoWrong: "Low risk."
335
+ };
336
+ }
337
+ if (g0 === "fetch") {
338
+ return {
339
+ tier: "green",
340
+ plainEnglish: "Download updates from the remote without merging into your branch.",
341
+ whatChanges: "Updates remote-tracking branches locally.",
342
+ whatCouldGoWrong: "Usually safe; large fetches can be slow."
343
+ };
344
+ }
345
+ const yellowVerbs = /* @__PURE__ */ new Set([
346
+ "add",
347
+ "commit",
348
+ "push",
349
+ "pull",
350
+ "checkout",
351
+ "switch",
352
+ "merge",
353
+ "restore",
354
+ "mv",
355
+ "rm"
356
+ ]);
357
+ if (yellowVerbs.has(g0)) {
358
+ return {
359
+ tier: "yellow",
360
+ plainEnglish: describeYellow(g0, args),
361
+ whatChanges: "Updates your repo, index, or working tree depending on the subcommand.",
362
+ whatCouldGoWrong: "Conflicts, partial updates, or pushes can fail \u2014 read git messages if something goes wrong."
363
+ };
364
+ }
365
+ if (g0 === "branch" && args.length > 1) {
366
+ return {
367
+ tier: "yellow",
368
+ plainEnglish: "Create, rename, or delete branches (not a simple list).",
369
+ whatChanges: "Branch pointers or names change.",
370
+ whatCouldGoWrong: "Wrong branch names can confuse history \u2014 double-check names."
371
+ };
372
+ }
373
+ if (g0 === "stash" && (args.includes("push") || args.includes("pop") || args.includes("apply"))) {
374
+ return {
375
+ tier: "yellow",
376
+ plainEnglish: "Save or restore work in progress using the stash.",
377
+ whatChanges: "Working tree and possibly index change when applying or popping.",
378
+ whatCouldGoWrong: "Stash pop can conflict; you may need to resolve overlaps by hand."
379
+ };
380
+ }
381
+ if (g0 === "tag" && !args.includes("-l") && !args.includes("--list")) {
382
+ return {
383
+ tier: "yellow",
384
+ plainEnglish: "Create or manage tags.",
385
+ whatChanges: "Tags point to commits and can be pushed.",
386
+ whatCouldGoWrong: "Tagging mistakes can confuse releases \u2014 confirm names and targets."
387
+ };
388
+ }
389
+ return {
390
+ tier: "yellow",
391
+ plainEnglish: `Run git subcommand \u201C${g0}\u201D with your arguments.`,
392
+ whatChanges: "May read or write the repository depending on what you asked for.",
393
+ whatCouldGoWrong: "If you are unsure, prefer read-only commands (status, diff, log) first."
394
+ };
395
+ }
396
+ function describeGreen(verb, args) {
397
+ switch (verb) {
398
+ case "status":
399
+ return "Show which files changed, staged, or are untracked.";
400
+ case "diff":
401
+ return "Show line-by-line differences.";
402
+ case "log":
403
+ return "Show recent commits.";
404
+ case "show":
405
+ return "Show a commit, tag, or object details.";
406
+ case "ls-files":
407
+ return "List files known to git in the index or working tree.";
408
+ case "rev-parse":
409
+ return "Resolve refs (for example branch names) to commit hashes.";
410
+ case "blame":
411
+ return "Show who last changed each line of a file.";
412
+ case "grep":
413
+ return "Search tracked files for a pattern.";
414
+ default:
415
+ return `Run git ${verb} in read-only fashion.`;
416
+ }
417
+ }
418
+ function describeYellow(verb, gitArgs) {
419
+ switch (verb) {
420
+ case "add":
421
+ return "Stage files so the next commit includes them.";
422
+ case "commit":
423
+ return "Save staged changes as a new commit.";
424
+ case "push":
425
+ return "Upload local commits to a remote repository.";
426
+ case "pull":
427
+ return "Download and merge changes from a remote branch.";
428
+ case "checkout":
429
+ return "Switch branches or restore files (can change your working tree).";
430
+ case "switch":
431
+ return "Switch to another branch.";
432
+ case "merge":
433
+ return "Combine another branch into your current branch.";
434
+ case "restore":
435
+ return "Restore working tree or index files.";
436
+ case "mv":
437
+ case "rm":
438
+ return "Change or remove tracked files as requested.";
439
+ default:
440
+ return `Run git ${verb} ${gitArgs.slice(1).join(" ")}`.trim();
441
+ }
442
+ }
443
+ function classifyGitCommand(gitArgs) {
444
+ const summary = commandSummary(gitArgs);
445
+ const { tier, plainEnglish, whatChanges, whatCouldGoWrong } = tierAndCopy(gitArgs);
446
+ const needsSnapshot = tier === "red";
447
+ return {
448
+ tier,
449
+ needsSnapshot,
450
+ summary,
451
+ plainEnglish,
452
+ whatChanges,
453
+ whatCouldGoWrong
454
+ };
455
+ }
456
+ async function explainGitCommand(projectRoot, gitArgs) {
457
+ void projectRoot;
458
+ return classifyGitCommand(gitArgs);
459
+ }
460
+ async function createSafetySnapshot(projectRoot) {
461
+ const suffix = randomBytes(4).toString("hex");
462
+ const branchName = `conductor-safety-${Date.now()}-${suffix}`;
463
+ await runGit(projectRoot, ["branch", branchName]);
464
+ let stashSaved = false;
465
+ try {
466
+ await runGit(projectRoot, [
467
+ "stash",
468
+ "push",
469
+ "-u",
470
+ "-m",
471
+ "Conductor safety stash before a risky git command"
472
+ ]);
473
+ stashSaved = true;
474
+ } catch {
475
+ stashSaved = false;
476
+ }
477
+ return { branchName, stashSaved };
478
+ }
479
+ async function executeConfirmedGitCommand(projectRoot, gitArgs) {
480
+ const c = classifyGitCommand(gitArgs);
481
+ if (c.tier === "green") {
482
+ const output = await runGit(projectRoot, gitArgs);
483
+ return {
484
+ tier: c.tier,
485
+ summary: c.summary,
486
+ ran: true,
487
+ output,
488
+ snapshot: null
489
+ };
490
+ }
491
+ return {
492
+ tier: c.tier,
493
+ summary: c.summary,
494
+ ran: false,
495
+ output: "",
496
+ snapshot: null,
497
+ classification: c
498
+ };
499
+ }
500
+ async function executeNarrated(projectRoot, gitArgs) {
501
+ const c = classifyGitCommand(gitArgs);
502
+ if (c.tier === "green" || c.tier === "yellow") {
503
+ const output = await runGit(projectRoot, gitArgs);
504
+ return {
505
+ tier: c.tier,
506
+ summary: c.summary,
507
+ ran: true,
508
+ output,
509
+ snapshot: null
510
+ };
511
+ }
512
+ return {
513
+ tier: c.tier,
514
+ summary: c.summary,
515
+ ran: false,
516
+ output: "",
517
+ snapshot: null,
518
+ classification: c
519
+ };
520
+ }
521
+
522
+ export {
523
+ runGit,
524
+ getGitBranchInfo,
525
+ formatGitBranchLine,
526
+ getCurrentBranch,
527
+ getStatusSummary,
528
+ getRecentCommits,
529
+ classifyGitCommand,
530
+ explainGitCommand,
531
+ createSafetySnapshot,
532
+ executeConfirmedGitCommand,
533
+ executeNarrated
534
+ };