@qelos/aidev 0.5.0 → 0.5.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.
Files changed (99) hide show
  1. package/.env.aidev.example +8 -0
  2. package/README.md +137 -2
  3. package/dist/cli.js +14 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/accepted.d.ts +7 -2
  6. package/dist/commands/accepted.d.ts.map +1 -1
  7. package/dist/commands/accepted.js +17 -2
  8. package/dist/commands/accepted.js.map +1 -1
  9. package/dist/commands/help.d.ts.map +1 -1
  10. package/dist/commands/help.js +13 -0
  11. package/dist/commands/help.js.map +1 -1
  12. package/dist/commands/run.d.ts +16 -1
  13. package/dist/commands/run.d.ts.map +1 -1
  14. package/dist/commands/run.js +408 -20
  15. package/dist/commands/run.js.map +1 -1
  16. package/dist/commands/tasks.d.ts +1 -0
  17. package/dist/commands/tasks.d.ts.map +1 -1
  18. package/dist/commands/tasks.js +29 -0
  19. package/dist/commands/tasks.js.map +1 -1
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +6 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/github.d.ts +2 -0
  24. package/dist/github.d.ts.map +1 -1
  25. package/dist/github.js +28 -0
  26. package/dist/github.js.map +1 -1
  27. package/dist/hooks.d.ts +16 -1
  28. package/dist/hooks.d.ts.map +1 -1
  29. package/dist/hooks.js +1 -0
  30. package/dist/hooks.js.map +1 -1
  31. package/dist/providers/linear.js +1 -1
  32. package/dist/providers/linear.js.map +1 -1
  33. package/dist/sessions.d.ts +20 -0
  34. package/dist/sessions.d.ts.map +1 -0
  35. package/dist/sessions.js +171 -0
  36. package/dist/sessions.js.map +1 -0
  37. package/dist/types.d.ts +2 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/__tests__/ai-runners.test.d.ts +0 -2
  41. package/dist/__tests__/ai-runners.test.d.ts.map +0 -1
  42. package/dist/__tests__/ai-runners.test.js +0 -367
  43. package/dist/__tests__/ai-runners.test.js.map +0 -1
  44. package/dist/__tests__/clickup-format.test.d.ts +0 -2
  45. package/dist/__tests__/clickup-format.test.d.ts.map +0 -1
  46. package/dist/__tests__/clickup-format.test.js +0 -256
  47. package/dist/__tests__/clickup-format.test.js.map +0 -1
  48. package/dist/__tests__/config.test.d.ts +0 -2
  49. package/dist/__tests__/config.test.d.ts.map +0 -1
  50. package/dist/__tests__/config.test.js +0 -418
  51. package/dist/__tests__/config.test.js.map +0 -1
  52. package/dist/__tests__/git.test.d.ts +0 -2
  53. package/dist/__tests__/git.test.d.ts.map +0 -1
  54. package/dist/__tests__/git.test.js +0 -697
  55. package/dist/__tests__/git.test.js.map +0 -1
  56. package/dist/__tests__/github.test.d.ts +0 -2
  57. package/dist/__tests__/github.test.d.ts.map +0 -1
  58. package/dist/__tests__/github.test.js +0 -209
  59. package/dist/__tests__/github.test.js.map +0 -1
  60. package/dist/__tests__/help.test.d.ts +0 -2
  61. package/dist/__tests__/help.test.d.ts.map +0 -1
  62. package/dist/__tests__/help.test.js +0 -34
  63. package/dist/__tests__/help.test.js.map +0 -1
  64. package/dist/__tests__/hooks.test.d.ts +0 -2
  65. package/dist/__tests__/hooks.test.d.ts.map +0 -1
  66. package/dist/__tests__/hooks.test.js +0 -333
  67. package/dist/__tests__/hooks.test.js.map +0 -1
  68. package/dist/__tests__/init.test.d.ts +0 -2
  69. package/dist/__tests__/init.test.d.ts.map +0 -1
  70. package/dist/__tests__/init.test.js +0 -596
  71. package/dist/__tests__/init.test.js.map +0 -1
  72. package/dist/__tests__/local-provider.test.d.ts +0 -2
  73. package/dist/__tests__/local-provider.test.d.ts.map +0 -1
  74. package/dist/__tests__/local-provider.test.js +0 -631
  75. package/dist/__tests__/local-provider.test.js.map +0 -1
  76. package/dist/__tests__/lockfile.test.d.ts +0 -2
  77. package/dist/__tests__/lockfile.test.d.ts.map +0 -1
  78. package/dist/__tests__/lockfile.test.js +0 -144
  79. package/dist/__tests__/lockfile.test.js.map +0 -1
  80. package/dist/__tests__/permissions.test.d.ts +0 -2
  81. package/dist/__tests__/permissions.test.d.ts.map +0 -1
  82. package/dist/__tests__/permissions.test.js +0 -151
  83. package/dist/__tests__/permissions.test.js.map +0 -1
  84. package/dist/__tests__/platform.test.d.ts +0 -2
  85. package/dist/__tests__/platform.test.d.ts.map +0 -1
  86. package/dist/__tests__/platform.test.js +0 -329
  87. package/dist/__tests__/platform.test.js.map +0 -1
  88. package/dist/__tests__/providers.test.d.ts +0 -2
  89. package/dist/__tests__/providers.test.d.ts.map +0 -1
  90. package/dist/__tests__/providers.test.js +0 -925
  91. package/dist/__tests__/providers.test.js.map +0 -1
  92. package/dist/__tests__/run.test.d.ts +0 -2
  93. package/dist/__tests__/run.test.d.ts.map +0 -1
  94. package/dist/__tests__/run.test.js +0 -529
  95. package/dist/__tests__/run.test.js.map +0 -1
  96. package/dist/__tests__/schedule.test.d.ts +0 -2
  97. package/dist/__tests__/schedule.test.d.ts.map +0 -1
  98. package/dist/__tests__/schedule.test.js +0 -258
  99. package/dist/__tests__/schedule.test.js.map +0 -1
@@ -1,697 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- const node_test_1 = require("node:test");
40
- const strict_1 = __importDefault(require("node:assert/strict"));
41
- const node_child_process_1 = require("node:child_process");
42
- const fs = __importStar(require("node:fs"));
43
- const os = __importStar(require("node:os"));
44
- const path = __importStar(require("node:path"));
45
- const git_1 = require("../git");
46
- // ─── slugify ──────────────────────────────────────────────────────────────────
47
- (0, node_test_1.describe)('slugify', () => {
48
- (0, node_test_1.it)('lowercases input', () => {
49
- strict_1.default.equal((0, git_1.slugify)('FIX LOGIN BUG'), 'fix-login-bug');
50
- });
51
- (0, node_test_1.it)('replaces spaces with dashes', () => {
52
- strict_1.default.equal((0, git_1.slugify)('fix login bug'), 'fix-login-bug');
53
- });
54
- (0, node_test_1.it)('removes special characters', () => {
55
- strict_1.default.equal((0, git_1.slugify)('feat: add @user support!'), 'feat-add-user-support');
56
- });
57
- (0, node_test_1.it)('collapses multiple separators into one dash', () => {
58
- strict_1.default.equal((0, git_1.slugify)('hello---world'), 'hello-world');
59
- });
60
- (0, node_test_1.it)('strips leading and trailing dashes', () => {
61
- strict_1.default.equal((0, git_1.slugify)('---hello---'), 'hello');
62
- });
63
- (0, node_test_1.it)('truncates to 50 characters', () => {
64
- strict_1.default.equal((0, git_1.slugify)('a'.repeat(100)).length, 50);
65
- });
66
- (0, node_test_1.it)('handles empty string', () => {
67
- strict_1.default.equal((0, git_1.slugify)(''), '');
68
- });
69
- (0, node_test_1.it)('handles string with only special chars', () => {
70
- strict_1.default.equal((0, git_1.slugify)('!!!'), '');
71
- });
72
- });
73
- // ─── isProtectedBranch ────────────────────────────────────────────────────────
74
- (0, node_test_1.describe)('isProtectedBranch', () => {
75
- (0, node_test_1.it)('recognizes main as protected', () => {
76
- strict_1.default.equal((0, git_1.isProtectedBranch)('main'), true);
77
- });
78
- (0, node_test_1.it)('recognizes master as protected', () => {
79
- strict_1.default.equal((0, git_1.isProtectedBranch)('master'), true);
80
- });
81
- (0, node_test_1.it)('recognizes develop as protected', () => {
82
- strict_1.default.equal((0, git_1.isProtectedBranch)('develop'), true);
83
- });
84
- (0, node_test_1.it)('recognizes production as protected', () => {
85
- strict_1.default.equal((0, git_1.isProtectedBranch)('production'), true);
86
- });
87
- (0, node_test_1.it)('is case-insensitive', () => {
88
- strict_1.default.equal((0, git_1.isProtectedBranch)('MAIN'), true);
89
- strict_1.default.equal((0, git_1.isProtectedBranch)('Master'), true);
90
- strict_1.default.equal((0, git_1.isProtectedBranch)('DEVELOP'), true);
91
- });
92
- (0, node_test_1.it)('does not flag feature branches', () => {
93
- strict_1.default.equal((0, git_1.isProtectedBranch)('feature/login'), false);
94
- strict_1.default.equal((0, git_1.isProtectedBranch)('abc123/fix-bug'), false);
95
- strict_1.default.equal((0, git_1.isProtectedBranch)('hotfix/urgent'), false);
96
- });
97
- });
98
- // ─── Integration tests using temp git repos ───────────────────────────────────
99
- function gitCmd(args, cwd) {
100
- const result = (0, node_child_process_1.spawnSync)('git', args, { cwd, encoding: 'utf8' });
101
- if (result.status !== 0) {
102
- throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`);
103
- }
104
- }
105
- function initRepo(dir) {
106
- gitCmd(['init', '-b', 'main'], dir);
107
- gitCmd(['config', 'user.email', 'test@test.com'], dir);
108
- gitCmd(['config', 'user.name', 'Test'], dir);
109
- fs.writeFileSync(path.join(dir, 'README.md'), '# test\n');
110
- gitCmd(['add', '.'], dir);
111
- gitCmd(['commit', '-m', 'initial commit'], dir);
112
- }
113
- (0, node_test_1.describe)('getCurrentBranch (integration)', () => {
114
- let tmpDir;
115
- let originalCwd;
116
- (0, node_test_1.beforeEach)(() => {
117
- originalCwd = process.cwd();
118
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
119
- initRepo(tmpDir);
120
- process.chdir(tmpDir);
121
- });
122
- (0, node_test_1.afterEach)(() => {
123
- process.chdir(originalCwd);
124
- fs.rmSync(tmpDir, { recursive: true, force: true });
125
- });
126
- (0, node_test_1.it)('returns the current branch name', () => {
127
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'main');
128
- });
129
- (0, node_test_1.it)('returns the new branch after checkout -b', () => {
130
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
131
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'feature/test');
132
- });
133
- });
134
- (0, node_test_1.describe)('createBranch with expectedBase (integration)', () => {
135
- let tmpDir;
136
- let originalCwd;
137
- (0, node_test_1.beforeEach)(() => {
138
- originalCwd = process.cwd();
139
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
140
- initRepo(tmpDir);
141
- process.chdir(tmpDir);
142
- });
143
- (0, node_test_1.afterEach)(() => {
144
- process.chdir(originalCwd);
145
- fs.rmSync(tmpDir, { recursive: true, force: true });
146
- });
147
- (0, node_test_1.it)('succeeds when on the expected base branch', () => {
148
- strict_1.default.equal((0, git_1.createBranch)('task123/fix-bug', 'main'), true);
149
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'task123/fix-bug');
150
- });
151
- (0, node_test_1.it)('fails when not on the expected base branch', () => {
152
- gitCmd(['checkout', '-b', 'some-other-branch'], tmpDir);
153
- strict_1.default.equal((0, git_1.createBranch)('task123/fix-bug', 'main'), false);
154
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'some-other-branch');
155
- });
156
- (0, node_test_1.it)('works without expectedBase (backward compatible)', () => {
157
- strict_1.default.equal((0, git_1.createBranch)('task123/fix-bug'), true);
158
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'task123/fix-bug');
159
- });
160
- });
161
- (0, node_test_1.describe)('createBranchFromRemote (integration)', () => {
162
- let tmpDir;
163
- let bareDir;
164
- let originalCwd;
165
- (0, node_test_1.beforeEach)(() => {
166
- originalCwd = process.cwd();
167
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
168
- bareDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-bare-'));
169
- gitCmd(['init', '--bare', '-b', 'main'], bareDir);
170
- initRepo(tmpDir);
171
- gitCmd(['remote', 'add', 'origin', bareDir], tmpDir);
172
- gitCmd(['push', 'origin', 'main'], tmpDir);
173
- process.chdir(tmpDir);
174
- });
175
- (0, node_test_1.afterEach)(() => {
176
- process.chdir(originalCwd);
177
- fs.rmSync(tmpDir, { recursive: true, force: true });
178
- fs.rmSync(bareDir, { recursive: true, force: true });
179
- });
180
- (0, node_test_1.it)('creates a branch from origin/main at the latest remote commit', () => {
181
- // Push a new commit to origin from a separate clone
182
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
183
- try {
184
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
185
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
186
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
187
- fs.writeFileSync(path.join(cloneDir, 'new.txt'), 'remote commit');
188
- gitCmd(['add', '.'], cloneDir);
189
- gitCmd(['commit', '-m', 'remote commit'], cloneDir);
190
- gitCmd(['push', 'origin', 'main'], cloneDir);
191
- }
192
- finally {
193
- fs.rmSync(cloneDir, { recursive: true, force: true });
194
- }
195
- strict_1.default.equal((0, git_1.createBranchFromRemote)('origin', 'main', 'task123/fix-bug'), true);
196
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'task123/fix-bug');
197
- // The new branch should contain the remote commit's file
198
- strict_1.default.ok(fs.existsSync(path.join(tmpDir, 'new.txt')));
199
- });
200
- (0, node_test_1.it)('does not require the local base branch to be checked out', () => {
201
- gitCmd(['checkout', '-b', 'some-other-branch'], tmpDir);
202
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'some-other-branch');
203
- strict_1.default.equal((0, git_1.createBranchFromRemote)('origin', 'main', 'task456/new-feature'), true);
204
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'task456/new-feature');
205
- });
206
- (0, node_test_1.it)('stashes dirty changes before creating the branch', () => {
207
- fs.writeFileSync(path.join(tmpDir, 'dirty.txt'), 'uncommitted');
208
- strict_1.default.equal((0, git_1.hasChanges)(), true);
209
- strict_1.default.equal((0, git_1.createBranchFromRemote)('origin', 'main', 'task789/clean-start'), true);
210
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'task789/clean-start');
211
- });
212
- (0, node_test_1.it)('branches from the remote ref, not the local base branch', () => {
213
- // Push a new commit to origin from a separate clone
214
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
215
- try {
216
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
217
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
218
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
219
- fs.writeFileSync(path.join(cloneDir, 'remote-only.txt'), 'only on remote');
220
- gitCmd(['add', '.'], cloneDir);
221
- gitCmd(['commit', '-m', 'remote-only commit'], cloneDir);
222
- gitCmd(['push', 'origin', 'main'], cloneDir);
223
- }
224
- finally {
225
- fs.rmSync(cloneDir, { recursive: true, force: true });
226
- }
227
- // Local main is behind origin/main — don't pull
228
- strict_1.default.equal((0, git_1.createBranchFromRemote)('origin', 'main', 'task999/from-remote'), true);
229
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'task999/from-remote');
230
- // Branch should have the remote commit even though local main doesn't
231
- strict_1.default.ok(fs.existsSync(path.join(tmpDir, 'remote-only.txt')));
232
- });
233
- (0, node_test_1.it)('fails when the remote base branch does not exist', () => {
234
- strict_1.default.equal((0, git_1.createBranchFromRemote)('origin', 'nonexistent', 'task000/bad'), false);
235
- });
236
- (0, node_test_1.it)('falls back to checking out existing branch when checkout -b fails (pending task follow-up)', () => {
237
- // Simulate the pending-task scenario: branch already exists on the remote
238
- // because a previous run created it. A follow-up comment triggers another run
239
- // which calls createBranchFromRemote again — checkout -b fails because
240
- // the branch already exists, so it should fallback to fetchAndCheckoutBranch.
241
- gitCmd(['checkout', '-b', 'task123/existing-branch'], tmpDir);
242
- fs.writeFileSync(path.join(tmpDir, 'work.txt'), 'previous work');
243
- gitCmd(['add', '.'], tmpDir);
244
- gitCmd(['commit', '-m', 'work on existing branch'], tmpDir);
245
- gitCmd(['push', 'origin', 'task123/existing-branch'], tmpDir);
246
- // Switch back to main so we're not already on the target branch
247
- gitCmd(['checkout', 'main'], tmpDir);
248
- // Now createBranchFromRemote should fail on checkout -b (branch exists)
249
- // but succeed by falling back to checking out the existing branch
250
- strict_1.default.equal((0, git_1.createBranchFromRemote)('origin', 'main', 'task123/existing-branch'), true);
251
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'task123/existing-branch');
252
- // Should have the file from the existing branch
253
- strict_1.default.ok(fs.existsSync(path.join(tmpDir, 'work.txt')));
254
- });
255
- (0, node_test_1.it)('falls back and picks up new remote commits on existing branch', () => {
256
- // Create the branch, push it, then switch away — keep local branch alive
257
- // so checkout -b will fail and trigger the fallback path
258
- gitCmd(['checkout', '-b', 'task456/followup'], tmpDir);
259
- fs.writeFileSync(path.join(tmpDir, 'v1.txt'), 'first version');
260
- gitCmd(['add', '.'], tmpDir);
261
- gitCmd(['commit', '-m', 'v1'], tmpDir);
262
- gitCmd(['push', 'origin', 'task456/followup'], tmpDir);
263
- gitCmd(['checkout', 'main'], tmpDir);
264
- // Push a new commit to that branch from a separate clone
265
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
266
- try {
267
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
268
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
269
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
270
- gitCmd(['checkout', 'task456/followup'], cloneDir);
271
- fs.writeFileSync(path.join(cloneDir, 'v2.txt'), 'second version');
272
- gitCmd(['add', '.'], cloneDir);
273
- gitCmd(['commit', '-m', 'v2'], cloneDir);
274
- gitCmd(['push', 'origin', 'task456/followup'], cloneDir);
275
- }
276
- finally {
277
- fs.rmSync(cloneDir, { recursive: true, force: true });
278
- }
279
- // checkout -b fails (branch exists locally), fallback checks out + pulls
280
- strict_1.default.equal((0, git_1.createBranchFromRemote)('origin', 'main', 'task456/followup'), true);
281
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'task456/followup');
282
- // Should have both the original and the new remote commit's files
283
- strict_1.default.ok(fs.existsSync(path.join(tmpDir, 'v1.txt')));
284
- strict_1.default.ok(fs.existsSync(path.join(tmpDir, 'v2.txt')));
285
- });
286
- });
287
- (0, node_test_1.describe)('commit with expectedBranch (integration)', () => {
288
- let tmpDir;
289
- let originalCwd;
290
- (0, node_test_1.beforeEach)(() => {
291
- originalCwd = process.cwd();
292
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
293
- initRepo(tmpDir);
294
- process.chdir(tmpDir);
295
- });
296
- (0, node_test_1.afterEach)(() => {
297
- process.chdir(originalCwd);
298
- fs.rmSync(tmpDir, { recursive: true, force: true });
299
- });
300
- (0, node_test_1.it)('succeeds when on the expected branch', () => {
301
- gitCmd(['checkout', '-b', 'task123/fix-bug'], tmpDir);
302
- fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'content');
303
- strict_1.default.equal((0, git_1.addAll)(), true);
304
- strict_1.default.equal((0, git_1.commit)('test commit', 'task123/fix-bug'), true);
305
- });
306
- (0, node_test_1.it)('fails when on a different branch than expected', () => {
307
- gitCmd(['checkout', '-b', 'task123/fix-bug'], tmpDir);
308
- gitCmd(['checkout', '-b', 'wrong-branch'], tmpDir);
309
- fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'content');
310
- strict_1.default.equal((0, git_1.addAll)(), true);
311
- strict_1.default.equal((0, git_1.commit)('test commit', 'task123/fix-bug'), false);
312
- });
313
- (0, node_test_1.it)('refuses to commit when expectedBranch is a protected branch', () => {
314
- fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'content');
315
- strict_1.default.equal((0, git_1.addAll)(), true);
316
- strict_1.default.equal((0, git_1.commit)('test commit', 'main'), false);
317
- });
318
- (0, node_test_1.it)('works without expectedBranch (backward compatible)', () => {
319
- fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'content');
320
- strict_1.default.equal((0, git_1.addAll)(), true);
321
- strict_1.default.equal((0, git_1.commit)('test commit'), true);
322
- });
323
- });
324
- (0, node_test_1.describe)('push validation (integration)', () => {
325
- let tmpDir;
326
- let bareDir;
327
- let originalCwd;
328
- (0, node_test_1.beforeEach)(() => {
329
- originalCwd = process.cwd();
330
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
331
- bareDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-bare-'));
332
- gitCmd(['init', '--bare', '-b', 'main'], bareDir);
333
- initRepo(tmpDir);
334
- gitCmd(['remote', 'add', 'origin', bareDir], tmpDir);
335
- gitCmd(['push', 'origin', 'main'], tmpDir);
336
- process.chdir(tmpDir);
337
- });
338
- (0, node_test_1.afterEach)(() => {
339
- process.chdir(originalCwd);
340
- fs.rmSync(tmpDir, { recursive: true, force: true });
341
- fs.rmSync(bareDir, { recursive: true, force: true });
342
- });
343
- (0, node_test_1.it)('refuses to push to a protected branch', () => {
344
- strict_1.default.equal((0, git_1.push)('origin', 'main'), false);
345
- });
346
- (0, node_test_1.it)('refuses to push when current branch does not match target', () => {
347
- gitCmd(['checkout', '-b', 'task123/fix-bug'], tmpDir);
348
- strict_1.default.equal((0, git_1.push)('origin', 'task456/other-task'), false);
349
- });
350
- (0, node_test_1.it)('succeeds when current branch matches target and is not protected', () => {
351
- gitCmd(['checkout', '-b', 'task123/fix-bug'], tmpDir);
352
- fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'content');
353
- gitCmd(['add', '.'], tmpDir);
354
- gitCmd(['commit', '-m', 'add file'], tmpDir);
355
- strict_1.default.equal((0, git_1.push)('origin', 'task123/fix-bug'), true);
356
- });
357
- (0, node_test_1.it)('uses explicit HEAD refspec so stale local branches are not pushed', () => {
358
- gitCmd(['checkout', '-b', 'task123/fix-bug'], tmpDir);
359
- fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'v1');
360
- gitCmd(['add', '.'], tmpDir);
361
- gitCmd(['commit', '-m', 'v1'], tmpDir);
362
- strict_1.default.equal((0, git_1.push)('origin', 'task123/fix-bug'), true);
363
- fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'v2');
364
- gitCmd(['add', '.'], tmpDir);
365
- gitCmd(['commit', '-m', 'v2'], tmpDir);
366
- strict_1.default.equal((0, git_1.push)('origin', 'task123/fix-bug'), true);
367
- const log = (0, node_child_process_1.spawnSync)('git', ['log', '--oneline', 'origin/task123/fix-bug'], {
368
- cwd: tmpDir,
369
- encoding: 'utf8',
370
- });
371
- strict_1.default.ok(log.stdout.includes('v2'));
372
- });
373
- });
374
- (0, node_test_1.describe)('stashChanges (integration)', () => {
375
- let tmpDir;
376
- let originalCwd;
377
- (0, node_test_1.beforeEach)(() => {
378
- originalCwd = process.cwd();
379
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
380
- initRepo(tmpDir);
381
- process.chdir(tmpDir);
382
- });
383
- (0, node_test_1.afterEach)(() => {
384
- process.chdir(originalCwd);
385
- fs.rmSync(tmpDir, { recursive: true, force: true });
386
- });
387
- (0, node_test_1.it)('returns true and does nothing when working tree is clean', () => {
388
- strict_1.default.equal((0, git_1.hasChanges)(), false);
389
- strict_1.default.equal((0, git_1.stashChanges)(), true);
390
- strict_1.default.equal((0, git_1.hasChanges)(), false);
391
- });
392
- (0, node_test_1.it)('stashes uncommitted changes so working tree is clean afterward', () => {
393
- fs.writeFileSync(path.join(tmpDir, 'dirty.txt'), 'uncommitted');
394
- strict_1.default.equal((0, git_1.hasChanges)(), true);
395
- strict_1.default.equal((0, git_1.stashChanges)(), true);
396
- strict_1.default.equal((0, git_1.hasChanges)(), false);
397
- });
398
- (0, node_test_1.it)('stashes untracked files (using -u flag)', () => {
399
- fs.writeFileSync(path.join(tmpDir, 'untracked.txt'), 'new file');
400
- strict_1.default.equal((0, git_1.hasChanges)(), true);
401
- strict_1.default.equal((0, git_1.stashChanges)(), true);
402
- strict_1.default.equal((0, git_1.hasChanges)(), false);
403
- });
404
- });
405
- (0, node_test_1.describe)('fetchAndCheckout with dirty working tree (integration)', () => {
406
- let tmpDir;
407
- let bareDir;
408
- let originalCwd;
409
- (0, node_test_1.beforeEach)(() => {
410
- originalCwd = process.cwd();
411
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
412
- bareDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-bare-'));
413
- gitCmd(['init', '--bare', '-b', 'main'], bareDir);
414
- initRepo(tmpDir);
415
- gitCmd(['remote', 'add', 'origin', bareDir], tmpDir);
416
- gitCmd(['push', 'origin', 'main'], tmpDir);
417
- process.chdir(tmpDir);
418
- });
419
- (0, node_test_1.afterEach)(() => {
420
- process.chdir(originalCwd);
421
- fs.rmSync(tmpDir, { recursive: true, force: true });
422
- fs.rmSync(bareDir, { recursive: true, force: true });
423
- });
424
- (0, node_test_1.it)('succeeds even when there are uncommitted changes (stashes them)', () => {
425
- gitCmd(['checkout', '-b', 'feature/dirty'], tmpDir);
426
- fs.writeFileSync(path.join(tmpDir, 'leftover.txt'), 'from previous run');
427
- strict_1.default.equal((0, git_1.hasChanges)(), true);
428
- strict_1.default.equal((0, git_1.fetchAndCheckout)('origin', 'main'), true);
429
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'main');
430
- });
431
- (0, node_test_1.it)('succeeds even when there are untracked files (stashes them)', () => {
432
- fs.writeFileSync(path.join(tmpDir, 'untracked.txt'), 'untracked');
433
- strict_1.default.equal((0, git_1.hasChanges)(), true);
434
- strict_1.default.equal((0, git_1.fetchAndCheckout)('origin', 'main'), true);
435
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'main');
436
- });
437
- });
438
- (0, node_test_1.describe)('fetchAndCheckout sync verification (integration)', () => {
439
- let tmpDir;
440
- let bareDir;
441
- let originalCwd;
442
- (0, node_test_1.beforeEach)(() => {
443
- originalCwd = process.cwd();
444
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
445
- bareDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-bare-'));
446
- gitCmd(['init', '--bare', '-b', 'main'], bareDir);
447
- initRepo(tmpDir);
448
- gitCmd(['remote', 'add', 'origin', bareDir], tmpDir);
449
- gitCmd(['push', 'origin', 'main'], tmpDir);
450
- process.chdir(tmpDir);
451
- });
452
- (0, node_test_1.afterEach)(() => {
453
- process.chdir(originalCwd);
454
- fs.rmSync(tmpDir, { recursive: true, force: true });
455
- fs.rmSync(bareDir, { recursive: true, force: true });
456
- });
457
- (0, node_test_1.it)('succeeds when local main matches origin/main', () => {
458
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
459
- strict_1.default.equal((0, git_1.fetchAndCheckout)('origin', 'main'), true);
460
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'main');
461
- });
462
- (0, node_test_1.it)('succeeds when origin has new commits (fast-forward)', () => {
463
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
464
- try {
465
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
466
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
467
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
468
- fs.writeFileSync(path.join(cloneDir, 'new.txt'), 'from clone');
469
- gitCmd(['add', '.'], cloneDir);
470
- gitCmd(['commit', '-m', 'remote commit'], cloneDir);
471
- gitCmd(['push', 'origin', 'main'], cloneDir);
472
- }
473
- finally {
474
- fs.rmSync(cloneDir, { recursive: true, force: true });
475
- }
476
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
477
- strict_1.default.equal((0, git_1.fetchAndCheckout)('origin', 'main'), true);
478
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'main');
479
- });
480
- (0, node_test_1.it)('fails when local main has diverged from origin/main', () => {
481
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
482
- try {
483
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
484
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
485
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
486
- fs.writeFileSync(path.join(cloneDir, 'remote.txt'), 'from clone');
487
- gitCmd(['add', '.'], cloneDir);
488
- gitCmd(['commit', '-m', 'remote commit'], cloneDir);
489
- gitCmd(['push', 'origin', 'main'], cloneDir);
490
- }
491
- finally {
492
- fs.rmSync(cloneDir, { recursive: true, force: true });
493
- }
494
- fs.writeFileSync(path.join(tmpDir, 'local.txt'), 'local only');
495
- gitCmd(['add', '.'], tmpDir);
496
- gitCmd(['commit', '-m', 'local commit'], tmpDir);
497
- // pull will create a merge commit, making local ahead of origin/main
498
- strict_1.default.equal((0, git_1.fetchAndCheckout)('origin', 'main'), false);
499
- });
500
- });
501
- // ─── checkConflictsWithBase (integration) ─────────────────────────────────────
502
- (0, node_test_1.describe)('checkConflictsWithBase (integration)', () => {
503
- let tmpDir;
504
- let bareDir;
505
- let originalCwd;
506
- (0, node_test_1.beforeEach)(() => {
507
- originalCwd = process.cwd();
508
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
509
- bareDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-bare-'));
510
- gitCmd(['init', '--bare', '-b', 'main'], bareDir);
511
- initRepo(tmpDir);
512
- gitCmd(['remote', 'add', 'origin', bareDir], tmpDir);
513
- gitCmd(['push', 'origin', 'main'], tmpDir);
514
- process.chdir(tmpDir);
515
- });
516
- (0, node_test_1.afterEach)(() => {
517
- process.chdir(originalCwd);
518
- fs.rmSync(tmpDir, { recursive: true, force: true });
519
- fs.rmSync(bareDir, { recursive: true, force: true });
520
- });
521
- (0, node_test_1.it)('reports clean when branch is up to date with base', () => {
522
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
523
- fs.writeFileSync(path.join(tmpDir, 'feature.txt'), 'new file');
524
- gitCmd(['add', '.'], tmpDir);
525
- gitCmd(['commit', '-m', 'feature commit'], tmpDir);
526
- gitCmd(['fetch', 'origin'], tmpDir);
527
- const result = (0, git_1.checkConflictsWithBase)('origin', 'main');
528
- strict_1.default.equal(result.clean, true);
529
- strict_1.default.equal(result.behindCommits, 0);
530
- strict_1.default.deepEqual(result.conflictFiles, []);
531
- });
532
- (0, node_test_1.it)('reports clean when base has new non-conflicting commits', () => {
533
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
534
- fs.writeFileSync(path.join(tmpDir, 'feature.txt'), 'new file');
535
- gitCmd(['add', '.'], tmpDir);
536
- gitCmd(['commit', '-m', 'feature commit'], tmpDir);
537
- // Add a non-conflicting commit to main via bare repo
538
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
539
- try {
540
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
541
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
542
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
543
- fs.writeFileSync(path.join(cloneDir, 'other.txt'), 'no conflict');
544
- gitCmd(['add', '.'], cloneDir);
545
- gitCmd(['commit', '-m', 'main commit'], cloneDir);
546
- gitCmd(['push', 'origin', 'main'], cloneDir);
547
- }
548
- finally {
549
- fs.rmSync(cloneDir, { recursive: true, force: true });
550
- }
551
- gitCmd(['fetch', 'origin'], tmpDir);
552
- const result = (0, git_1.checkConflictsWithBase)('origin', 'main');
553
- strict_1.default.equal(result.clean, true);
554
- strict_1.default.ok(result.behindCommits > 0);
555
- strict_1.default.deepEqual(result.conflictFiles, []);
556
- });
557
- (0, node_test_1.it)('detects conflicts when base and branch modify the same file', () => {
558
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
559
- fs.writeFileSync(path.join(tmpDir, 'README.md'), '# feature change\n');
560
- gitCmd(['add', '.'], tmpDir);
561
- gitCmd(['commit', '-m', 'feature commit'], tmpDir);
562
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
563
- try {
564
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
565
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
566
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
567
- fs.writeFileSync(path.join(cloneDir, 'README.md'), '# main change\n');
568
- gitCmd(['add', '.'], cloneDir);
569
- gitCmd(['commit', '-m', 'main commit'], cloneDir);
570
- gitCmd(['push', 'origin', 'main'], cloneDir);
571
- }
572
- finally {
573
- fs.rmSync(cloneDir, { recursive: true, force: true });
574
- }
575
- gitCmd(['fetch', 'origin'], tmpDir);
576
- const result = (0, git_1.checkConflictsWithBase)('origin', 'main');
577
- strict_1.default.equal(result.clean, false);
578
- strict_1.default.ok(result.conflictFiles.includes('README.md'));
579
- });
580
- (0, node_test_1.it)('leaves working tree clean after conflict detection (aborts trial merge)', () => {
581
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
582
- fs.writeFileSync(path.join(tmpDir, 'README.md'), '# feature\n');
583
- gitCmd(['add', '.'], tmpDir);
584
- gitCmd(['commit', '-m', 'feature'], tmpDir);
585
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
586
- try {
587
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
588
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
589
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
590
- fs.writeFileSync(path.join(cloneDir, 'README.md'), '# main\n');
591
- gitCmd(['add', '.'], cloneDir);
592
- gitCmd(['commit', '-m', 'main'], cloneDir);
593
- gitCmd(['push', 'origin', 'main'], cloneDir);
594
- }
595
- finally {
596
- fs.rmSync(cloneDir, { recursive: true, force: true });
597
- }
598
- gitCmd(['fetch', 'origin'], tmpDir);
599
- (0, git_1.checkConflictsWithBase)('origin', 'main');
600
- // Should be back on feature branch with no merge in progress
601
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'feature/test');
602
- strict_1.default.equal((0, git_1.hasChanges)(), false);
603
- });
604
- });
605
- // ─── mergeBaseBranch + commitMerge (integration) ─────────────────────────────
606
- (0, node_test_1.describe)('mergeBaseBranch and commitMerge (integration)', () => {
607
- let tmpDir;
608
- let bareDir;
609
- let originalCwd;
610
- (0, node_test_1.beforeEach)(() => {
611
- originalCwd = process.cwd();
612
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-git-test-'));
613
- bareDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-bare-'));
614
- gitCmd(['init', '--bare', '-b', 'main'], bareDir);
615
- initRepo(tmpDir);
616
- gitCmd(['remote', 'add', 'origin', bareDir], tmpDir);
617
- gitCmd(['push', 'origin', 'main'], tmpDir);
618
- process.chdir(tmpDir);
619
- });
620
- (0, node_test_1.afterEach)(() => {
621
- process.chdir(originalCwd);
622
- fs.rmSync(tmpDir, { recursive: true, force: true });
623
- fs.rmSync(bareDir, { recursive: true, force: true });
624
- });
625
- (0, node_test_1.it)('merges cleanly when there are no conflicts', () => {
626
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
627
- fs.writeFileSync(path.join(tmpDir, 'feature.txt'), 'new file');
628
- gitCmd(['add', '.'], tmpDir);
629
- gitCmd(['commit', '-m', 'feature commit'], tmpDir);
630
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
631
- try {
632
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
633
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
634
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
635
- fs.writeFileSync(path.join(cloneDir, 'other.txt'), 'no conflict');
636
- gitCmd(['add', '.'], cloneDir);
637
- gitCmd(['commit', '-m', 'main commit'], cloneDir);
638
- gitCmd(['push', 'origin', 'main'], cloneDir);
639
- }
640
- finally {
641
- fs.rmSync(cloneDir, { recursive: true, force: true });
642
- }
643
- gitCmd(['fetch', 'origin'], tmpDir);
644
- strict_1.default.equal((0, git_1.mergeBaseBranch)('origin', 'main'), true);
645
- strict_1.default.ok(fs.existsSync(path.join(tmpDir, 'other.txt')));
646
- });
647
- (0, node_test_1.it)('returns false when there are conflicts, and abortMerge restores state', () => {
648
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
649
- fs.writeFileSync(path.join(tmpDir, 'README.md'), '# feature\n');
650
- gitCmd(['add', '.'], tmpDir);
651
- gitCmd(['commit', '-m', 'feature'], tmpDir);
652
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
653
- try {
654
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
655
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
656
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
657
- fs.writeFileSync(path.join(cloneDir, 'README.md'), '# main\n');
658
- gitCmd(['add', '.'], cloneDir);
659
- gitCmd(['commit', '-m', 'main'], cloneDir);
660
- gitCmd(['push', 'origin', 'main'], cloneDir);
661
- }
662
- finally {
663
- fs.rmSync(cloneDir, { recursive: true, force: true });
664
- }
665
- gitCmd(['fetch', 'origin'], tmpDir);
666
- strict_1.default.equal((0, git_1.mergeBaseBranch)('origin', 'main'), false);
667
- (0, git_1.abortMerge)();
668
- strict_1.default.equal((0, git_1.getCurrentBranch)(), 'feature/test');
669
- strict_1.default.equal((0, git_1.hasChanges)(), false);
670
- });
671
- (0, node_test_1.it)('commitMerge completes a conflicted merge after manual resolution', () => {
672
- gitCmd(['checkout', '-b', 'feature/test'], tmpDir);
673
- fs.writeFileSync(path.join(tmpDir, 'README.md'), '# feature\n');
674
- gitCmd(['add', '.'], tmpDir);
675
- gitCmd(['commit', '-m', 'feature'], tmpDir);
676
- const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-clone-'));
677
- try {
678
- gitCmd(['clone', bareDir, cloneDir], cloneDir);
679
- gitCmd(['config', 'user.email', 'other@test.com'], cloneDir);
680
- gitCmd(['config', 'user.name', 'Other'], cloneDir);
681
- fs.writeFileSync(path.join(cloneDir, 'README.md'), '# main\n');
682
- gitCmd(['add', '.'], cloneDir);
683
- gitCmd(['commit', '-m', 'main'], cloneDir);
684
- gitCmd(['push', 'origin', 'main'], cloneDir);
685
- }
686
- finally {
687
- fs.rmSync(cloneDir, { recursive: true, force: true });
688
- }
689
- gitCmd(['fetch', 'origin'], tmpDir);
690
- (0, git_1.mergeBaseBranch)('origin', 'main');
691
- // Manually resolve the conflict
692
- fs.writeFileSync(path.join(tmpDir, 'README.md'), '# merged\n');
693
- strict_1.default.equal((0, git_1.commitMerge)('Merge main into feature'), true);
694
- strict_1.default.equal((0, git_1.hasChanges)(), false);
695
- });
696
- });
697
- //# sourceMappingURL=git.test.js.map