shadow-git 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.temp/.sgit/info/exclude +1 -1
- package/CLAUDE.md +2 -1
- package/README.md +1 -1
- package/dist/cli.js +322 -74
- package/package.json +1 -1
- package/src/cli.ts +36 -4
- package/src/commands/forward.ts +3 -3
- package/src/commands/track/add.ts +6 -4
- package/src/commands/track/clone.ts +16 -1
- package/src/commands/track/destroy.ts +5 -0
- package/src/commands/track/ignore.ts +2 -2
- package/src/commands/track/index.ts +12 -0
- package/src/commands/track/init.ts +5 -1
- package/src/commands/track/remove.ts +3 -2
- package/src/commands/track/swap.ts +61 -0
- package/src/commands/track/sync.ts +9 -6
- package/src/commands/track/unswap.ts +49 -0
- package/src/commands/track/verify.ts +11 -9
- package/src/core/errors.ts +31 -1
- package/src/core/exclude.ts +2 -2
- package/src/core/gitignore.ts +18 -3
- package/src/core/repository.ts +8 -0
- package/src/core/swap-mode.ts +83 -0
- package/src/utils/logger.ts +12 -0
- package/tests/commands/forward.test.ts +6 -6
- package/tests/commands/track/add.test.ts +6 -6
- package/tests/commands/track/swap.test.ts +266 -0
- package/tests/commands/track/verify.test.ts +16 -16
- package/tests/core/exclude.test.ts +10 -10
- package/tests/core/gitignore.test.ts +30 -30
- package/tests/helpers/guard-child.mjs +2 -2
|
@@ -57,11 +57,11 @@ describe('gitignore manipulator', () => {
|
|
|
57
57
|
// disableGitignores
|
|
58
58
|
// ---------------------------------------------------------------
|
|
59
59
|
describe('disableGitignores', () => {
|
|
60
|
-
it('renames .gitignore to .gitignore-
|
|
60
|
+
it('renames .gitignore to .gitignore.sgit-parked in tracked directory', () => {
|
|
61
61
|
const disabled = disableGitignores(tempDir, ['secret/']);
|
|
62
62
|
expect(disabled).toContain('secret/.gitignore');
|
|
63
63
|
expect(fileExists(join(tempDir, 'secret', '.gitignore'))).toBe(false);
|
|
64
|
-
expect(fileExists(join(tempDir, 'secret', '.gitignore-
|
|
64
|
+
expect(fileExists(join(tempDir, 'secret', '.gitignore.sgit-parked'))).toBe(
|
|
65
65
|
true,
|
|
66
66
|
);
|
|
67
67
|
});
|
|
@@ -139,9 +139,9 @@ describe('gitignore manipulator', () => {
|
|
|
139
139
|
expect(disabled).toEqual([]);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
it('throws StaleGitignoreError when .gitignore-
|
|
142
|
+
it('throws StaleGitignoreError when .gitignore.sgit-parked already exists', async () => {
|
|
143
143
|
await writeFile(
|
|
144
|
-
join(tempDir, 'secret', '.gitignore-
|
|
144
|
+
join(tempDir, 'secret', '.gitignore.sgit-parked'),
|
|
145
145
|
'stale\n',
|
|
146
146
|
);
|
|
147
147
|
expect(() => disableGitignores(tempDir, ['secret/'])).toThrow(
|
|
@@ -212,12 +212,12 @@ describe('gitignore manipulator', () => {
|
|
|
212
212
|
// enableGitignores
|
|
213
213
|
// ---------------------------------------------------------------
|
|
214
214
|
describe('enableGitignores', () => {
|
|
215
|
-
it('renames .gitignore-
|
|
215
|
+
it('renames .gitignore.sgit-parked back to .gitignore', () => {
|
|
216
216
|
disableGitignores(tempDir, ['secret/']);
|
|
217
217
|
const enabled = enableGitignores(tempDir, ['secret/']);
|
|
218
218
|
expect(enabled).toHaveLength(1);
|
|
219
219
|
expect(fileExists(join(tempDir, 'secret', '.gitignore'))).toBe(true);
|
|
220
|
-
expect(fileExists(join(tempDir, 'secret', '.gitignore-
|
|
220
|
+
expect(fileExists(join(tempDir, 'secret', '.gitignore.sgit-parked'))).toBe(
|
|
221
221
|
false,
|
|
222
222
|
);
|
|
223
223
|
});
|
|
@@ -255,28 +255,28 @@ describe('gitignore manipulator', () => {
|
|
|
255
255
|
// findStaleGitignores
|
|
256
256
|
// ---------------------------------------------------------------
|
|
257
257
|
describe('findStaleGitignores', () => {
|
|
258
|
-
it('finds .gitignore-
|
|
258
|
+
it('finds .gitignore.sgit-parked in tracked dirs', async () => {
|
|
259
259
|
await writeFile(
|
|
260
|
-
join(tempDir, 'secret', '.gitignore-
|
|
260
|
+
join(tempDir, 'secret', '.gitignore.sgit-parked'),
|
|
261
261
|
'stale\n',
|
|
262
262
|
);
|
|
263
263
|
const stale = findStaleGitignores(tempDir, ['secret/']);
|
|
264
|
-
expect(stale).toEqual(['secret/.gitignore-
|
|
264
|
+
expect(stale).toEqual(['secret/.gitignore.sgit-parked']);
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
-
it('finds .gitignore-
|
|
267
|
+
it('finds .gitignore.sgit-parked in ancestor dirs for file entries', async () => {
|
|
268
268
|
await writeFile(
|
|
269
|
-
join(tempDir, 'secret', '.gitignore-
|
|
269
|
+
join(tempDir, 'secret', '.gitignore.sgit-parked'),
|
|
270
270
|
'stale\n',
|
|
271
271
|
);
|
|
272
272
|
const stale = findStaleGitignores(tempDir, ['secret/data.txt']);
|
|
273
|
-
expect(stale).toContain('secret/.gitignore-
|
|
273
|
+
expect(stale).toContain('secret/.gitignore.sgit-parked');
|
|
274
274
|
});
|
|
275
275
|
|
|
276
|
-
it('finds .gitignore-
|
|
277
|
-
await writeFile(join(tempDir, '.gitignore-
|
|
276
|
+
it('finds .gitignore.sgit-parked at root', async () => {
|
|
277
|
+
await writeFile(join(tempDir, '.gitignore.sgit-parked'), 'stale\n');
|
|
278
278
|
const stale = findStaleGitignores(tempDir, ['secret/']);
|
|
279
|
-
expect(stale).toContain('.gitignore-
|
|
279
|
+
expect(stale).toContain('.gitignore.sgit-parked');
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
it('returns empty when no stale files exist', () => {
|
|
@@ -314,7 +314,7 @@ describe('gitignore manipulator', () => {
|
|
|
314
314
|
it('disables gitignores and returns cleanup function', () => {
|
|
315
315
|
const cleanup = setupGitignoreGuard(tempDir, ['secret/']);
|
|
316
316
|
expect(fileExists(join(tempDir, 'secret', '.gitignore'))).toBe(false);
|
|
317
|
-
expect(fileExists(join(tempDir, 'secret', '.gitignore-
|
|
317
|
+
expect(fileExists(join(tempDir, 'secret', '.gitignore.sgit-parked'))).toBe(
|
|
318
318
|
true,
|
|
319
319
|
);
|
|
320
320
|
expect(fileExists(join(tempDir, '.sgit', '.gitignore-guard'))).toBe(true);
|
|
@@ -322,7 +322,7 @@ describe('gitignore manipulator', () => {
|
|
|
322
322
|
cleanup();
|
|
323
323
|
|
|
324
324
|
expect(fileExists(join(tempDir, 'secret', '.gitignore'))).toBe(true);
|
|
325
|
-
expect(fileExists(join(tempDir, 'secret', '.gitignore-
|
|
325
|
+
expect(fileExists(join(tempDir, 'secret', '.gitignore.sgit-parked'))).toBe(
|
|
326
326
|
false,
|
|
327
327
|
);
|
|
328
328
|
expect(fileExists(join(tempDir, '.sgit', '.gitignore-guard'))).toBe(
|
|
@@ -365,10 +365,10 @@ describe('gitignore manipulator', () => {
|
|
|
365
365
|
unlinkSync(lockPath);
|
|
366
366
|
});
|
|
367
367
|
|
|
368
|
-
it('auto-cleans stale .
|
|
368
|
+
it('auto-cleans stale .gitignore.sgit-parked files from a crashed process', () => {
|
|
369
369
|
renameSync(
|
|
370
370
|
join(tempDir, 'secret', '.gitignore'),
|
|
371
|
-
join(tempDir, 'secret', '.gitignore-
|
|
371
|
+
join(tempDir, 'secret', '.gitignore.sgit-parked'),
|
|
372
372
|
);
|
|
373
373
|
writeFileSync(
|
|
374
374
|
join(tempDir, '.sgit', '.gitignore-guard'),
|
|
@@ -376,8 +376,8 @@ describe('gitignore manipulator', () => {
|
|
|
376
376
|
);
|
|
377
377
|
|
|
378
378
|
const cleanup = setupGitignoreGuard(tempDir, ['secret/']);
|
|
379
|
-
// Guard should have restored stale .
|
|
380
|
-
expect(fileExists(join(tempDir, 'secret', '.gitignore-
|
|
379
|
+
// Guard should have restored stale .gitignore.sgit-parked, then re-disabled
|
|
380
|
+
expect(fileExists(join(tempDir, 'secret', '.gitignore.sgit-parked'))).toBe(
|
|
381
381
|
true,
|
|
382
382
|
);
|
|
383
383
|
cleanup();
|
|
@@ -401,7 +401,7 @@ describe('gitignore manipulator', () => {
|
|
|
401
401
|
cleanup();
|
|
402
402
|
}
|
|
403
403
|
expect(fileExists(join(tempDir, 'secret', '.gitignore'))).toBe(true);
|
|
404
|
-
expect(fileExists(join(tempDir, 'secret', '.gitignore-
|
|
404
|
+
expect(fileExists(join(tempDir, 'secret', '.gitignore.sgit-parked'))).toBe(
|
|
405
405
|
false,
|
|
406
406
|
);
|
|
407
407
|
});
|
|
@@ -458,9 +458,9 @@ describe('gitignore manipulator', () => {
|
|
|
458
458
|
});
|
|
459
459
|
|
|
460
460
|
// ---------------------------------------------------------------
|
|
461
|
-
// .gitignore-
|
|
461
|
+
// .gitignore.sgit-parked must never persist
|
|
462
462
|
// ---------------------------------------------------------------
|
|
463
|
-
describe('.gitignore-
|
|
463
|
+
describe('.gitignore.sgit-parked must not exist after any sgit command', () => {
|
|
464
464
|
it('does not exist after successful guard lifecycle', () => {
|
|
465
465
|
const cleanup = setupGitignoreGuard(tempDir, ['secret/']);
|
|
466
466
|
cleanup();
|
|
@@ -479,23 +479,23 @@ describe('gitignore manipulator', () => {
|
|
|
479
479
|
expect(findStaleGitignores(tempDir, ['secret/'])).toEqual([]);
|
|
480
480
|
});
|
|
481
481
|
|
|
482
|
-
it('disableGitignores throws when .gitignore-
|
|
482
|
+
it('disableGitignores throws when .gitignore.sgit-parked already exists', async () => {
|
|
483
483
|
await writeFile(
|
|
484
|
-
join(tempDir, 'secret', '.gitignore-
|
|
484
|
+
join(tempDir, 'secret', '.gitignore.sgit-parked'),
|
|
485
485
|
'stale content\n',
|
|
486
486
|
);
|
|
487
487
|
expect(() => disableGitignores(tempDir, ['secret/'])).toThrow(
|
|
488
488
|
StaleGitignoreError,
|
|
489
489
|
);
|
|
490
490
|
expect(() => disableGitignores(tempDir, ['secret/'])).toThrow(
|
|
491
|
-
/stale .gitignore-
|
|
491
|
+
/stale .gitignore.sgit-parked/,
|
|
492
492
|
);
|
|
493
493
|
});
|
|
494
494
|
|
|
495
495
|
it('guard auto-recovers stale files then proceeds normally', () => {
|
|
496
496
|
renameSync(
|
|
497
497
|
join(tempDir, 'secret', '.gitignore'),
|
|
498
|
-
join(tempDir, 'secret', '.gitignore-
|
|
498
|
+
join(tempDir, 'secret', '.gitignore.sgit-parked'),
|
|
499
499
|
);
|
|
500
500
|
writeFileSync(
|
|
501
501
|
join(tempDir, '.sgit', '.gitignore-guard'),
|
|
@@ -506,7 +506,7 @@ describe('gitignore manipulator', () => {
|
|
|
506
506
|
cleanup();
|
|
507
507
|
|
|
508
508
|
expect(fileExists(join(tempDir, 'secret', '.gitignore'))).toBe(true);
|
|
509
|
-
expect(fileExists(join(tempDir, 'secret', '.gitignore-
|
|
509
|
+
expect(fileExists(join(tempDir, 'secret', '.gitignore.sgit-parked'))).toBe(
|
|
510
510
|
false,
|
|
511
511
|
);
|
|
512
512
|
expect(findStaleGitignores(tempDir, ['secret/'])).toEqual([]);
|
|
@@ -518,7 +518,7 @@ describe('gitignore manipulator', () => {
|
|
|
518
518
|
|
|
519
519
|
function assertGitignoreRestored(root: string): void {
|
|
520
520
|
expect(fileExists(join(root, 'secret', '.gitignore'))).toBe(true);
|
|
521
|
-
expect(fileExists(join(root, 'secret', '.gitignore-
|
|
521
|
+
expect(fileExists(join(root, 'secret', '.gitignore.sgit-parked'))).toBe(false);
|
|
522
522
|
expect(fileExists(join(root, '.sgit', '.gitignore-guard'))).toBe(false);
|
|
523
523
|
}
|
|
524
524
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* This script directly implements the guard's cleanup mechanism using plain
|
|
7
7
|
* node:fs so it can run without TypeScript transpilation. It renames
|
|
8
|
-
* .gitignore → .gitignore-
|
|
8
|
+
* .gitignore → .gitignore.sgit-parked, registers signal/exit handlers, then
|
|
9
9
|
* exits in the specified way.
|
|
10
10
|
*/
|
|
11
11
|
import {
|
|
@@ -20,7 +20,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
20
20
|
|
|
21
21
|
const [,, root, exitMode] = process.argv;
|
|
22
22
|
const manifestPaths = ['secret/'];
|
|
23
|
-
const DISABLED_NAME = '.gitignore-
|
|
23
|
+
const DISABLED_NAME = '.gitignore.sgit-parked';
|
|
24
24
|
|
|
25
25
|
// --- Minimal guard implementation (mirrors src/core/gitignore.ts) ---
|
|
26
26
|
|