@ferueda/grove-cli 0.2.0 → 0.3.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/dist/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import pc from "picocolors";
4
- import { handleError, setDebug } from "./error-handler.js";
4
+ import { handleError, setDebug, setJson } from "./error-handler.js";
5
5
  import { acquireCmd } from "./commands/acquire.js";
6
6
  import { releaseCmd } from "./commands/release.js";
7
7
  import { statusCmd } from "./commands/status.js";
8
8
  import { destroyCmd, destroyAllCmd } from "./commands/destroy.js";
9
+ import { inspectCmd } from "./commands/inspect.js";
10
+ import { repairCmd } from "./commands/repair.js";
9
11
  process.on("uncaughtException", (err) => {
10
12
  console.error(pc.red(`Fatal: ${err.message}`));
11
13
  process.exitCode = 1;
@@ -20,16 +22,20 @@ program
20
22
  .description("CLI for Grove - A programmatic git worktree pool manager")
21
23
  .version("0.1.0")
22
24
  .option("--debug", "Show verbose error output including stack traces")
23
- .hook("preAction", (thisCommand) => {
24
- const opts = thisCommand.optsWithGlobals();
25
+ .hook("preAction", (thisCommand, actionCommand) => {
26
+ const opts = actionCommand.optsWithGlobals();
25
27
  if (opts.debug)
26
28
  setDebug(true);
29
+ if (opts.json)
30
+ setJson(true);
27
31
  });
28
32
  program.addCommand(acquireCmd);
29
33
  program.addCommand(releaseCmd);
30
34
  program.addCommand(statusCmd);
31
35
  program.addCommand(destroyCmd);
32
36
  program.addCommand(destroyAllCmd);
37
+ program.addCommand(inspectCmd);
38
+ program.addCommand(repairCmd);
33
39
  try {
34
40
  await program.parseAsync(process.argv);
35
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"acquire.d.ts","sourceRoot":"","sources":["../../src/commands/acquire.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,eAAO,MAAM,UAAU,SA4CnB,CAAC"}
1
+ {"version":3,"file":"acquire.d.ts","sourceRoot":"","sources":["../../src/commands/acquire.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,eAAO,MAAM,UAAU,SA6FnB,CAAC"}
@@ -7,9 +7,51 @@ export const acquireCmd = new Command("acquire")
7
7
  .description("Acquire a worktree from the pool")
8
8
  .option("--shell", "Drop into an interactive subshell inside the worktree")
9
9
  .option("-r, --repo <path>", "Path to repository root")
10
+ .option("--lease <id>", "Lease ID to acquire")
11
+ .option("--owner <id>", "Owner ID for the lease")
12
+ .option("--branch <name>", "Branch to check out")
13
+ .option("--ref <sha>", "Ref to check out detached")
14
+ .option("--create-branch-from <ref>", "Create branch from this ref")
15
+ .option("--fail-if-exists", "Fail if branch already exists")
16
+ .option("--json", "Output result as JSON")
10
17
  .action(async (options) => {
11
18
  try {
12
19
  const grove = await loadGrove({ repo: options.repo });
20
+ if (options.lease) {
21
+ let modeOpts = {};
22
+ if (options.branch) {
23
+ modeOpts = { mode: "branch", branch: options.branch };
24
+ if (options.createBranchFrom) {
25
+ modeOpts.createBranch = {
26
+ from: options.createBranchFrom,
27
+ ifExists: options.failIfExists ? "fail" : "reuse",
28
+ };
29
+ }
30
+ }
31
+ else if (options.ref) {
32
+ modeOpts = { mode: "detached", ref: options.ref };
33
+ }
34
+ else {
35
+ // fallback to default branch logic if nothing specified?
36
+ modeOpts = { mode: "branch", branch: "main" }; // or we can just let SDK handle it, wait SDK requires mode.
37
+ // Since SDK requires mode, if not specified let's default to branch "main" or what SDK would do
38
+ // actually let's throw
39
+ throw new Error("Lease mode requires either --branch or --ref");
40
+ }
41
+ const acquireOpts = {
42
+ leaseId: options.lease,
43
+ ownerId: options.owner,
44
+ ...modeOpts,
45
+ };
46
+ const lease = await grove.acquire(acquireOpts);
47
+ if (options.json) {
48
+ process.stdout.write(JSON.stringify(lease, null, 2) + "\n");
49
+ return;
50
+ }
51
+ console.error(pc.green(`🌳 Acquired lease ${lease.leaseId} at ${lease.path}`));
52
+ return;
53
+ }
54
+ // Legacy flow
13
55
  const slot = await grove.acquire();
14
56
  if (options.shell) {
15
57
  console.error(pc.green(`🌳 Entered worktree at ${slot.path}. Type 'exit' to return.`));
@@ -38,8 +80,12 @@ export const acquireCmd = new Command("acquire")
38
80
  });
39
81
  }
40
82
  else {
41
- // Output raw path to stdout for pipeability
42
- process.stdout.write(slot.path + "\n");
83
+ if (options.json) {
84
+ process.stdout.write(JSON.stringify(slot, null, 2) + "\n");
85
+ }
86
+ else {
87
+ process.stdout.write(slot.path + "\n");
88
+ }
43
89
  }
44
90
  }
45
91
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"destroy.d.ts","sourceRoot":"","sources":["../../src/commands/destroy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,UAAU,SAanB,CAAC;AAEL,eAAO,MAAM,aAAa,SAYtB,CAAC"}
1
+ {"version":3,"file":"destroy.d.ts","sourceRoot":"","sources":["../../src/commands/destroy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,UAAU,SAoBnB,CAAC;AAEL,eAAO,MAAM,aAAa,SAkBtB,CAAC"}
@@ -4,14 +4,21 @@ import { handleError } from "../error-handler.js";
4
4
  import pc from "picocolors";
5
5
  export const destroyCmd = new Command("destroy")
6
6
  .description("Destroy a specific worktree from the pool")
7
- .argument("<path>", "Path to the worktree to destroy")
7
+ .argument("<pathOrLeaseId>", "Path or lease ID to destroy")
8
8
  .option("-f, --force", "Force destroy even if in use")
9
+ .option("--delete-branch", "Also delete the branch associated with this lease")
9
10
  .option("-r, --repo <path>", "Path to repository root")
10
- .action(async (worktreePath, options) => {
11
+ .option("--json", "Output result as JSON")
12
+ .action(async (pathOrLeaseId, options) => {
11
13
  try {
12
14
  const grove = await loadGrove({ repo: options.repo });
13
- await grove.destroy(worktreePath, { force: options.force });
14
- console.error(pc.green(`🌳 Destroyed worktree at ${worktreePath}`));
15
+ await grove.destroy(pathOrLeaseId, { force: options.force, deleteBranch: options.deleteBranch });
16
+ if (options.json) {
17
+ process.stdout.write(JSON.stringify({ success: true, target: pathOrLeaseId }) + "\n");
18
+ }
19
+ else {
20
+ console.error(pc.green(`🌳 Destroyed worktree/lease ${pathOrLeaseId}`));
21
+ }
15
22
  }
16
23
  catch (err) {
17
24
  handleError(err);
@@ -21,11 +28,17 @@ export const destroyAllCmd = new Command("destroy-all")
21
28
  .description("Destroy all worktrees in the pool")
22
29
  .option("-f, --force", "Force destroy even if in use")
23
30
  .option("-r, --repo <path>", "Path to repository root")
31
+ .option("--json", "Output result as JSON")
24
32
  .action(async (options) => {
25
33
  try {
26
34
  const grove = await loadGrove({ repo: options.repo });
27
35
  await grove.destroyAll({ force: options.force });
28
- console.error(pc.green("🌳 Destroyed all worktrees in the pool."));
36
+ if (options.json) {
37
+ process.stdout.write(JSON.stringify({ success: true }) + "\n");
38
+ }
39
+ else {
40
+ console.error(pc.green("🌳 Destroyed all worktrees in the pool."));
41
+ }
29
42
  }
30
43
  catch (err) {
31
44
  handleError(err);
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare const inspectCmd: Command;
3
+ //# sourceMappingURL=inspect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,UAAU,SA6BnB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { Command } from "commander";
2
+ import { loadGrove } from "../utils.js";
3
+ import { handleError } from "../error-handler.js";
4
+ import pc from "picocolors";
5
+ export const inspectCmd = new Command("inspect")
6
+ .description("Inspect a specific lease or worktree")
7
+ .argument("<pathOrLeaseId>", "Path or lease ID to inspect")
8
+ .option("-r, --repo <path>", "Path to repository root")
9
+ .option("--json", "Output result as JSON")
10
+ .action(async (pathOrLeaseId, options) => {
11
+ try {
12
+ const grove = await loadGrove({ repo: options.repo });
13
+ const lease = await grove.inspect(pathOrLeaseId);
14
+ if (!lease) {
15
+ throw new Error(`Lease not found: ${pathOrLeaseId}`);
16
+ }
17
+ if (options.json) {
18
+ process.stdout.write(JSON.stringify(lease, null, 2) + "\n");
19
+ }
20
+ else {
21
+ console.log(pc.bold(`Lease: ${lease.leaseId}`));
22
+ console.log(`Path: ${lease.path}`);
23
+ console.log(`State: ${lease.state}`);
24
+ console.log(`Branch: ${lease.branch || "-"}`);
25
+ console.log(`Safety: ${lease.processSafety}`);
26
+ if (lease.pendingCleanup) {
27
+ console.log(`Pending Cleanup: ${JSON.stringify(lease.pendingCleanup)}`);
28
+ }
29
+ }
30
+ }
31
+ catch (err) {
32
+ handleError(err);
33
+ }
34
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"release.d.ts","sourceRoot":"","sources":["../../src/commands/release.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,UAAU,SAcnB,CAAC"}
1
+ {"version":3,"file":"release.d.ts","sourceRoot":"","sources":["../../src/commands/release.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,eAAO,MAAM,UAAU,SAqDnB,CAAC"}
@@ -4,14 +4,51 @@ import { handleError } from "../error-handler.js";
4
4
  import pc from "picocolors";
5
5
  export const releaseCmd = new Command("release")
6
6
  .description("Release a worktree back to the pool")
7
- .argument("[path]", "Path to the worktree to release (defaults to CWD)")
7
+ .argument("[pathOrLeaseId]", "Path or lease ID to release (defaults to CWD)")
8
8
  .option("-r, --repo <path>", "Path to repository root")
9
- .action(async (worktreePath, options) => {
9
+ .option("--cleanup <action>", "Cleanup action (preserve, reset, quarantine)")
10
+ .option("--reset-to <ref>", "Branch/ref to reset to")
11
+ .option("-f, --force", "Force cleanup even if in use")
12
+ .option("--json", "Output result as JSON")
13
+ .action(async (pathOrLeaseId, options) => {
10
14
  try {
11
- const targetPath = worktreePath || process.cwd();
15
+ const targetPath = pathOrLeaseId || process.cwd();
12
16
  const grove = await loadGrove({ repo: options.repo });
17
+ if (options.cleanup) {
18
+ if (!["preserve", "reset", "quarantine"].includes(options.cleanup)) {
19
+ throw new Error("Invalid cleanup action");
20
+ }
21
+ let releaseOpts;
22
+ if (options.cleanup === "preserve") {
23
+ releaseOpts = { cleanup: "preserve" };
24
+ }
25
+ else if (options.cleanup === "quarantine") {
26
+ releaseOpts = { cleanup: "quarantine" };
27
+ }
28
+ else {
29
+ releaseOpts = {
30
+ cleanup: "reset",
31
+ force: options.force
32
+ };
33
+ if (options.resetTo) {
34
+ releaseOpts.resetTo = options.resetTo;
35
+ }
36
+ }
37
+ const lease = await grove.release(targetPath, releaseOpts);
38
+ if (options.json) {
39
+ process.stdout.write(JSON.stringify(lease, null, 2) + "\n");
40
+ return;
41
+ }
42
+ console.error(pc.green(`🌳 Lease ${lease.leaseId} released with action ${options.cleanup}.`));
43
+ return;
44
+ }
13
45
  await grove.release(targetPath);
14
- console.error(pc.green("🌳 Worktree returned to pool."));
46
+ if (options.json) {
47
+ process.stdout.write(JSON.stringify({ success: true, path: targetPath }) + "\n");
48
+ }
49
+ else {
50
+ console.error(pc.green("🌳 Worktree returned to pool."));
51
+ }
15
52
  }
16
53
  catch (err) {
17
54
  handleError(err);
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare const repairCmd: Command;
3
+ //# sourceMappingURL=repair.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repair.d.ts","sourceRoot":"","sources":["../../src/commands/repair.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,SAAS,SAoClB,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { Command } from "commander";
2
+ import { loadGrove } from "../utils.js";
3
+ import { handleError } from "../error-handler.js";
4
+ import pc from "picocolors";
5
+ export const repairCmd = new Command("repair")
6
+ .description("Repair a stuck or broken lease")
7
+ .argument("<leaseId>", "Lease ID to repair")
8
+ .requiredOption("--action <action>", "Action to take: quarantine, resume-cleanup, or force-destroy")
9
+ .option("-f, --force", "Force action even if processes are running")
10
+ .option("-r, --repo <path>", "Path to repository root")
11
+ .option("--json", "Output result as JSON")
12
+ .action(async (leaseId, options) => {
13
+ try {
14
+ if (!["quarantine", "resume-cleanup", "force-destroy"].includes(options.action)) {
15
+ throw new Error("Invalid action. Must be quarantine, resume-cleanup, or force-destroy.");
16
+ }
17
+ const grove = await loadGrove({ repo: options.repo });
18
+ const lease = await grove.repair({
19
+ leaseId,
20
+ action: options.action,
21
+ force: options.force,
22
+ });
23
+ if (!lease) {
24
+ if (options.json) {
25
+ process.stdout.write(JSON.stringify({ status: "destroyed", leaseId }) + "\n");
26
+ }
27
+ else {
28
+ console.error(pc.green(`🌳 Lease ${leaseId} was successfully force-destroyed.`));
29
+ }
30
+ return;
31
+ }
32
+ if (options.json) {
33
+ process.stdout.write(JSON.stringify(lease, null, 2) + "\n");
34
+ }
35
+ else {
36
+ console.error(pc.green(`🌳 Lease ${lease.leaseId} repaired with action ${options.action}. New state: ${lease.state}`));
37
+ }
38
+ }
39
+ catch (err) {
40
+ handleError(err);
41
+ }
42
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,SAAS,SAqDlB,CAAC"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,SAAS,SA+ElB,CAAC"}
@@ -5,10 +5,32 @@ import pc from "picocolors";
5
5
  export const statusCmd = new Command("status")
6
6
  .description("Show the status of all worktrees in the pool")
7
7
  .option("-r, --repo <path>", "Path to repository root")
8
+ .option("--leases", "Show lease metadata instead of ephemeral slots")
8
9
  .option("--json", "Output status as JSON")
9
10
  .action(async (options) => {
10
11
  try {
11
12
  const grove = await loadGrove({ repo: options.repo });
13
+ if (options.leases) {
14
+ const leases = await grove.listLeases();
15
+ if (options.json) {
16
+ process.stdout.write(JSON.stringify(leases, null, 2) + "\n");
17
+ return;
18
+ }
19
+ if (leases.length === 0) {
20
+ console.log("🌳 No leases in pool.");
21
+ return;
22
+ }
23
+ console.log(pc.bold("Lease ID\tState\t\tBranch\t\tPath"));
24
+ console.log("------------------------------------------------------------------");
25
+ for (const l of leases) {
26
+ const stateStr = l.state === "leased" ? pc.green(l.state) :
27
+ l.state === "quarantined" ? pc.red(l.state) :
28
+ pc.yellow(l.state);
29
+ console.log(`${l.leaseId}\t${stateStr}\t\t${l.branch || "-"}\t\t${l.path}`);
30
+ }
31
+ return;
32
+ }
33
+ // Legacy
12
34
  const trees = await grove.list();
13
35
  if (options.json) {
14
36
  process.stdout.write(JSON.stringify(trees, null, 2) + "\n");
@@ -1,3 +1,4 @@
1
1
  export declare function setDebug(enabled: boolean): void;
2
+ export declare function setJson(enabled: boolean): void;
2
3
  export declare function handleError(err: unknown): never;
3
4
  //# sourceMappingURL=error-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../src/error-handler.ts"],"names":[],"mappings":"AAKA,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE/C;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,CAsB/C"}
1
+ {"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../src/error-handler.ts"],"names":[],"mappings":"AAMA,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE/C;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE9C;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,CA+B/C"}
@@ -1,10 +1,22 @@
1
1
  import pc from "picocolors";
2
2
  import { GroveError, GitCommandError } from "@ferueda/grove";
3
3
  let debugEnabled = false;
4
+ let jsonEnabled = false;
4
5
  export function setDebug(enabled) {
5
6
  debugEnabled = enabled;
6
7
  }
8
+ export function setJson(enabled) {
9
+ jsonEnabled = enabled;
10
+ }
7
11
  export function handleError(err) {
12
+ if (jsonEnabled) {
13
+ const errorObj = { error: err instanceof Error ? err.message : String(err) };
14
+ if (err instanceof GroveError) {
15
+ errorObj.code = err.code;
16
+ }
17
+ process.stdout.write(JSON.stringify(errorObj, null, 2) + "\n");
18
+ process.exit(1);
19
+ }
8
20
  if (err instanceof GroveError) {
9
21
  console.error(pc.red(`[${err.code}] ${err.message}`));
10
22
  if (debugEnabled) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferueda/grove-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ferueda/grove.git"
@@ -19,7 +19,7 @@
19
19
  "commander": "^13.0.0",
20
20
  "execa": "^9.6.0",
21
21
  "picocolors": "^1.1.0",
22
- "@ferueda/grove": "0.2.0"
22
+ "@ferueda/grove": "0.3.0"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsgo -p tsconfig.json"