dubstack 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/dist/commands/create.d.ts +18 -0
  4. package/dist/commands/create.d.ts.map +1 -0
  5. package/dist/commands/create.js +35 -0
  6. package/dist/commands/create.js.map +1 -0
  7. package/dist/commands/init.d.ts +18 -0
  8. package/dist/commands/init.d.ts.map +1 -0
  9. package/dist/commands/init.js +41 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/log.d.ts +12 -0
  12. package/dist/commands/log.d.ts.map +1 -0
  13. package/dist/commands/log.js +77 -0
  14. package/dist/commands/log.js.map +1 -0
  15. package/dist/commands/restack.d.ts +33 -0
  16. package/dist/commands/restack.d.ts.map +1 -0
  17. package/dist/commands/restack.js +190 -0
  18. package/dist/commands/restack.js.map +1 -0
  19. package/dist/commands/undo.d.ts +21 -0
  20. package/dist/commands/undo.d.ts.map +1 -0
  21. package/dist/commands/undo.js +63 -0
  22. package/dist/commands/undo.js.map +1 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +125 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/lib/errors.d.ts +15 -0
  28. package/dist/lib/errors.d.ts.map +1 -0
  29. package/dist/lib/errors.js +18 -0
  30. package/dist/lib/errors.js.map +1 -0
  31. package/dist/lib/git.d.ts +69 -0
  32. package/dist/lib/git.d.ts.map +1 -0
  33. package/dist/lib/git.js +184 -0
  34. package/dist/lib/git.js.map +1 -0
  35. package/dist/lib/state.d.ts +70 -0
  36. package/dist/lib/state.d.ts.map +1 -0
  37. package/dist/lib/state.js +110 -0
  38. package/dist/lib/state.js.map +1 -0
  39. package/dist/lib/undo-log.d.ts +33 -0
  40. package/dist/lib/undo-log.d.ts.map +1 -0
  41. package/dist/lib/undo-log.js +37 -0
  42. package/dist/lib/undo-log.js.map +1 -0
  43. package/package.json +49 -0
@@ -0,0 +1,184 @@
1
+ import { execa } from "execa";
2
+ import { DubError } from "./errors.js";
3
+ /**
4
+ * Checks whether the given directory is inside a git repository.
5
+ * @returns `true` if inside a git worktree, `false` otherwise. Never throws.
6
+ */
7
+ export async function isGitRepo(cwd) {
8
+ try {
9
+ const { stdout } = await execa("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
10
+ return stdout.trim() === "true";
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ /**
17
+ * Returns the absolute path to the repository root.
18
+ * @throws {DubError} If not inside a git repository.
19
+ */
20
+ export async function getRepoRoot(cwd) {
21
+ try {
22
+ const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], {
23
+ cwd,
24
+ });
25
+ return stdout.trim();
26
+ }
27
+ catch {
28
+ throw new DubError("Not a git repository. Run this command inside a git repo.");
29
+ }
30
+ }
31
+ /**
32
+ * Returns the name of the currently checked-out branch.
33
+ * @throws {DubError} If HEAD is detached or the repo has no commits.
34
+ */
35
+ export async function getCurrentBranch(cwd) {
36
+ try {
37
+ const { stdout } = await execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
38
+ const branch = stdout.trim();
39
+ if (branch === "HEAD") {
40
+ throw new DubError("HEAD is detached. Checkout a branch before running this command.");
41
+ }
42
+ return branch;
43
+ }
44
+ catch (error) {
45
+ if (error instanceof DubError)
46
+ throw error;
47
+ throw new DubError("Repository has no commits. Make at least one commit first.");
48
+ }
49
+ }
50
+ /**
51
+ * Checks whether a branch with the given name exists locally.
52
+ * @returns `true` if the branch exists, `false` otherwise. Never throws.
53
+ */
54
+ export async function branchExists(name, cwd) {
55
+ try {
56
+ await execa("git", ["rev-parse", "--verify", `refs/heads/${name}`], {
57
+ cwd,
58
+ });
59
+ return true;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ /**
66
+ * Creates a new branch and switches to it.
67
+ * @throws {DubError} If a branch with that name already exists.
68
+ */
69
+ export async function createBranch(name, cwd) {
70
+ if (await branchExists(name, cwd)) {
71
+ throw new DubError(`Branch '${name}' already exists.`);
72
+ }
73
+ await execa("git", ["checkout", "-b", name], { cwd });
74
+ }
75
+ /**
76
+ * Switches to an existing branch.
77
+ * @throws {DubError} If the branch does not exist.
78
+ */
79
+ export async function checkoutBranch(name, cwd) {
80
+ try {
81
+ await execa("git", ["checkout", name], { cwd });
82
+ }
83
+ catch {
84
+ throw new DubError(`Branch '${name}' not found.`);
85
+ }
86
+ }
87
+ /**
88
+ * Deletes a local branch forcefully. Used by undo to remove created branches.
89
+ * @throws {DubError} If the branch does not exist.
90
+ */
91
+ export async function deleteBranch(name, cwd) {
92
+ try {
93
+ await execa("git", ["branch", "-D", name], { cwd });
94
+ }
95
+ catch {
96
+ throw new DubError(`Failed to delete branch '${name}'. It may not exist.`);
97
+ }
98
+ }
99
+ /**
100
+ * Force-moves a branch pointer to a specific commit SHA.
101
+ * Used by undo to reset branches to their pre-operation tips.
102
+ */
103
+ export async function forceBranchTo(name, sha, cwd) {
104
+ try {
105
+ const current = await getCurrentBranch(cwd).catch(() => null);
106
+ if (current === name) {
107
+ await execa("git", ["reset", "--hard", sha], { cwd });
108
+ }
109
+ else {
110
+ await execa("git", ["branch", "-f", name, sha], { cwd });
111
+ }
112
+ }
113
+ catch (error) {
114
+ if (error instanceof DubError)
115
+ throw error;
116
+ throw new DubError(`Failed to reset branch '${name}' to ${sha}.`);
117
+ }
118
+ }
119
+ /**
120
+ * Checks whether the working tree is clean (no uncommitted changes).
121
+ * @returns `true` if clean (no output from `git status --porcelain`).
122
+ */
123
+ export async function isWorkingTreeClean(cwd) {
124
+ const { stdout } = await execa("git", ["status", "--porcelain"], { cwd });
125
+ return stdout.trim() === "";
126
+ }
127
+ /**
128
+ * Performs `git rebase --onto` to move a branch from one base to another.
129
+ *
130
+ * @param newBase - The commit/branch to rebase onto
131
+ * @param oldBase - The old base commit to replay from
132
+ * @param branch - The branch being rebased
133
+ * @throws {DubError} If a merge conflict occurs during rebase
134
+ */
135
+ export async function rebaseOnto(newBase, oldBase, branch, cwd) {
136
+ try {
137
+ await execa("git", ["rebase", "--onto", newBase, oldBase, branch], { cwd });
138
+ }
139
+ catch {
140
+ throw new DubError(`Conflict while restacking '${branch}'.\n` +
141
+ " Resolve conflicts, stage changes, then run: dub restack --continue");
142
+ }
143
+ }
144
+ /**
145
+ * Continues an in-progress rebase after conflicts have been resolved.
146
+ * @throws {DubError} If the rebase continue fails.
147
+ */
148
+ export async function rebaseContinue(cwd) {
149
+ try {
150
+ await execa("git", ["rebase", "--continue"], {
151
+ cwd,
152
+ env: { GIT_EDITOR: "true" },
153
+ });
154
+ }
155
+ catch {
156
+ throw new DubError("Failed to continue rebase. Ensure all conflicts are resolved and staged.");
157
+ }
158
+ }
159
+ /**
160
+ * Returns the merge-base (common ancestor) commit SHA of two branches.
161
+ */
162
+ export async function getMergeBase(a, b, cwd) {
163
+ try {
164
+ const { stdout } = await execa("git", ["merge-base", a, b], { cwd });
165
+ return stdout.trim();
166
+ }
167
+ catch {
168
+ throw new DubError(`Could not find common ancestor between '${a}' and '${b}'.`);
169
+ }
170
+ }
171
+ /**
172
+ * Returns the commit SHA at the tip of a branch.
173
+ * @throws {DubError} If the branch does not exist.
174
+ */
175
+ export async function getBranchTip(name, cwd) {
176
+ try {
177
+ const { stdout } = await execa("git", ["rev-parse", name], { cwd });
178
+ return stdout.trim();
179
+ }
180
+ catch {
181
+ throw new DubError(`Branch '${name}' not found.`);
182
+ }
183
+ }
184
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/lib/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW;IAC1C,IAAI,CAAC;QACJ,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAC7B,KAAK,EACL,CAAC,WAAW,EAAE,uBAAuB,CAAC,EACtC,EAAE,GAAG,EAAE,CACP,CAAC;QACF,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAW;IAC5C,IAAI,CAAC;QACJ,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAAE;YACvE,GAAG;SACH,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,QAAQ,CACjB,2DAA2D,CAC3D,CAAC;IACH,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAW;IACjD,IAAI,CAAC;QACJ,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAC7B,KAAK,EACL,CAAC,WAAW,EAAE,cAAc,EAAE,MAAM,CAAC,EACrC,EAAE,GAAG,EAAE,CACP,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACvB,MAAM,IAAI,QAAQ,CACjB,kEAAkE,CAClE,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,KAAK,YAAY,QAAQ;YAAE,MAAM,KAAK,CAAC;QAC3C,MAAM,IAAI,QAAQ,CACjB,4DAA4D,CAC5D,CAAC;IACH,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,IAAY,EACZ,GAAW;IAEX,IAAI,CAAC;QACJ,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,cAAc,IAAI,EAAE,CAAC,EAAE;YACnE,GAAG;SACH,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,GAAW;IAC3D,IAAI,MAAM,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,QAAQ,CAAC,WAAW,IAAI,mBAAmB,CAAC,CAAC;IACxD,CAAC;IACD,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;AACvD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,GAAW;IAC7D,IAAI,CAAC;QACJ,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,QAAQ,CAAC,WAAW,IAAI,cAAc,CAAC,CAAC;IACnD,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,GAAW;IAC3D,IAAI,CAAC;QACJ,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,QAAQ,CAAC,4BAA4B,IAAI,sBAAsB,CAAC,CAAC;IAC5E,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,IAAY,EACZ,GAAW,EACX,GAAW;IAEX,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC9D,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACtB,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACvD,CAAC;aAAM,CAAC;YACP,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1D,CAAC;IACF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,KAAK,YAAY,QAAQ;YAAE,MAAM,KAAK,CAAC;QAC3C,MAAM,IAAI,QAAQ,CAAC,2BAA2B,IAAI,QAAQ,GAAG,GAAG,CAAC,CAAC;IACnE,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAW;IACnD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IAC1E,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,OAAe,EACf,OAAe,EACf,MAAc,EACd,GAAW;IAEX,IAAI,CAAC;QACJ,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,QAAQ,CACjB,8BAA8B,MAAM,MAAM;YACzC,sEAAsE,CACvE,CAAC;IACH,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAW;IAC/C,IAAI,CAAC;QACJ,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE;YAC5C,GAAG;YACH,GAAG,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;SAC3B,CAAC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,QAAQ,CACjB,0EAA0E,CAC1E,CAAC;IACH,CAAC;AACF,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,CAAS,EACT,CAAS,EACT,GAAW;IAEX,IAAI,CAAC;QACJ,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACrE,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,QAAQ,CACjB,2CAA2C,CAAC,UAAU,CAAC,IAAI,CAC3D,CAAC;IACH,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,GAAW;IAC3D,IAAI,CAAC;QACJ,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACpE,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,QAAQ,CAAC,WAAW,IAAI,cAAc,CAAC,CAAC;IACnD,CAAC;AACF,CAAC"}
@@ -0,0 +1,70 @@
1
+ /** A branch within a stack. */
2
+ export interface Branch {
3
+ /** Branch name, e.g. "feat/api-endpoint" */
4
+ name: string;
5
+ /** Set to "root" for the base branch (e.g. main). Omitted for children. */
6
+ type?: "root";
7
+ /** Name of the parent branch. `null` only for root branches. */
8
+ parent: string | null;
9
+ /** GitHub PR URL. Populated in Phase 2. */
10
+ pr_link: string | null;
11
+ }
12
+ /** A stack of dependent branches. */
13
+ export interface Stack {
14
+ /** Unique identifier for this stack. */
15
+ id: string;
16
+ /** Ordered list of branches in the stack. */
17
+ branches: Branch[];
18
+ }
19
+ /** Root state persisted to `.git/dubstack/state.json`. */
20
+ export interface DubState {
21
+ /** All tracked stacks in this repository. */
22
+ stacks: Stack[];
23
+ }
24
+ /**
25
+ * Returns the absolute path to the dubstack state file.
26
+ * @throws {DubError} If not inside a git repository.
27
+ */
28
+ export declare function getStatePath(cwd: string): Promise<string>;
29
+ /**
30
+ * Returns the absolute path to the dubstack directory inside `.git`.
31
+ * @throws {DubError} If not inside a git repository.
32
+ */
33
+ export declare function getDubDir(cwd: string): Promise<string>;
34
+ /**
35
+ * Reads and parses the dubstack state file.
36
+ * @throws {DubError} If the state file is missing or contains invalid JSON.
37
+ */
38
+ export declare function readState(cwd: string): Promise<DubState>;
39
+ /**
40
+ * Writes the dubstack state to disk.
41
+ * Creates the parent directory if it doesn't exist.
42
+ */
43
+ export declare function writeState(state: DubState, cwd: string): Promise<void>;
44
+ /**
45
+ * Initializes the dubstack state directory and file.
46
+ * Idempotent — returns `"already_exists"` if already initialized.
47
+ *
48
+ * @returns `"created"` if freshly initialized, `"already_exists"` if state file already present.
49
+ */
50
+ export declare function initState(cwd: string): Promise<"created" | "already_exists">;
51
+ /**
52
+ * Finds the stack containing a given branch.
53
+ * @returns The matching stack, or `undefined` if the branch isn't tracked.
54
+ */
55
+ export declare function findStackForBranch(state: DubState, name: string): Stack | undefined;
56
+ /**
57
+ * Adds a child branch to the state, linking it to its parent.
58
+ *
59
+ * Decision tree:
60
+ * 1. If `child` already exists in any stack → throws `DubError` (no duplicates)
61
+ * 2. If `parent` is found in an existing stack → appends child to that stack
62
+ * 3. If `parent` is not in any stack → creates a new stack with parent as root
63
+ *
64
+ * @param state - The state to mutate (modified in place)
65
+ * @param child - Name of the new branch
66
+ * @param parent - Name of the parent branch
67
+ * @throws {DubError} If child branch already exists in state
68
+ */
69
+ export declare function addBranchToStack(state: DubState, child: string, parent: string): void;
70
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/lib/state.ts"],"names":[],"mappings":"AAMA,+BAA+B;AAC/B,MAAM,WAAW,MAAM;IACtB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,qCAAqC;AACrC,MAAM,WAAW,KAAK;IACrB,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,0DAA0D;AAC1D,MAAM,WAAW,QAAQ;IACxB,6CAA6C;IAC7C,MAAM,EAAE,KAAK,EAAE,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG/D;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAa9D;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO5E;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC9B,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,SAAS,GAAG,gBAAgB,CAAC,CAYvC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,MAAM,GACV,KAAK,GAAG,SAAS,CAInB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACZ,IAAI,CAsBN"}
@@ -0,0 +1,110 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { DubError } from "./errors.js";
5
+ import { getRepoRoot } from "./git.js";
6
+ /**
7
+ * Returns the absolute path to the dubstack state file.
8
+ * @throws {DubError} If not inside a git repository.
9
+ */
10
+ export async function getStatePath(cwd) {
11
+ const root = await getRepoRoot(cwd);
12
+ return path.join(root, ".git", "dubstack", "state.json");
13
+ }
14
+ /**
15
+ * Returns the absolute path to the dubstack directory inside `.git`.
16
+ * @throws {DubError} If not inside a git repository.
17
+ */
18
+ export async function getDubDir(cwd) {
19
+ const root = await getRepoRoot(cwd);
20
+ return path.join(root, ".git", "dubstack");
21
+ }
22
+ /**
23
+ * Reads and parses the dubstack state file.
24
+ * @throws {DubError} If the state file is missing or contains invalid JSON.
25
+ */
26
+ export async function readState(cwd) {
27
+ const statePath = await getStatePath(cwd);
28
+ if (!fs.existsSync(statePath)) {
29
+ throw new DubError("DubStack is not initialized. Run 'dub init' first.");
30
+ }
31
+ try {
32
+ const raw = fs.readFileSync(statePath, "utf-8");
33
+ return JSON.parse(raw);
34
+ }
35
+ catch {
36
+ throw new DubError("State file is corrupted. Delete .git/dubstack and run 'dub init' to re-initialize.");
37
+ }
38
+ }
39
+ /**
40
+ * Writes the dubstack state to disk.
41
+ * Creates the parent directory if it doesn't exist.
42
+ */
43
+ export async function writeState(state, cwd) {
44
+ const statePath = await getStatePath(cwd);
45
+ const dir = path.dirname(statePath);
46
+ if (!fs.existsSync(dir)) {
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ }
49
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
50
+ }
51
+ /**
52
+ * Initializes the dubstack state directory and file.
53
+ * Idempotent — returns `"already_exists"` if already initialized.
54
+ *
55
+ * @returns `"created"` if freshly initialized, `"already_exists"` if state file already present.
56
+ */
57
+ export async function initState(cwd) {
58
+ const statePath = await getStatePath(cwd);
59
+ const dir = path.dirname(statePath);
60
+ if (fs.existsSync(statePath)) {
61
+ return "already_exists";
62
+ }
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ const emptyState = { stacks: [] };
65
+ fs.writeFileSync(statePath, `${JSON.stringify(emptyState, null, 2)}\n`);
66
+ return "created";
67
+ }
68
+ /**
69
+ * Finds the stack containing a given branch.
70
+ * @returns The matching stack, or `undefined` if the branch isn't tracked.
71
+ */
72
+ export function findStackForBranch(state, name) {
73
+ return state.stacks.find((stack) => stack.branches.some((b) => b.name === name));
74
+ }
75
+ /**
76
+ * Adds a child branch to the state, linking it to its parent.
77
+ *
78
+ * Decision tree:
79
+ * 1. If `child` already exists in any stack → throws `DubError` (no duplicates)
80
+ * 2. If `parent` is found in an existing stack → appends child to that stack
81
+ * 3. If `parent` is not in any stack → creates a new stack with parent as root
82
+ *
83
+ * @param state - The state to mutate (modified in place)
84
+ * @param child - Name of the new branch
85
+ * @param parent - Name of the parent branch
86
+ * @throws {DubError} If child branch already exists in state
87
+ */
88
+ export function addBranchToStack(state, child, parent) {
89
+ if (findStackForBranch(state, child)) {
90
+ throw new DubError(`Branch '${child}' is already tracked in a stack.`);
91
+ }
92
+ const childBranch = { name: child, parent, pr_link: null };
93
+ const existingStack = findStackForBranch(state, parent);
94
+ if (existingStack) {
95
+ existingStack.branches.push(childBranch);
96
+ }
97
+ else {
98
+ const rootBranch = {
99
+ name: parent,
100
+ type: "root",
101
+ parent: null,
102
+ pr_link: null,
103
+ };
104
+ state.stacks.push({
105
+ id: crypto.randomUUID(),
106
+ branches: [rootBranch, childBranch],
107
+ });
108
+ }
109
+ }
110
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/lib/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { DubState } from "./state.js";
2
+ /**
3
+ * Snapshot of system state before a mutation, used by `dub undo`.
4
+ * Only one undo level is supported — each new mutation overwrites the previous snapshot.
5
+ */
6
+ export interface UndoEntry {
7
+ /** Which command created this snapshot. */
8
+ operation: "create" | "restack";
9
+ /** ISO timestamp of when the snapshot was taken. */
10
+ timestamp: string;
11
+ /** The branch user was on before the operation. */
12
+ previousBranch: string;
13
+ /** Full copy of state.json before mutation. */
14
+ previousState: DubState;
15
+ /** Map of branch name → commit SHA before mutation. */
16
+ branchTips: Record<string, string>;
17
+ /** Branches created by this operation (to be deleted on undo). */
18
+ createdBranches: string[];
19
+ }
20
+ /**
21
+ * Saves an undo entry to disk. Overwrites any previous entry (1 level only).
22
+ */
23
+ export declare function saveUndoEntry(entry: UndoEntry, cwd: string): Promise<void>;
24
+ /**
25
+ * Reads the most recent undo entry.
26
+ * @throws {DubError} If no undo entry exists.
27
+ */
28
+ export declare function readUndoEntry(cwd: string): Promise<UndoEntry>;
29
+ /**
30
+ * Deletes the undo entry file. Called after a successful undo.
31
+ */
32
+ export declare function clearUndoEntry(cwd: string): Promise<void>;
33
+ //# sourceMappingURL=undo-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"undo-log.d.ts","sourceRoot":"","sources":["../../src/lib/undo-log.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C;;;GAGG;AACH,MAAM,WAAW,SAAS;IACzB,2CAA2C;IAC3C,SAAS,EAAE,QAAQ,GAAG,SAAS,CAAC;IAChC,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,+CAA+C;IAC/C,aAAa,EAAE,QAAQ,CAAC;IACxB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,kEAAkE;IAClE,eAAe,EAAE,MAAM,EAAE,CAAC;CAC1B;AAOD;;GAEG;AACH,wBAAsB,aAAa,CAClC,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAOnE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/D"}
@@ -0,0 +1,37 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { DubError } from "./errors.js";
4
+ import { getDubDir } from "./state.js";
5
+ async function getUndoPath(cwd) {
6
+ const dubDir = await getDubDir(cwd);
7
+ return path.join(dubDir, "undo.json");
8
+ }
9
+ /**
10
+ * Saves an undo entry to disk. Overwrites any previous entry (1 level only).
11
+ */
12
+ export async function saveUndoEntry(entry, cwd) {
13
+ const undoPath = await getUndoPath(cwd);
14
+ fs.writeFileSync(undoPath, `${JSON.stringify(entry, null, 2)}\n`);
15
+ }
16
+ /**
17
+ * Reads the most recent undo entry.
18
+ * @throws {DubError} If no undo entry exists.
19
+ */
20
+ export async function readUndoEntry(cwd) {
21
+ const undoPath = await getUndoPath(cwd);
22
+ if (!fs.existsSync(undoPath)) {
23
+ throw new DubError("Nothing to undo.");
24
+ }
25
+ const raw = fs.readFileSync(undoPath, "utf-8");
26
+ return JSON.parse(raw);
27
+ }
28
+ /**
29
+ * Deletes the undo entry file. Called after a successful undo.
30
+ */
31
+ export async function clearUndoEntry(cwd) {
32
+ const undoPath = await getUndoPath(cwd);
33
+ if (fs.existsSync(undoPath)) {
34
+ fs.unlinkSync(undoPath);
35
+ }
36
+ }
37
+ //# sourceMappingURL=undo-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"undo-log.js","sourceRoot":"","sources":["../../src/lib/undo-log.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAqBvC,KAAK,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AACvC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,KAAgB,EAChB,GAAW;IAEX,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;IACxC,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACnE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW;IAC9C,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IACxC,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAc,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAW;IAC/C,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC;AACF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "dubstack",
3
+ "version": "0.1.3",
4
+ "description": "CLI tool for managing stacked diffs (dependent git branches)",
5
+ "type": "module",
6
+ "bin": {
7
+ "dub": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/wiseiodev/dubstack.git"
18
+ },
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "keywords": [
23
+ "git",
24
+ "stacked-diffs",
25
+ "cli",
26
+ "branches"
27
+ ],
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "chalk": "^5.6.2",
31
+ "commander": "^14.0.3",
32
+ "execa": "^9.6.1"
33
+ },
34
+ "devDependencies": {
35
+ "@biomejs/biome": "^2.4.2",
36
+ "@types/node": "^25.2.3",
37
+ "tsx": "^4.21.0",
38
+ "typescript": "^5.9.3",
39
+ "vitest": "^4.0.18"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "dev": "tsx src/index.ts",
44
+ "test": "vitest run",
45
+ "typecheck": "tsc --noEmit",
46
+ "checks": "biome check .",
47
+ "checks:fix": "biome check --write ."
48
+ }
49
+ }