@procedures/git 0.0.0-stage → 0.2.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1130 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/index.d.ts +20 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +20 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/module.d.ts +22 -0
  10. package/dist/module.d.ts.map +1 -0
  11. package/dist/module.js +514 -0
  12. package/dist/module.js.map +1 -0
  13. package/dist/procedures/git/add.d.ts +12 -0
  14. package/dist/procedures/git/add.d.ts.map +1 -0
  15. package/dist/procedures/git/add.js +26 -0
  16. package/dist/procedures/git/add.js.map +1 -0
  17. package/dist/procedures/git/args.d.ts +15 -0
  18. package/dist/procedures/git/args.d.ts.map +1 -0
  19. package/dist/procedures/git/args.js +20 -0
  20. package/dist/procedures/git/args.js.map +1 -0
  21. package/dist/procedures/git/branch.d.ts +12 -0
  22. package/dist/procedures/git/branch.d.ts.map +1 -0
  23. package/dist/procedures/git/branch.js +54 -0
  24. package/dist/procedures/git/branch.js.map +1 -0
  25. package/dist/procedures/git/checkout.d.ts +12 -0
  26. package/dist/procedures/git/checkout.d.ts.map +1 -0
  27. package/dist/procedures/git/checkout.js +23 -0
  28. package/dist/procedures/git/checkout.js.map +1 -0
  29. package/dist/procedures/git/clone.d.ts +12 -0
  30. package/dist/procedures/git/clone.d.ts.map +1 -0
  31. package/dist/procedures/git/clone.js +28 -0
  32. package/dist/procedures/git/clone.js.map +1 -0
  33. package/dist/procedures/git/commit.d.ts +12 -0
  34. package/dist/procedures/git/commit.d.ts.map +1 -0
  35. package/dist/procedures/git/commit.js +41 -0
  36. package/dist/procedures/git/commit.js.map +1 -0
  37. package/dist/procedures/git/diff.d.ts +18 -0
  38. package/dist/procedures/git/diff.d.ts.map +1 -0
  39. package/dist/procedures/git/diff.js +58 -0
  40. package/dist/procedures/git/diff.js.map +1 -0
  41. package/dist/procedures/git/fetch.d.ts +12 -0
  42. package/dist/procedures/git/fetch.d.ts.map +1 -0
  43. package/dist/procedures/git/fetch.js +29 -0
  44. package/dist/procedures/git/fetch.js.map +1 -0
  45. package/dist/procedures/git/index.d.ts +15 -0
  46. package/dist/procedures/git/index.d.ts.map +1 -0
  47. package/dist/procedures/git/index.js +15 -0
  48. package/dist/procedures/git/index.js.map +1 -0
  49. package/dist/procedures/git/init.d.ts +12 -0
  50. package/dist/procedures/git/init.d.ts.map +1 -0
  51. package/dist/procedures/git/init.js +32 -0
  52. package/dist/procedures/git/init.js.map +1 -0
  53. package/dist/procedures/git/log.d.ts +12 -0
  54. package/dist/procedures/git/log.d.ts.map +1 -0
  55. package/dist/procedures/git/log.js +35 -0
  56. package/dist/procedures/git/log.js.map +1 -0
  57. package/dist/procedures/git/predicates.d.ts +40 -0
  58. package/dist/procedures/git/predicates.d.ts.map +1 -0
  59. package/dist/procedures/git/predicates.js +76 -0
  60. package/dist/procedures/git/predicates.js.map +1 -0
  61. package/dist/procedures/git/pull.d.ts +12 -0
  62. package/dist/procedures/git/pull.d.ts.map +1 -0
  63. package/dist/procedures/git/pull.js +36 -0
  64. package/dist/procedures/git/pull.js.map +1 -0
  65. package/dist/procedures/git/push.d.ts +12 -0
  66. package/dist/procedures/git/push.d.ts.map +1 -0
  67. package/dist/procedures/git/push.js +39 -0
  68. package/dist/procedures/git/push.js.map +1 -0
  69. package/dist/procedures/git/remote.d.ts +13 -0
  70. package/dist/procedures/git/remote.d.ts.map +1 -0
  71. package/dist/procedures/git/remote.js +30 -0
  72. package/dist/procedures/git/remote.js.map +1 -0
  73. package/dist/procedures/git/run.d.ts +32 -0
  74. package/dist/procedures/git/run.d.ts.map +1 -0
  75. package/dist/procedures/git/run.js +60 -0
  76. package/dist/procedures/git/run.js.map +1 -0
  77. package/dist/procedures/git/stash.d.ts +38 -0
  78. package/dist/procedures/git/stash.d.ts.map +1 -0
  79. package/dist/procedures/git/stash.js +211 -0
  80. package/dist/procedures/git/stash.js.map +1 -0
  81. package/dist/procedures/git/status.d.ts +17 -0
  82. package/dist/procedures/git/status.d.ts.map +1 -0
  83. package/dist/procedures/git/status.js +107 -0
  84. package/dist/procedures/git/status.js.map +1 -0
  85. package/dist/procedures/git.d.ts +5 -0
  86. package/dist/procedures/git.d.ts.map +1 -0
  87. package/dist/procedures/git.js +5 -0
  88. package/dist/procedures/git.js.map +1 -0
  89. package/dist/register.d.ts +7 -0
  90. package/dist/register.d.ts.map +1 -0
  91. package/dist/register.js +9 -0
  92. package/dist/register.js.map +1 -0
  93. package/dist/types.d.ts +350 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/types.js +397 -0
  96. package/dist/types.js.map +1 -0
  97. package/package.json +77 -3
  98. package/src/index.ts +20 -0
  99. package/src/module.ts +656 -0
  100. package/src/procedures/git/add.ts +29 -0
  101. package/src/procedures/git/args.ts +21 -0
  102. package/src/procedures/git/branch.ts +61 -0
  103. package/src/procedures/git/checkout.ts +27 -0
  104. package/src/procedures/git/clone.ts +33 -0
  105. package/src/procedures/git/commit.ts +46 -0
  106. package/src/procedures/git/diff.ts +62 -0
  107. package/src/procedures/git/fetch.ts +32 -0
  108. package/src/procedures/git/index.ts +22 -0
  109. package/src/procedures/git/init.ts +43 -0
  110. package/src/procedures/git/log.ts +40 -0
  111. package/src/procedures/git/predicates.ts +90 -0
  112. package/src/procedures/git/pull.ts +45 -0
  113. package/src/procedures/git/push.ts +42 -0
  114. package/src/procedures/git/remote.ts +31 -0
  115. package/src/procedures/git/run.ts +70 -0
  116. package/src/procedures/git/stash.ts +255 -0
  117. package/src/procedures/git/status.ts +98 -0
  118. package/src/procedures/git.ts +5 -0
  119. package/src/register.ts +11 -0
  120. package/src/types.ts +815 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * git.stash.* procedures
3
+ *
4
+ * Stash operations for saving and restoring work in progress.
5
+ * Includes export/import for snapshot storage.
6
+ */
7
+
8
+ import { gitArg } from "./args.js";
9
+ import { git, GitError, type GitContext } from "./run.js";
10
+ import type {
11
+ GitStashListInput,
12
+ GitStashListOutput,
13
+ GitStashPushInput,
14
+ GitStashPushOutput,
15
+ GitStashPopInput,
16
+ GitStashPopOutput,
17
+ GitStashApplyInput,
18
+ GitStashApplyOutput,
19
+ GitStashDropInput,
20
+ GitStashDropOutput,
21
+ GitStashExportInput,
22
+ GitStashExportOutput,
23
+ GitStashImportInput,
24
+ GitStashImportOutput,
25
+ GitStashEntry,
26
+ } from "../../types.js";
27
+
28
+ /**
29
+ * True when git stopped on a merge conflict. Git writes "CONFLICT (content): ..." to stdout,
30
+ * so the message alone (which has stderr) does not show it. (Before, the procedures examined
31
+ * only the message, and never reported a conflict.)
32
+ */
33
+ function isConflict(error: unknown): boolean {
34
+ return error instanceof GitError && /conflict/i.test(`${error.stdout}\n${error.stderr}`);
35
+ }
36
+
37
+ /** The message of a git failure. An abort is not a git failure: it throws again. */
38
+ function failure(error: unknown): string {
39
+ if (!(error instanceof GitError)) throw error;
40
+ return error.message;
41
+ }
42
+
43
+ function stashRef(index: number | undefined): string {
44
+ return index !== undefined ? `stash@{${index}}` : "stash@{0}";
45
+ }
46
+
47
+ /**
48
+ * List all stashes
49
+ */
50
+ export async function gitStashList(input: GitStashListInput, ctx: GitContext = {}): Promise<GitStashListOutput> {
51
+ const cwd = input.cwd || process.cwd();
52
+
53
+ let output: string;
54
+ try {
55
+ // Get stash list with format: stash@{n}: WIP on branch: message
56
+ output = await git(["stash", "list", "--format=%H%x00%gd%x00%gs"], { cwd, signal: ctx.signal });
57
+ } catch (error) {
58
+ // No stashes or not a git repo
59
+ failure(error);
60
+ return { stashes: [], count: 0 };
61
+ }
62
+
63
+ const stashes: GitStashEntry[] = [];
64
+ for (const line of output.split("\n").filter(Boolean)) {
65
+ const parts = line.split("\0");
66
+ const hash = parts[0] || "";
67
+ const ref = parts[1] || "";
68
+ const message = parts[2] || "";
69
+ // Extract index from stash@{n}
70
+ const indexMatch = ref.match(/stash@\{(\d+)\}/);
71
+ const index = indexMatch && indexMatch[1] ? parseInt(indexMatch[1], 10) : 0;
72
+
73
+ stashes.push({
74
+ index,
75
+ ref,
76
+ hash,
77
+ message,
78
+ });
79
+ }
80
+
81
+ return { stashes, count: stashes.length };
82
+ }
83
+
84
+ /**
85
+ * Push changes to stash
86
+ */
87
+ export async function gitStashPush(input: GitStashPushInput, ctx: GitContext = {}): Promise<GitStashPushOutput> {
88
+ const cwd = input.cwd || process.cwd();
89
+ const run = { cwd, signal: ctx.signal };
90
+
91
+ const args: string[] = ["stash", "push"];
92
+
93
+ if (input.message) {
94
+ args.push("-m", input.message);
95
+ }
96
+
97
+ if (input.includeUntracked) {
98
+ args.push("--include-untracked");
99
+ }
100
+
101
+ if (input.keepIndex) {
102
+ args.push("--keep-index");
103
+ }
104
+
105
+ if (input.paths && input.paths.length > 0) {
106
+ args.push("--");
107
+ args.push(...input.paths);
108
+ }
109
+
110
+ try {
111
+ const output = await git(args, run);
112
+
113
+ // Check if anything was stashed
114
+ if (output.includes("No local changes to save")) {
115
+ return { stashed: false, message: "No local changes to save" };
116
+ }
117
+
118
+ // Get the ref of the new stash
119
+ const refOutput = (await git(["stash", "list", "-1", "--format=%gd"], run)).trim();
120
+
121
+ return {
122
+ stashed: true,
123
+ ref: refOutput || "stash@{0}",
124
+ message: input.message || "WIP",
125
+ };
126
+ } catch (error) {
127
+ return { stashed: false, message: failure(error) || "Failed to stash" };
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Pop stash (apply and remove)
133
+ */
134
+ export async function gitStashPop(input: GitStashPopInput, ctx: GitContext = {}): Promise<GitStashPopOutput> {
135
+ const cwd = input.cwd || process.cwd();
136
+ const ref = stashRef(input.index);
137
+
138
+ try {
139
+ await git(["stash", "pop", gitArg("ref", ref)], { cwd, signal: ctx.signal });
140
+ return { applied: true, ref, dropped: true };
141
+ } catch (error) {
142
+ // Check if it's a conflict
143
+ if (isConflict(error)) {
144
+ return { applied: false, ref, dropped: false, conflict: true };
145
+ }
146
+ failure(error);
147
+ return { applied: false, ref, dropped: false };
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Apply stash without removing
153
+ */
154
+ export async function gitStashApply(input: GitStashApplyInput, ctx: GitContext = {}): Promise<GitStashApplyOutput> {
155
+ const cwd = input.cwd || process.cwd();
156
+ const ref = stashRef(input.index);
157
+
158
+ try {
159
+ await git(["stash", "apply", gitArg("ref", ref)], { cwd, signal: ctx.signal });
160
+ return { applied: true, ref };
161
+ } catch (error) {
162
+ if (isConflict(error)) {
163
+ return { applied: false, ref, conflict: true };
164
+ }
165
+ failure(error);
166
+ return { applied: false, ref };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Drop a stash
172
+ */
173
+ export async function gitStashDrop(input: GitStashDropInput, ctx: GitContext = {}): Promise<GitStashDropOutput> {
174
+ const cwd = input.cwd || process.cwd();
175
+ const ref = stashRef(input.index);
176
+
177
+ try {
178
+ await git(["stash", "drop", gitArg("ref", ref)], { cwd, signal: ctx.signal });
179
+ return { dropped: true, ref };
180
+ } catch (error) {
181
+ failure(error);
182
+ return { dropped: false, ref };
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Export a stash as a patch (for snapshot storage)
188
+ */
189
+ export async function gitStashExport(input: GitStashExportInput, ctx: GitContext = {}): Promise<GitStashExportOutput> {
190
+ const cwd = input.cwd || process.cwd();
191
+ const run = { cwd, signal: ctx.signal };
192
+ const ref = stashRef(input.index);
193
+
194
+ try {
195
+ // Get the stash as a patch
196
+ const patch = await git(["stash", "show", "-p", gitArg("ref", ref)], run);
197
+
198
+ // Get metadata
199
+ const metadata = (await git(["stash", "list", "-1", "--format=%H%x00%gs", ref], run)).trim();
200
+ const metaParts = metadata.split("\0");
201
+ const hash = metaParts[0] || "";
202
+ const message = metaParts[1] || "";
203
+
204
+ // Get untracked files if present (stash^3 contains untracked files if any)
205
+ let untrackedPatch = "";
206
+ try {
207
+ untrackedPatch = await git(["show", `${ref}^3`, "--format=", "--name-only"], run);
208
+ } catch (error) {
209
+ // No untracked files in this stash
210
+ failure(error);
211
+ }
212
+
213
+ return {
214
+ ref,
215
+ hash,
216
+ message,
217
+ patch,
218
+ hasUntracked: untrackedPatch.length > 0,
219
+ untrackedPatch: untrackedPatch || undefined,
220
+ };
221
+ } catch (error) {
222
+ throw new Error(`Failed to export stash ${ref}: ${failure(error)}`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Import a stash from a patch (for snapshot restore)
228
+ * Note: This creates a new stash, it doesn't recreate the exact stash state
229
+ */
230
+ export async function gitStashImport(input: GitStashImportInput, ctx: GitContext = {}): Promise<GitStashImportOutput> {
231
+ const cwd = input.cwd || process.cwd();
232
+ const run = { cwd, signal: ctx.signal };
233
+
234
+ try {
235
+ // Apply the patch to working directory
236
+ await git(["apply", "--3way"], { ...run, input: input.patch });
237
+
238
+ // Stash the changes with the original message
239
+ const message = input.message || "Imported stash";
240
+ const args = ["stash", "push", "-m", message];
241
+
242
+ if (input.includeUntracked) {
243
+ args.push("--include-untracked");
244
+ }
245
+
246
+ await git(args, run);
247
+
248
+ // Get the new stash ref
249
+ const ref = (await git(["stash", "list", "-1", "--format=%gd"], run)).trim();
250
+
251
+ return { imported: true, ref: ref || "stash@{0}" };
252
+ } catch (error) {
253
+ return { imported: false, error: failure(error) || "Failed to import stash" };
254
+ }
255
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * git.status procedure
3
+ *
4
+ * Get git status
5
+ *
6
+ * One call of `git status --porcelain=v2 --branch -z` gives the branch, the ahead and behind
7
+ * counts and the files. With `-z`, git writes each path as it is: no quotes, and a rename gives
8
+ * its old path in a separate field (deep dive WRP-7). Before, the procedure read the quoted form,
9
+ * so a path with a space kept its quotes, and a rename gave the text "old -> new" as the path.
10
+ */
11
+
12
+ import { git, zFields, type GitContext } from "./run.js";
13
+ import type { GitStatusInput, GitStatusOutput, GitStatusFile } from "../../types.js";
14
+
15
+ /** The status of one entry, from its two status letters (index, then work tree). */
16
+ function classify(xy: string): Pick<GitStatusFile, "status" | "staged"> {
17
+ const index = xy[0];
18
+ const worktree = xy[1];
19
+ if (index === "A") return { status: "added", staged: true };
20
+ if (index === "D") return { status: "deleted", staged: true };
21
+ if (index === "R") return { status: "renamed", staged: true };
22
+ if (index === "C") return { status: "copied", staged: true };
23
+ if (index === "M" || index === "T") return { status: "modified", staged: true };
24
+ if (worktree === "D") return { status: "deleted", staged: false };
25
+ if (worktree === "A") return { status: "added", staged: false };
26
+ if (worktree === "R") return { status: "renamed", staged: false };
27
+ return { status: "modified", staged: false };
28
+ }
29
+
30
+ /** The text after the first `count` space-separated fields of a line. */
31
+ function afterFields(line: string, count: number): string {
32
+ let at = 0;
33
+ for (let i = 0; i < count; i++) at = line.indexOf(" ", at) + 1;
34
+ return line.slice(at);
35
+ }
36
+
37
+ /**
38
+ * Get git status
39
+ */
40
+ export async function gitStatus(input: GitStatusInput, ctx: GitContext = {}): Promise<GitStatusOutput> {
41
+ const output = await git(["status", "--porcelain=v2", "--branch", "-z"], { cwd: input.cwd, signal: ctx.signal });
42
+
43
+ let branch = "HEAD";
44
+ let ahead = 0;
45
+ let behind = 0;
46
+ const files: GitStatusFile[] = [];
47
+ const lines: string[] = [];
48
+
49
+ const fields = zFields(output);
50
+ for (let i = 0; i < fields.length; i++) {
51
+ const field = fields[i]!;
52
+ if (field.startsWith("# branch.head ")) {
53
+ const head = field.slice("# branch.head ".length);
54
+ branch = head === "(detached)" ? "HEAD" : head;
55
+ } else if (field.startsWith("# branch.ab ")) {
56
+ const match = /\+(\d+) -(\d+)/.exec(field);
57
+ ahead = Number(match?.[1] ?? 0);
58
+ behind = Number(match?.[2] ?? 0);
59
+ } else if (field.startsWith("1 ")) {
60
+ // 1 XY sub mH mI mW hH hI path
61
+ const xy = field.slice(2, 4);
62
+ const path = afterFields(field, 8);
63
+ files.push({ path, ...classify(xy) });
64
+ lines.push(`${xy.replace(/\./g, " ")} ${path}`);
65
+ } else if (field.startsWith("2 ")) {
66
+ // 2 XY sub mH mI mW hH hI Xscore path, then the old path in the next field
67
+ const xy = field.slice(2, 4);
68
+ const path = afterFields(field, 9);
69
+ const from = fields[++i] ?? "";
70
+ files.push({ path, from, ...classify(xy) });
71
+ lines.push(`${xy.replace(/\./g, " ")} ${from} -> ${path}`);
72
+ } else if (field.startsWith("u ")) {
73
+ // An unmerged path: u XY sub m1 m2 m3 mW h1 h2 h3 path
74
+ const xy = field.slice(2, 4);
75
+ const path = afterFields(field, 10);
76
+ files.push({ path, status: "modified", staged: false });
77
+ lines.push(`${xy} ${path}`);
78
+ } else if (field.startsWith("? ")) {
79
+ const path = field.slice(2);
80
+ files.push({ path, status: "untracked", staged: false });
81
+ lines.push(`?? ${path}`);
82
+ } else if (field.startsWith("! ")) {
83
+ const path = field.slice(2);
84
+ files.push({ path, status: "ignored", staged: false });
85
+ lines.push(`!! ${path}`);
86
+ }
87
+ }
88
+
89
+ const result: GitStatusOutput = {
90
+ branch,
91
+ ahead,
92
+ behind,
93
+ files,
94
+ clean: files.length === 0,
95
+ };
96
+ if (input.short) result.lines = lines;
97
+ return result;
98
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Git procedures barrel export
3
+ */
4
+
5
+ export * from "./git/index.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/git, and of the modules that it
3
+ * requires, in the global registry: `import "@procedures/git/register"`. The package root and the
4
+ * `./module` entry register nothing (ARCHITECTURE-PROPOSALS-2026-10, P9).
5
+ */
6
+
7
+ import { registerGitProcedures } from "./module.js";
8
+
9
+ export * from "./module.js";
10
+
11
+ registerGitProcedures();