@hyperdrive.bot/gut 0.1.14-alpha.0 → 0.1.14
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/dist/commands/worktree/create.js +0 -8
- package/dist/services/worktree.service.d.ts +0 -24
- package/dist/services/worktree.service.js +0 -90
- package/package.json +2 -2
- package/dist/commands/worktree/gc.d.ts +0 -13
- package/dist/commands/worktree/gc.js +0 -126
- package/dist/commands/worktree/list.d.ts +0 -12
- package/dist/commands/worktree/list.js +0 -63
- package/dist/commands/worktree/list.test.d.ts +0 -1
- package/dist/commands/worktree/list.test.js +0 -182
- package/dist/commands/worktree/prune.d.ts +0 -6
- package/dist/commands/worktree/prune.js +0 -28
- package/dist/commands/worktree/prune.test.d.ts +0 -1
- package/dist/commands/worktree/prune.test.js +0 -105
- package/dist/commands/worktree/remove.d.ts +0 -12
- package/dist/commands/worktree/remove.js +0 -48
- package/dist/commands/worktree/status.d.ts +0 -9
- package/dist/commands/worktree/status.js +0 -58
- package/dist/utils/duration.d.ts +0 -1
- package/dist/utils/duration.js +0 -26
- package/oclif.manifest.json +0 -2418
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
vi.mock('node:fs', () => ({
|
|
4
|
-
default: {
|
|
5
|
-
existsSync: vi.fn(() => true),
|
|
6
|
-
},
|
|
7
|
-
existsSync: vi.fn(() => true),
|
|
8
|
-
}));
|
|
9
|
-
vi.mock('chalk', () => ({
|
|
10
|
-
default: {
|
|
11
|
-
bold: (s) => s,
|
|
12
|
-
cyan: (s) => s,
|
|
13
|
-
dim: (s) => s,
|
|
14
|
-
green: (s) => s,
|
|
15
|
-
yellow: (s) => s,
|
|
16
|
-
},
|
|
17
|
-
}));
|
|
18
|
-
function makeRecord(overrides = {}) {
|
|
19
|
-
return {
|
|
20
|
-
baseBranch: 'master',
|
|
21
|
-
createdAt: new Date('2026-03-19T13:22:00Z').getTime(),
|
|
22
|
-
entities: [
|
|
23
|
-
{ branch: 'workflow/deploy-batch', entityName: 'api', entityPath: 'packages/serverless/api', worktreePath: '/tmp/gut-worktrees/workflow-deploy-batch/packages/serverless/api' },
|
|
24
|
-
],
|
|
25
|
-
name: 'workflow/deploy-batch',
|
|
26
|
-
path: '/tmp/gut-worktrees/workflow-deploy-batch',
|
|
27
|
-
...overrides,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
async function makeCmd(state) {
|
|
31
|
-
const logs = [];
|
|
32
|
-
const { default: WorktreePrune } = await import('./prune.js');
|
|
33
|
-
const cmd = Object.create(WorktreePrune.prototype);
|
|
34
|
-
cmd.log = (msg = '') => { logs.push(msg); };
|
|
35
|
-
const stubs = {
|
|
36
|
-
configService: { getWorkspaceRoot: vi.fn(() => '/fake/workspace') },
|
|
37
|
-
gitService: { worktreePrune: vi.fn(async () => { }) },
|
|
38
|
-
worktreeService: {
|
|
39
|
-
loadState: vi.fn(() => state),
|
|
40
|
-
saveState: vi.fn(),
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
cmd.configService = stubs.configService;
|
|
44
|
-
cmd.gitService = stubs.gitService;
|
|
45
|
-
cmd.worktreeService = stubs.worktreeService;
|
|
46
|
-
return { cmd, logs, stubs };
|
|
47
|
-
}
|
|
48
|
-
describe('WorktreePrune', () => {
|
|
49
|
-
it('mixed stale+healthy: removes only stale, logs, calls saveState(kept) and worktreePrune once', async () => {
|
|
50
|
-
const healthy = makeRecord({ name: 'workflow/alive', path: '/tmp/alive' });
|
|
51
|
-
const stale = makeRecord({ name: 'workflow/dead', path: '/tmp/dead' });
|
|
52
|
-
vi.mocked(fs.existsSync).mockImplementation((p) => p === '/tmp/alive');
|
|
53
|
-
const { cmd, logs, stubs } = await makeCmd({ worktrees: [healthy, stale] });
|
|
54
|
-
await cmd.run();
|
|
55
|
-
expect(stubs.worktreeService.saveState).toHaveBeenCalledTimes(1);
|
|
56
|
-
expect(stubs.worktreeService.saveState).toHaveBeenCalledWith({ worktrees: [healthy] });
|
|
57
|
-
expect(stubs.gitService.worktreePrune).toHaveBeenCalledTimes(1);
|
|
58
|
-
expect(stubs.gitService.worktreePrune).toHaveBeenCalledWith('/fake/workspace');
|
|
59
|
-
const output = logs.join('\n');
|
|
60
|
-
expect(output).toContain('✗');
|
|
61
|
-
expect(output).toContain('workflow/dead');
|
|
62
|
-
expect(output).toContain('/tmp/dead');
|
|
63
|
-
expect(output).not.toContain('workflow/alive');
|
|
64
|
-
expect(output).toContain('✓ Pruned 1 stale worktree record(s)');
|
|
65
|
-
});
|
|
66
|
-
it('all stale: removes all, saveState called with empty, summary reports full count', async () => {
|
|
67
|
-
const a = makeRecord({ name: 'workflow/dead-a', path: '/tmp/dead-a' });
|
|
68
|
-
const b = makeRecord({ name: 'workflow/dead-b', path: '/tmp/dead-b' });
|
|
69
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
70
|
-
const { cmd, logs, stubs } = await makeCmd({ worktrees: [a, b] });
|
|
71
|
-
await cmd.run();
|
|
72
|
-
expect(stubs.worktreeService.saveState).toHaveBeenCalledTimes(1);
|
|
73
|
-
expect(stubs.worktreeService.saveState).toHaveBeenCalledWith({ worktrees: [] });
|
|
74
|
-
expect(stubs.gitService.worktreePrune).toHaveBeenCalledTimes(1);
|
|
75
|
-
expect(stubs.gitService.worktreePrune).toHaveBeenCalledWith('/fake/workspace');
|
|
76
|
-
const output = logs.join('\n');
|
|
77
|
-
expect(output).toContain('workflow/dead-a');
|
|
78
|
-
expect(output).toContain('/tmp/dead-a');
|
|
79
|
-
expect(output).toContain('workflow/dead-b');
|
|
80
|
-
expect(output).toContain('/tmp/dead-b');
|
|
81
|
-
expect(output).toContain('✓ Pruned 2 stale worktree record(s)');
|
|
82
|
-
});
|
|
83
|
-
it('all healthy: no save, no git prune, logs empty-state message', async () => {
|
|
84
|
-
const a = makeRecord({ name: 'workflow/alive-a', path: '/tmp/alive-a' });
|
|
85
|
-
const b = makeRecord({ name: 'workflow/alive-b', path: '/tmp/alive-b' });
|
|
86
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
87
|
-
const { cmd, logs, stubs } = await makeCmd({ worktrees: [a, b] });
|
|
88
|
-
await cmd.run();
|
|
89
|
-
expect(stubs.worktreeService.saveState).not.toHaveBeenCalled();
|
|
90
|
-
expect(stubs.gitService.worktreePrune).not.toHaveBeenCalled();
|
|
91
|
-
const output = logs.join('\n');
|
|
92
|
-
expect(output).toContain('No stale worktree records found');
|
|
93
|
-
expect(output).not.toContain('Pruned');
|
|
94
|
-
});
|
|
95
|
-
it('empty state: no save, no git prune, logs empty-state message', async () => {
|
|
96
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
97
|
-
const { cmd, logs, stubs } = await makeCmd({ worktrees: [] });
|
|
98
|
-
await cmd.run();
|
|
99
|
-
expect(stubs.worktreeService.saveState).not.toHaveBeenCalled();
|
|
100
|
-
expect(stubs.gitService.worktreePrune).not.toHaveBeenCalled();
|
|
101
|
-
const output = logs.join('\n');
|
|
102
|
-
expect(output).toContain('No stale worktree records found');
|
|
103
|
-
expect(output).not.toContain('Pruned');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { BaseCommand } from '../../base-command.js';
|
|
2
|
-
export default class WorktreeRemove extends BaseCommand {
|
|
3
|
-
static args: {
|
|
4
|
-
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
-
};
|
|
6
|
-
static description: string;
|
|
7
|
-
static examples: string[];
|
|
8
|
-
static flags: {
|
|
9
|
-
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
-
};
|
|
11
|
-
run(): Promise<void>;
|
|
12
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { BaseCommand } from '../../base-command.js';
|
|
4
|
-
export default class WorktreeRemove extends BaseCommand {
|
|
5
|
-
static args = {
|
|
6
|
-
name: Args.string({ description: 'Worktree name to remove', required: true }),
|
|
7
|
-
};
|
|
8
|
-
static description = 'Remove a worktree and all entity worktrees';
|
|
9
|
-
static examples = [
|
|
10
|
-
'<%= config.bin %> worktree remove workflow/batch',
|
|
11
|
-
'<%= config.bin %> worktree remove workflow/batch --force',
|
|
12
|
-
];
|
|
13
|
-
static flags = {
|
|
14
|
-
force: Flags.boolean({ char: 'f', default: false, description: 'Remove even with uncommitted changes' }),
|
|
15
|
-
};
|
|
16
|
-
async run() {
|
|
17
|
-
const { args, flags } = await this.parse(WorktreeRemove);
|
|
18
|
-
const { name } = args;
|
|
19
|
-
const record = this.worktreeService.get(name);
|
|
20
|
-
if (!record) {
|
|
21
|
-
this.error(`Worktree '${name}' not found`);
|
|
22
|
-
}
|
|
23
|
-
this.log(chalk.bold('\n🗑️ Removing worktree...'));
|
|
24
|
-
const result = await this.worktreeService.removeWorktreeRecord(name, {
|
|
25
|
-
force: flags.force,
|
|
26
|
-
logger: (msg) => {
|
|
27
|
-
if (msg.includes('⚠'))
|
|
28
|
-
this.log(chalk.yellow(msg));
|
|
29
|
-
else if (msg.includes('○'))
|
|
30
|
-
this.log(chalk.dim(msg));
|
|
31
|
-
else if (msg.includes('✓'))
|
|
32
|
-
this.log(chalk.green(msg));
|
|
33
|
-
else
|
|
34
|
-
this.log(msg);
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
if (result.skippedDueToDirty) {
|
|
38
|
-
const dirtyList = result.dirtyEntities
|
|
39
|
-
.map(d => ` ${d.entityName}: ${d.changeCount} uncommitted changes`)
|
|
40
|
-
.join('\n');
|
|
41
|
-
this.error(`Cannot remove worktree with uncommitted changes:\n${dirtyList}\n\nUse --force to remove anyway.`);
|
|
42
|
-
}
|
|
43
|
-
this.log(chalk.dim('─'.repeat(50)));
|
|
44
|
-
this.log(chalk.green(`✓ Removed worktree '${name}'`));
|
|
45
|
-
this.log(` Path: ${record.path}`);
|
|
46
|
-
this.log(` Entities: ${result.removedEntities} removed${result.skippedEntities > 0 ? `, ${result.skippedEntities} skipped` : ''}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { BaseCommand } from '../../base-command.js';
|
|
2
|
-
export default class WorktreeStatus extends BaseCommand {
|
|
3
|
-
static args: {
|
|
4
|
-
name: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
-
};
|
|
6
|
-
static description: string;
|
|
7
|
-
static examples: string[];
|
|
8
|
-
run(): Promise<void>;
|
|
9
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { Args } from '@oclif/core';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import Table from 'cli-table3';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import { BaseCommand } from '../../base-command.js';
|
|
6
|
-
export default class WorktreeStatus extends BaseCommand {
|
|
7
|
-
static args = {
|
|
8
|
-
name: Args.string({ description: 'Worktree name (shows all if omitted)', required: false }),
|
|
9
|
-
};
|
|
10
|
-
static description = 'Show per-entity git status for worktrees';
|
|
11
|
-
static examples = [
|
|
12
|
-
'<%= config.bin %> worktree status',
|
|
13
|
-
'<%= config.bin %> worktree status workflow/deploy-batch',
|
|
14
|
-
];
|
|
15
|
-
async run() {
|
|
16
|
-
const { args } = await this.parse(WorktreeStatus);
|
|
17
|
-
let worktrees;
|
|
18
|
-
if (args.name) {
|
|
19
|
-
const record = this.worktreeService.get(args.name);
|
|
20
|
-
if (!record) {
|
|
21
|
-
this.error(`Worktree "${args.name}" not found. Run "gut worktree list" to see active worktrees.`);
|
|
22
|
-
}
|
|
23
|
-
worktrees = [record];
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
worktrees = this.worktreeService.list();
|
|
27
|
-
if (worktrees.length === 0) {
|
|
28
|
-
this.log(`No active worktrees. Create one with: ${chalk.cyan('gut worktree create <branch-name>')}`);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
for (const worktree of worktrees) {
|
|
33
|
-
this.log(chalk.bold(`\n${worktree.name}`));
|
|
34
|
-
this.log(chalk.dim(worktree.path));
|
|
35
|
-
const table = new Table({
|
|
36
|
-
head: [chalk.cyan('Entity'), chalk.cyan('Branch'), chalk.cyan('Status'), chalk.cyan('Changes'), chalk.cyan('Ahead'), chalk.cyan('Behind')],
|
|
37
|
-
style: { border: [], head: [] },
|
|
38
|
-
});
|
|
39
|
-
for (const entity of worktree.entities) {
|
|
40
|
-
if (!fs.existsSync(entity.worktreePath)) {
|
|
41
|
-
table.push([entity.entityName, '-', chalk.yellow('[stale - path missing]'), '-', '-', '-']);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
const status = await this.gitService.getStatus(entity.worktreePath);
|
|
45
|
-
const changeCount = status.changes.length + status.untracked.length;
|
|
46
|
-
table.push([
|
|
47
|
-
entity.entityName,
|
|
48
|
-
status.branch,
|
|
49
|
-
status.hasChanges ? chalk.yellow('dirty') : chalk.green('clean'),
|
|
50
|
-
status.hasChanges ? chalk.yellow(`${changeCount} changes`) : chalk.green('clean'),
|
|
51
|
-
status.ahead.toString(),
|
|
52
|
-
status.behind.toString(),
|
|
53
|
-
]);
|
|
54
|
-
}
|
|
55
|
-
this.log(table.toString());
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
package/dist/utils/duration.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function parseDuration(input: string): number;
|
package/dist/utils/duration.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const SUFFIX_MS = {
|
|
2
|
-
d: 86_400_000,
|
|
3
|
-
h: 3_600_000,
|
|
4
|
-
m: 60_000,
|
|
5
|
-
s: 1000,
|
|
6
|
-
};
|
|
7
|
-
export function parseDuration(input) {
|
|
8
|
-
const invalid = () => {
|
|
9
|
-
throw new Error(`Invalid duration: '${input}'. Expected format like '7d', '24h', '30m', or '90s'.`);
|
|
10
|
-
};
|
|
11
|
-
if (typeof input !== 'string' || input.length === 0)
|
|
12
|
-
invalid();
|
|
13
|
-
if (/\s/.test(input))
|
|
14
|
-
invalid();
|
|
15
|
-
const match = /^(\d+)([a-zA-Z])$/.exec(input);
|
|
16
|
-
if (!match)
|
|
17
|
-
invalid();
|
|
18
|
-
const [, numStr, suffix] = match;
|
|
19
|
-
const multiplier = SUFFIX_MS[suffix];
|
|
20
|
-
if (multiplier === undefined)
|
|
21
|
-
invalid();
|
|
22
|
-
const value = Number(numStr);
|
|
23
|
-
if (!Number.isFinite(value) || value < 0)
|
|
24
|
-
invalid();
|
|
25
|
-
return value * multiplier;
|
|
26
|
-
}
|