@aku11i/phantom 0.1.0 → 0.1.2

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.
@@ -1,152 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { access, readdir } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { promisify } from "node:util";
5
- import { getGitRoot } from "../../git/libs/get-git-root.ts";
6
-
7
- const execAsync = promisify(exec);
8
-
9
- export interface GardenInfo {
10
- name: string;
11
- branch: string;
12
- status: "clean" | "dirty";
13
- changedFiles?: number;
14
- }
15
-
16
- export async function listGardens(): Promise<{
17
- success: boolean;
18
- message?: string;
19
- gardens?: GardenInfo[];
20
- }> {
21
- try {
22
- const gitRoot = await getGitRoot();
23
- const gardensPath = join(gitRoot, ".git", "phantom", "gardens");
24
-
25
- // Check if gardens directory exists
26
- try {
27
- await access(gardensPath);
28
- } catch {
29
- return {
30
- success: true,
31
- gardens: [],
32
- message: "No gardens found (gardens directory doesn't exist)",
33
- };
34
- }
35
-
36
- // Read gardens directory
37
- let gardenNames: string[];
38
- try {
39
- const entries = await readdir(gardensPath);
40
- // Filter entries to only include directories
41
- const validEntries = await Promise.all(
42
- entries.map(async (entry) => {
43
- try {
44
- const entryPath = join(gardensPath, entry);
45
- await access(entryPath);
46
- return entry;
47
- } catch {
48
- return null;
49
- }
50
- }),
51
- );
52
- gardenNames = validEntries.filter(
53
- (entry): entry is string => entry !== null,
54
- );
55
- } catch {
56
- return {
57
- success: true,
58
- gardens: [],
59
- message: "No gardens found (unable to read gardens directory)",
60
- };
61
- }
62
-
63
- if (gardenNames.length === 0) {
64
- return {
65
- success: true,
66
- gardens: [],
67
- message: "No gardens found",
68
- };
69
- }
70
-
71
- // Get detailed information for each garden
72
- const gardens: GardenInfo[] = await Promise.all(
73
- gardenNames.map(async (name) => {
74
- const gardenPath = join(gardensPath, name);
75
-
76
- // Get current branch
77
- let branch = "unknown";
78
- try {
79
- const { stdout } = await execAsync("git branch --show-current", {
80
- cwd: gardenPath,
81
- });
82
- branch = stdout.trim() || "detached HEAD";
83
- } catch {
84
- branch = "unknown";
85
- }
86
-
87
- // Get working directory status
88
- let status: "clean" | "dirty" = "clean";
89
- let changedFiles: number | undefined;
90
- try {
91
- const { stdout } = await execAsync("git status --porcelain", {
92
- cwd: gardenPath,
93
- });
94
- const changes = stdout.trim();
95
- if (changes) {
96
- status = "dirty";
97
- changedFiles = changes.split("\n").length;
98
- }
99
- } catch {
100
- // If git status fails, assume unknown status
101
- status = "clean";
102
- }
103
-
104
- return {
105
- name,
106
- branch,
107
- status,
108
- changedFiles,
109
- };
110
- }),
111
- );
112
-
113
- return {
114
- success: true,
115
- gardens,
116
- };
117
- } catch (error) {
118
- const errorMessage = error instanceof Error ? error.message : String(error);
119
- return {
120
- success: false,
121
- message: `Error listing gardens: ${errorMessage}`,
122
- };
123
- }
124
- }
125
-
126
- export async function gardensListHandler(): Promise<void> {
127
- const result = await listGardens();
128
-
129
- if (!result.success) {
130
- console.error(result.message);
131
- return;
132
- }
133
-
134
- if (!result.gardens || result.gardens.length === 0) {
135
- console.log(result.message || "No gardens found");
136
- return;
137
- }
138
-
139
- console.log("Gardens:");
140
- for (const garden of result.gardens) {
141
- const statusText =
142
- garden.status === "clean"
143
- ? "[clean]"
144
- : `[dirty: ${garden.changedFiles} files]`;
145
-
146
- console.log(
147
- ` ${garden.name.padEnd(20)} (branch: ${garden.branch.padEnd(20)}) ${statusText}`,
148
- );
149
- }
150
-
151
- console.log(`\nTotal: ${result.gardens.length} gardens`);
152
- }
@@ -1,149 +0,0 @@
1
- import { strictEqual } from "node:assert";
2
- import { before, describe, it, mock } from "node:test";
3
-
4
- describe("whereGarden", () => {
5
- let accessMock: ReturnType<typeof mock.fn>;
6
- let execMock: ReturnType<typeof mock.fn>;
7
- let whereGarden: typeof import("./where.ts").whereGarden;
8
-
9
- before(async () => {
10
- accessMock = mock.fn();
11
- execMock = mock.fn();
12
-
13
- mock.module("node:fs/promises", {
14
- namedExports: {
15
- access: accessMock,
16
- },
17
- });
18
-
19
- mock.module("node:child_process", {
20
- namedExports: {
21
- exec: execMock,
22
- },
23
- });
24
-
25
- mock.module("node:util", {
26
- namedExports: {
27
- promisify: (fn: unknown) => fn,
28
- },
29
- });
30
-
31
- ({ whereGarden } = await import("./where.ts"));
32
- });
33
-
34
- it("should return error when name is not provided", async () => {
35
- const result = await whereGarden("");
36
- strictEqual(result.success, false);
37
- strictEqual(result.message, "Error: garden name required");
38
- });
39
-
40
- it("should return error when garden does not exist", async () => {
41
- accessMock.mock.resetCalls();
42
- execMock.mock.resetCalls();
43
-
44
- // Mock getGitRoot
45
- execMock.mock.mockImplementation((cmd: string) => {
46
- if (cmd === "git rev-parse --show-toplevel") {
47
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
48
- }
49
- return Promise.resolve({ stdout: "", stderr: "" });
50
- });
51
-
52
- // Mock garden doesn't exist
53
- accessMock.mock.mockImplementation(() => {
54
- return Promise.reject(new Error("ENOENT"));
55
- });
56
-
57
- const result = await whereGarden("nonexistent-garden");
58
-
59
- strictEqual(result.success, false);
60
- strictEqual(
61
- result.message,
62
- "Error: Garden 'nonexistent-garden' does not exist",
63
- );
64
- });
65
-
66
- it("should return path when garden exists", async () => {
67
- accessMock.mock.resetCalls();
68
- execMock.mock.resetCalls();
69
-
70
- // Mock getGitRoot
71
- execMock.mock.mockImplementation((cmd: string) => {
72
- if (cmd === "git rev-parse --show-toplevel") {
73
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
74
- }
75
- return Promise.resolve({ stdout: "", stderr: "" });
76
- });
77
-
78
- // Mock garden exists
79
- accessMock.mock.mockImplementation(() => Promise.resolve());
80
-
81
- const result = await whereGarden("existing-garden");
82
-
83
- strictEqual(result.success, true);
84
- strictEqual(result.path, "/test/repo/.git/phantom/gardens/existing-garden");
85
- });
86
-
87
- it("should handle git root detection failures", async () => {
88
- accessMock.mock.resetCalls();
89
- execMock.mock.resetCalls();
90
-
91
- // Mock getGitRoot failure
92
- execMock.mock.mockImplementation(() => {
93
- return Promise.reject(new Error("Not a git repository"));
94
- });
95
-
96
- const result = await whereGarden("some-garden");
97
-
98
- strictEqual(result.success, false);
99
- strictEqual(result.message, "Error locating garden: Not a git repository");
100
- });
101
-
102
- it("should handle different garden names correctly", async () => {
103
- accessMock.mock.resetCalls();
104
- execMock.mock.resetCalls();
105
-
106
- // Mock getGitRoot
107
- execMock.mock.mockImplementation((cmd: string) => {
108
- if (cmd === "git rev-parse --show-toplevel") {
109
- return Promise.resolve({ stdout: "/different/repo\n", stderr: "" });
110
- }
111
- return Promise.resolve({ stdout: "", stderr: "" });
112
- });
113
-
114
- // Mock garden exists
115
- accessMock.mock.mockImplementation(() => Promise.resolve());
116
-
117
- const result = await whereGarden("feature-branch-123");
118
-
119
- strictEqual(result.success, true);
120
- strictEqual(
121
- result.path,
122
- "/different/repo/.git/phantom/gardens/feature-branch-123",
123
- );
124
- });
125
-
126
- it("should handle special characters in garden names", async () => {
127
- accessMock.mock.resetCalls();
128
- execMock.mock.resetCalls();
129
-
130
- // Mock getGitRoot
131
- execMock.mock.mockImplementation((cmd: string) => {
132
- if (cmd === "git rev-parse --show-toplevel") {
133
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
134
- }
135
- return Promise.resolve({ stdout: "", stderr: "" });
136
- });
137
-
138
- // Mock garden exists
139
- accessMock.mock.mockImplementation(() => Promise.resolve());
140
-
141
- const result = await whereGarden("feature-with-dashes_and_underscores");
142
-
143
- strictEqual(result.success, true);
144
- strictEqual(
145
- result.path,
146
- "/test/repo/.git/phantom/gardens/feature-with-dashes_and_underscores",
147
- );
148
- });
149
- });
@@ -1,53 +0,0 @@
1
- import { access } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { exit } from "node:process";
4
- import { getGitRoot } from "../../git/libs/get-git-root.ts";
5
-
6
- export async function whereGarden(name: string): Promise<{
7
- success: boolean;
8
- message?: string;
9
- path?: string;
10
- }> {
11
- if (!name) {
12
- return { success: false, message: "Error: garden name required" };
13
- }
14
-
15
- try {
16
- const gitRoot = await getGitRoot();
17
- const gardensPath = join(gitRoot, ".git", "phantom", "gardens");
18
- const gardenPath = join(gardensPath, name);
19
-
20
- // Check if garden exists
21
- try {
22
- await access(gardenPath);
23
- } catch {
24
- return {
25
- success: false,
26
- message: `Error: Garden '${name}' does not exist`,
27
- };
28
- }
29
-
30
- return {
31
- success: true,
32
- path: gardenPath,
33
- };
34
- } catch (error) {
35
- const errorMessage = error instanceof Error ? error.message : String(error);
36
- return {
37
- success: false,
38
- message: `Error locating garden: ${errorMessage}`,
39
- };
40
- }
41
- }
42
-
43
- export async function gardensWhereHandler(args: string[]): Promise<void> {
44
- const name = args[0];
45
- const result = await whereGarden(name);
46
-
47
- if (!result.success) {
48
- console.error(result.message);
49
- exit(1);
50
- }
51
-
52
- console.log(result.path);
53
- }
@@ -1,16 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { promisify } from "node:util";
3
-
4
- const execAsync = promisify(exec);
5
-
6
- export interface AddWorktreeOptions {
7
- path: string;
8
- branch: string;
9
- commitish?: string;
10
- }
11
-
12
- export async function addWorktree(options: AddWorktreeOptions): Promise<void> {
13
- const { path, branch, commitish = "HEAD" } = options;
14
-
15
- await execAsync(`git worktree add "${path}" -b "${branch}" ${commitish}`);
16
- }
@@ -1,9 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { promisify } from "node:util";
3
-
4
- const execAsync = promisify(exec);
5
-
6
- export async function getCurrentBranch(): Promise<string> {
7
- const { stdout } = await execAsync("git branch --show-current");
8
- return stdout.trim();
9
- }
@@ -1,9 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { promisify } from "node:util";
3
-
4
- const execAsync = promisify(exec);
5
-
6
- export async function getGitRoot(): Promise<string> {
7
- const { stdout } = await execAsync("git rev-parse --show-toplevel");
8
- return stdout.trim();
9
- }