@hyperdrive.bot/gut 0.1.11 → 0.1.13
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/README.md +128 -3
- package/dist/commands/context.d.ts +4 -0
- package/dist/commands/context.js +91 -6
- package/dist/commands/focus.js +3 -0
- package/dist/commands/worktree/create.d.ts +1 -0
- package/dist/commands/worktree/create.js +57 -15
- package/dist/utils/entity-spec.d.ts +15 -0
- package/dist/utils/entity-spec.js +29 -0
- package/oclif.manifest.json +30 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ $ npm install -g @hyperdrive.bot/gut
|
|
|
18
18
|
$ gut COMMAND
|
|
19
19
|
running command...
|
|
20
20
|
$ gut (--version)
|
|
21
|
-
@hyperdrive.bot/gut/0.1.
|
|
21
|
+
@hyperdrive.bot/gut/0.1.13 linux-x64 node-v22.22.1
|
|
22
22
|
$ gut --help [COMMAND]
|
|
23
23
|
USAGE
|
|
24
24
|
$ gut COMMAND
|
|
@@ -30,6 +30,9 @@ USAGE
|
|
|
30
30
|
* [`gut add [PATH]`](#gut-add-path)
|
|
31
31
|
* [`gut affected [ENTITY]`](#gut-affected-entity)
|
|
32
32
|
* [`gut audit`](#gut-audit)
|
|
33
|
+
* [`gut auth login`](#gut-auth-login)
|
|
34
|
+
* [`gut auth logout`](#gut-auth-logout)
|
|
35
|
+
* [`gut auth status`](#gut-auth-status)
|
|
33
36
|
* [`gut back`](#gut-back)
|
|
34
37
|
* [`gut checkout BRANCH`](#gut-checkout-branch)
|
|
35
38
|
* [`gut commit`](#gut-commit)
|
|
@@ -58,6 +61,7 @@ USAGE
|
|
|
58
61
|
* [`gut status`](#gut-status)
|
|
59
62
|
* [`gut sync`](#gut-sync)
|
|
60
63
|
* [`gut ticket`](#gut-ticket)
|
|
64
|
+
* [`gut ticket config`](#gut-ticket-config)
|
|
61
65
|
* [`gut ticket focus TICKETID`](#gut-ticket-focus-ticketid)
|
|
62
66
|
* [`gut ticket get TICKETID`](#gut-ticket-get-ticketid)
|
|
63
67
|
* [`gut ticket hint [HINT] TICKETID`](#gut-ticket-hint-hint-ticketid)
|
|
@@ -67,6 +71,7 @@ USAGE
|
|
|
67
71
|
* [`gut unfocus`](#gut-unfocus)
|
|
68
72
|
* [`gut used-by [ENTITY]`](#gut-used-by-entity)
|
|
69
73
|
* [`gut workspace ACTION`](#gut-workspace-action)
|
|
74
|
+
* [`gut worktree create NAME`](#gut-worktree-create-name)
|
|
70
75
|
|
|
71
76
|
## `gut add [PATH]`
|
|
72
77
|
|
|
@@ -155,6 +160,64 @@ EXAMPLES
|
|
|
155
160
|
$ gut audit --compliance
|
|
156
161
|
```
|
|
157
162
|
|
|
163
|
+
## `gut auth login`
|
|
164
|
+
|
|
165
|
+
Authenticate with Gut using OAuth 2.0 PKCE flow
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
USAGE
|
|
169
|
+
$ gut auth login [-d <value>] [-p <value>]
|
|
170
|
+
|
|
171
|
+
FLAGS
|
|
172
|
+
-d, --domain=<value> Tenant domain (e.g., acme.hyperdrive.bot)
|
|
173
|
+
-p, --port=<value> [default: 8766] Local callback server port
|
|
174
|
+
|
|
175
|
+
DESCRIPTION
|
|
176
|
+
Authenticate with Gut using OAuth 2.0 PKCE flow
|
|
177
|
+
|
|
178
|
+
EXAMPLES
|
|
179
|
+
$ gut auth login
|
|
180
|
+
|
|
181
|
+
$ gut auth login --domain acme.hyperdrive.bot
|
|
182
|
+
|
|
183
|
+
$ gut auth login --port 9876
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## `gut auth logout`
|
|
187
|
+
|
|
188
|
+
Remove stored credentials and logout
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
USAGE
|
|
192
|
+
$ gut auth logout
|
|
193
|
+
|
|
194
|
+
DESCRIPTION
|
|
195
|
+
Remove stored credentials and logout
|
|
196
|
+
|
|
197
|
+
EXAMPLES
|
|
198
|
+
$ gut auth logout
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## `gut auth status`
|
|
202
|
+
|
|
203
|
+
Show current authentication status
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
USAGE
|
|
207
|
+
$ gut auth status [-v]
|
|
208
|
+
|
|
209
|
+
FLAGS
|
|
210
|
+
-v, --verbose Show detailed credential information
|
|
211
|
+
|
|
212
|
+
DESCRIPTION
|
|
213
|
+
Show current authentication status
|
|
214
|
+
|
|
215
|
+
EXAMPLES
|
|
216
|
+
$ gut auth status
|
|
217
|
+
|
|
218
|
+
$ gut auth status --verbose
|
|
219
|
+
```
|
|
220
|
+
|
|
158
221
|
## `gut back`
|
|
159
222
|
|
|
160
223
|
Navigate back to the previous focus
|
|
@@ -842,6 +905,31 @@ EXAMPLES
|
|
|
842
905
|
$ gut ticket update PROJ-1234 --status in_progress
|
|
843
906
|
```
|
|
844
907
|
|
|
908
|
+
## `gut ticket config`
|
|
909
|
+
|
|
910
|
+
Configure ticket source (JIRA, GitHub, etc.) for this tenant
|
|
911
|
+
|
|
912
|
+
```
|
|
913
|
+
USAGE
|
|
914
|
+
$ gut ticket config [--secret-arn <value>] [-j] [--show] [--type jira|github|linear] [--url <value>]
|
|
915
|
+
|
|
916
|
+
FLAGS
|
|
917
|
+
-j, --json output as JSON
|
|
918
|
+
--secret-arn=<value> AWS Secrets Manager ARN for credentials
|
|
919
|
+
--show show current configuration
|
|
920
|
+
--type=<option> source type
|
|
921
|
+
<options: jira|github|linear>
|
|
922
|
+
--url=<value> base URL (e.g., https://company.atlassian.net)
|
|
923
|
+
|
|
924
|
+
DESCRIPTION
|
|
925
|
+
Configure ticket source (JIRA, GitHub, etc.) for this tenant
|
|
926
|
+
|
|
927
|
+
EXAMPLES
|
|
928
|
+
$ gut ticket config --show
|
|
929
|
+
|
|
930
|
+
$ gut ticket config --type jira --url https://company.atlassian.net --secret-arn arn:aws:secretsmanager:...
|
|
931
|
+
```
|
|
932
|
+
|
|
845
933
|
## `gut ticket focus TICKETID`
|
|
846
934
|
|
|
847
935
|
Focus on a ticket - downloads manifest and clones required entities
|
|
@@ -959,25 +1047,35 @@ Sync ticket state with external source (JIRA, GitHub, etc.)
|
|
|
959
1047
|
|
|
960
1048
|
```
|
|
961
1049
|
USAGE
|
|
962
|
-
$ gut ticket sync TICKETID [-d push|pull] [-j]
|
|
1050
|
+
$ gut ticket sync TICKETID [-d push|pull] [-j] [--no-enrich]
|
|
963
1051
|
|
|
964
1052
|
ARGUMENTS
|
|
965
|
-
TICKETID ticket ID to sync
|
|
1053
|
+
TICKETID ticket ID to sync (e.g., PROJ-1234)
|
|
966
1054
|
|
|
967
1055
|
FLAGS
|
|
968
1056
|
-d, --direction=<option> [default: push] sync direction (push: gut -> source, pull: source -> gut)
|
|
969
1057
|
<options: push|pull>
|
|
970
1058
|
-j, --json output as JSON
|
|
1059
|
+
--no-enrich skip enrichment queue when pulling new tickets
|
|
971
1060
|
|
|
972
1061
|
DESCRIPTION
|
|
973
1062
|
Sync ticket state with external source (JIRA, GitHub, etc.)
|
|
974
1063
|
|
|
1064
|
+
For pull direction:
|
|
1065
|
+
- If ticket exists in gut: updates from source
|
|
1066
|
+
- If ticket doesn't exist: creates it and queues enrichment
|
|
1067
|
+
|
|
1068
|
+
For push direction:
|
|
1069
|
+
- Updates source system with gut ticket state
|
|
1070
|
+
|
|
975
1071
|
EXAMPLES
|
|
976
1072
|
$ gut ticket sync PROJ-1234
|
|
977
1073
|
|
|
978
1074
|
$ gut ticket sync PROJ-1234 --direction push
|
|
979
1075
|
|
|
980
1076
|
$ gut ticket sync PROJ-1234 --direction pull
|
|
1077
|
+
|
|
1078
|
+
$ gut ticket sync PROJ-1234 --direction pull --no-enrich
|
|
981
1079
|
```
|
|
982
1080
|
|
|
983
1081
|
## `gut ticket update TICKETID`
|
|
@@ -1075,4 +1173,31 @@ EXAMPLES
|
|
|
1075
1173
|
|
|
1076
1174
|
$ gut workspace generate-metadata
|
|
1077
1175
|
```
|
|
1176
|
+
|
|
1177
|
+
## `gut worktree create NAME`
|
|
1178
|
+
|
|
1179
|
+
Create mirrored worktrees for super-repo and focused entities
|
|
1180
|
+
|
|
1181
|
+
```
|
|
1182
|
+
USAGE
|
|
1183
|
+
$ gut worktree create NAME [--base-dir <value>] [--from <value>] [--install]
|
|
1184
|
+
|
|
1185
|
+
ARGUMENTS
|
|
1186
|
+
NAME Branch name for the worktree
|
|
1187
|
+
|
|
1188
|
+
FLAGS
|
|
1189
|
+
--base-dir=<value> [default: /tmp/gut-worktrees] Root directory for worktrees
|
|
1190
|
+
--from=<value> Base branch to create from (defaults to current branch)
|
|
1191
|
+
--install Run pnpm install after creation
|
|
1192
|
+
|
|
1193
|
+
DESCRIPTION
|
|
1194
|
+
Create mirrored worktrees for super-repo and focused entities
|
|
1195
|
+
|
|
1196
|
+
EXAMPLES
|
|
1197
|
+
$ gut worktree create workflow/deploy-batch
|
|
1198
|
+
|
|
1199
|
+
$ gut worktree create workflow/deploy-batch --from master --install
|
|
1200
|
+
|
|
1201
|
+
$ gut worktree create feature/story-42 --base-dir /home/user/worktrees
|
|
1202
|
+
```
|
|
1078
1203
|
<!-- commandsstop -->
|
|
@@ -2,5 +2,9 @@ import { BaseCommand } from '../base-command.js';
|
|
|
2
2
|
export default class Context extends BaseCommand {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
};
|
|
5
9
|
run(): Promise<void>;
|
|
6
10
|
}
|
package/dist/commands/context.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
1
2
|
import chalk from 'chalk';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { BaseCommand } from '../base-command.js';
|
|
@@ -5,22 +6,106 @@ export default class Context extends BaseCommand {
|
|
|
5
6
|
static description = 'Show current focus context with entity details';
|
|
6
7
|
static examples = [
|
|
7
8
|
'<%= config.bin %> <%= command.id %>',
|
|
9
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --json --all',
|
|
8
11
|
];
|
|
12
|
+
static flags = {
|
|
13
|
+
all: Flags.boolean({
|
|
14
|
+
default: false,
|
|
15
|
+
description: 'Include all registered entities, not just focused ones (implies --json or adds to text output)',
|
|
16
|
+
}),
|
|
17
|
+
json: Flags.boolean({
|
|
18
|
+
default: false,
|
|
19
|
+
description: 'Emit structured JSON for programmatic consumption (includes per-entity branch + hasUncommitted)',
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
9
22
|
async run() {
|
|
10
|
-
await this.parse(Context);
|
|
23
|
+
const { flags } = await this.parse(Context);
|
|
24
|
+
const workspaceRoot = this.configService.getWorkspaceRoot();
|
|
11
25
|
const focusedEntities = await this.focusService.getFocusedEntities();
|
|
12
|
-
|
|
26
|
+
const focus = await this.focusService.getCurrentFocus();
|
|
27
|
+
const focusedNames = new Set(focusedEntities.map(e => e.name));
|
|
28
|
+
// For JSON or --all, resolve the full entity set. For normal text output,
|
|
29
|
+
// keep the existing behavior (focused only).
|
|
30
|
+
const includeAll = flags.json || flags.all;
|
|
31
|
+
const entitiesToReport = includeAll
|
|
32
|
+
? this.entityService.getAllEntities()
|
|
33
|
+
: focusedEntities;
|
|
34
|
+
// Enrich each entity with branch + uncommitted status. Entities may not
|
|
35
|
+
// exist on disk (not cloned), so be defensive.
|
|
36
|
+
const enriched = [];
|
|
37
|
+
for (const entity of entitiesToReport) {
|
|
38
|
+
const relativePath = entity.path.replace(/^\.\//, '');
|
|
39
|
+
const absolutePath = path.isAbsolute(entity.path)
|
|
40
|
+
? entity.path
|
|
41
|
+
: path.join(workspaceRoot, relativePath);
|
|
42
|
+
let currentBranch = null;
|
|
43
|
+
let hasUncommitted = false;
|
|
44
|
+
let pathExists = false;
|
|
45
|
+
try {
|
|
46
|
+
const { existsSync } = await import('node:fs');
|
|
47
|
+
pathExists = existsSync(path.join(absolutePath, '.git')) || existsSync(absolutePath);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
pathExists = false;
|
|
51
|
+
}
|
|
52
|
+
if (pathExists) {
|
|
53
|
+
try {
|
|
54
|
+
currentBranch = await this.gitService.getCurrentBranch(absolutePath);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
currentBranch = null;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
hasUncommitted = await this.gitService.hasChanges(absolutePath);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
hasUncommitted = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
enriched.push({
|
|
67
|
+
currentBranch,
|
|
68
|
+
focused: focusedNames.has(entity.name),
|
|
69
|
+
hasUncommitted,
|
|
70
|
+
name: entity.name,
|
|
71
|
+
path: relativePath,
|
|
72
|
+
pathExists,
|
|
73
|
+
type: entity.type,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (flags.json) {
|
|
77
|
+
const payload = {
|
|
78
|
+
entities: enriched,
|
|
79
|
+
focus: {
|
|
80
|
+
entities: [...focusedNames],
|
|
81
|
+
mode: focus?.mode ?? null,
|
|
82
|
+
},
|
|
83
|
+
workspaceRoot,
|
|
84
|
+
};
|
|
85
|
+
// Emit compact, machine-readable JSON on stdout. No chalk, no decoration.
|
|
86
|
+
this.log(JSON.stringify(payload, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (focusedEntities.length === 0 && !flags.all) {
|
|
13
90
|
this.log(chalk.yellow('No entities are currently focused'));
|
|
14
91
|
this.log(chalk.dim('Use "gut focus <entity>" to set focus'));
|
|
15
92
|
return;
|
|
16
93
|
}
|
|
17
|
-
const workspaceRoot = this.configService.getWorkspaceRoot();
|
|
18
94
|
this.log(chalk.bold('\n📍 Current Focus Context'));
|
|
19
95
|
this.log(chalk.dim('─'.repeat(50)));
|
|
20
|
-
for (const entity of
|
|
21
|
-
|
|
96
|
+
for (const entity of enriched) {
|
|
97
|
+
if (!flags.all && !entity.focused)
|
|
98
|
+
continue;
|
|
99
|
+
const marker = entity.focused ? chalk.green('▸') : chalk.dim('·');
|
|
100
|
+
this.log(`\n${marker} ${chalk.bold(entity.name)}`);
|
|
22
101
|
this.log(` ${chalk.dim('Type:')} ${entity.type}`);
|
|
23
|
-
this.log(` ${chalk.dim('Path:')} ${
|
|
102
|
+
this.log(` ${chalk.dim('Path:')} ${entity.path}`);
|
|
103
|
+
if (entity.pathExists) {
|
|
104
|
+
this.log(` ${chalk.dim('Branch:')} ${entity.currentBranch ?? chalk.red('(unknown)')}${entity.hasUncommitted ? chalk.yellow(' *dirty') : ''}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.log(` ${chalk.dim('Branch:')} ${chalk.dim('(not checked out)')}`);
|
|
108
|
+
}
|
|
24
109
|
}
|
|
25
110
|
this.log(chalk.dim('\n─'.repeat(50)));
|
|
26
111
|
this.log(`${chalk.dim('Total focused entities:')} ${focusedEntities.length}`);
|
package/dist/commands/focus.js
CHANGED
|
@@ -125,6 +125,9 @@ export default class Focus extends BaseCommand {
|
|
|
125
125
|
this.log(` ${this.getTypeEmoji(entity.type)} ${entity.name} (${entity.type})`);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
+
// Machine-readable confirmation line for programmatic callers (e.g.,
|
|
129
|
+
// bmad-workflow execSync). Always printed, grep-friendly format.
|
|
130
|
+
this.log(`FOCUSED: ${entities.map(e => e.name).join(',')}`);
|
|
128
131
|
}
|
|
129
132
|
catch (error) {
|
|
130
133
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -7,6 +7,7 @@ export default class WorktreeCreate extends BaseCommand {
|
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
9
|
'base-dir': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
entity: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
from: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
install: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
};
|
|
@@ -2,8 +2,11 @@ import { Args, Flags } from '@oclif/core';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { BaseCommand } from '../../base-command.js';
|
|
8
|
+
import { parseEntitySpecs } from '../../utils/entity-spec.js';
|
|
9
|
+
const xdgDefaultBaseDir = path.join(os.homedir(), '.local', 'share', 'gut', 'worktrees');
|
|
7
10
|
export default class WorktreeCreate extends BaseCommand {
|
|
8
11
|
static args = {
|
|
9
12
|
name: Args.string({ description: 'Branch name for the worktree', required: true }),
|
|
@@ -13,9 +16,18 @@ export default class WorktreeCreate extends BaseCommand {
|
|
|
13
16
|
'<%= config.bin %> worktree create workflow/deploy-batch',
|
|
14
17
|
'<%= config.bin %> worktree create workflow/deploy-batch --from master --install',
|
|
15
18
|
'<%= config.bin %> worktree create feature/story-42 --base-dir /home/user/worktrees',
|
|
19
|
+
'<%= config.bin %> worktree create seo-lake --entity serverless-api:feature/seo-lake --entity sign',
|
|
20
|
+
'GUT_WORKTREE_DIR=/tmp/gut-worktrees <%= config.bin %> worktree create feature/ci-run',
|
|
16
21
|
];
|
|
17
22
|
static flags = {
|
|
18
|
-
'base-dir': Flags.string({
|
|
23
|
+
'base-dir': Flags.string({
|
|
24
|
+
default: async () => process.env.GUT_WORKTREE_DIR ?? xdgDefaultBaseDir,
|
|
25
|
+
description: 'Root directory for worktrees (default: ~/.local/share/gut/worktrees, override via GUT_WORKTREE_DIR env var; explicit --base-dir wins)',
|
|
26
|
+
}),
|
|
27
|
+
entity: Flags.string({
|
|
28
|
+
description: 'Entity to include, optionally pinned to a branch via "name:branch". Repeatable. Overrides current focus when provided.',
|
|
29
|
+
multiple: true,
|
|
30
|
+
}),
|
|
19
31
|
from: Flags.string({ description: 'Base branch to create from (defaults to current branch)' }),
|
|
20
32
|
install: Flags.boolean({ default: false, description: 'Run pnpm install after creation' }),
|
|
21
33
|
};
|
|
@@ -24,6 +36,9 @@ export default class WorktreeCreate extends BaseCommand {
|
|
|
24
36
|
const workspaceRoot = this.configService.getWorkspaceRoot();
|
|
25
37
|
const slug = args.name.replace(/\//g, '-');
|
|
26
38
|
const wtPath = path.join(flags['base-dir'], slug);
|
|
39
|
+
// Ensure the base directory exists — git worktree add creates the target
|
|
40
|
+
// dir, but the parent (the base dir itself) must already exist.
|
|
41
|
+
fs.mkdirSync(flags['base-dir'], { recursive: true });
|
|
27
42
|
const baseBranch = flags.from ?? await this.gitService.getCurrentBranch(workspaceRoot);
|
|
28
43
|
// --- Validation (AC: 7) ---
|
|
29
44
|
const existing = this.worktreeService.get(args.name);
|
|
@@ -35,10 +50,35 @@ export default class WorktreeCreate extends BaseCommand {
|
|
|
35
50
|
if (branchCollision) {
|
|
36
51
|
this.error(`Branch "${args.name}" already has a worktree at ${branchCollision.path}. Remove it first or use a different name.`);
|
|
37
52
|
}
|
|
38
|
-
// ---
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
// --- Resolve entity set: --entity flag overrides focus ---
|
|
54
|
+
let entitySpecs;
|
|
55
|
+
try {
|
|
56
|
+
entitySpecs = parseEntitySpecs(flags.entity);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
60
|
+
this.error(message);
|
|
61
|
+
}
|
|
62
|
+
let selectedEntities;
|
|
63
|
+
const branchOverrides = new Map();
|
|
64
|
+
if (entitySpecs.length > 0) {
|
|
65
|
+
selectedEntities = [];
|
|
66
|
+
for (const spec of entitySpecs) {
|
|
67
|
+
const entity = this.entityService.findEntity(spec.name);
|
|
68
|
+
if (!entity) {
|
|
69
|
+
this.error(`Entity "${spec.name}" not found in workspace config. Run \`gut entity list\` to see available entities.`);
|
|
70
|
+
}
|
|
71
|
+
selectedEntities.push(entity);
|
|
72
|
+
if (spec.branch) {
|
|
73
|
+
branchOverrides.set(entity.name, spec.branch);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
selectedEntities = await this.focusService.getFocusedEntities();
|
|
79
|
+
if (selectedEntities.length === 0) {
|
|
80
|
+
this.error('No entities focused and no --entity flag provided. Use "gut focus <entity>" first, or pass --entity <name> (repeatable).');
|
|
81
|
+
}
|
|
42
82
|
}
|
|
43
83
|
// --- Create worktrees with atomic rollback (AC: 1, 2, 9) ---
|
|
44
84
|
const createdWorktrees = [];
|
|
@@ -49,24 +89,26 @@ export default class WorktreeCreate extends BaseCommand {
|
|
|
49
89
|
createdWorktrees.push({ repoPath: workspaceRoot, wtPath });
|
|
50
90
|
this.log(chalk.green('✓ Super-repo worktree created at ' + wtPath));
|
|
51
91
|
// Per-entity worktrees (AC: 2)
|
|
52
|
-
for (const entity of
|
|
92
|
+
for (const entity of selectedEntities) {
|
|
53
93
|
const entityRelativePath = entity.path.replace(/^\.\//, '');
|
|
54
94
|
const entityWtPath = path.join(wtPath, entityRelativePath);
|
|
55
95
|
const entityMainPath = path.join(workspaceRoot, entityRelativePath);
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
96
|
+
// Branch override from --entity name:branch wins; otherwise use entity's
|
|
97
|
+
// current branch in the main repo.
|
|
98
|
+
const override = branchOverrides.get(entity.name);
|
|
99
|
+
const entityBaseBranch = override ?? await this.gitService.getCurrentBranch(entityMainPath);
|
|
100
|
+
if (override) {
|
|
101
|
+
this.log(chalk.cyan(` Branch override: ${entity.name} → ${override}`));
|
|
102
|
+
}
|
|
103
|
+
else if (entityBaseBranch !== baseBranch) {
|
|
104
|
+
this.log(chalk.yellow(`⚠ Entity "${entity.name}" is on "${entityBaseBranch}" (super-repo: "${baseBranch}")`));
|
|
63
105
|
}
|
|
64
106
|
// Remove submodule stub
|
|
65
107
|
fs.rmSync(entityWtPath, { force: true, recursive: true });
|
|
66
108
|
// Create entity worktree
|
|
67
109
|
await this.gitService.worktreeAdd(entityMainPath, entityWtPath, args.name, entityBaseBranch);
|
|
68
110
|
createdWorktrees.push({ repoPath: entityMainPath, wtPath: entityWtPath });
|
|
69
|
-
this.log(chalk.green(`✓ Entity worktree: ${entity.name} → ${args.name}`));
|
|
111
|
+
this.log(chalk.green(`✓ Entity worktree: ${entity.name} → ${args.name} (from ${entityBaseBranch})`));
|
|
70
112
|
entityRecords.push({
|
|
71
113
|
branch: args.name,
|
|
72
114
|
entityName: entity.name,
|
|
@@ -81,7 +123,7 @@ export default class WorktreeCreate extends BaseCommand {
|
|
|
81
123
|
await this.gitService.worktreeRemove(entry.repoPath, entry.wtPath, true).catch(() => { });
|
|
82
124
|
}
|
|
83
125
|
await this.gitService.worktreePrune(workspaceRoot).catch(() => { });
|
|
84
|
-
for (const entity of
|
|
126
|
+
for (const entity of selectedEntities) {
|
|
85
127
|
const entityRelativePath = entity.path.replace(/^\.\//, '');
|
|
86
128
|
const entityMainPath = path.join(workspaceRoot, entityRelativePath);
|
|
87
129
|
await this.gitService.worktreePrune(entityMainPath).catch(() => { });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses `--entity` flag values for `gut worktree create`.
|
|
3
|
+
*
|
|
4
|
+
* Each spec is either a bare entity name (`serverless-api`) or a pinned
|
|
5
|
+
* spec (`serverless-api:feature/seo-lake`). The first `:` separates the
|
|
6
|
+
* entity name from the branch; the branch itself may contain `/` or `-`.
|
|
7
|
+
*
|
|
8
|
+
* Invalid shapes throw with a descriptive message so callers can surface
|
|
9
|
+
* the error to the user.
|
|
10
|
+
*/
|
|
11
|
+
export interface EntitySpec {
|
|
12
|
+
branch?: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function parseEntitySpecs(specs: string[] | undefined): EntitySpec[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function parseEntitySpecs(specs) {
|
|
2
|
+
if (!specs || specs.length === 0)
|
|
3
|
+
return [];
|
|
4
|
+
const parsed = [];
|
|
5
|
+
for (const raw of specs) {
|
|
6
|
+
const trimmed = raw.trim();
|
|
7
|
+
if (!trimmed)
|
|
8
|
+
continue;
|
|
9
|
+
const colonIdx = trimmed.indexOf(':');
|
|
10
|
+
if (colonIdx === -1) {
|
|
11
|
+
parsed.push({ name: trimmed });
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const name = trimmed.slice(0, colonIdx).trim();
|
|
15
|
+
const branch = trimmed.slice(colonIdx + 1).trim();
|
|
16
|
+
if (!name) {
|
|
17
|
+
throw new Error(`Invalid --entity spec "${raw}": missing entity name before ":"`);
|
|
18
|
+
}
|
|
19
|
+
if (branch) {
|
|
20
|
+
parsed.push({ branch, name });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
// "name:" is tolerated — same as bare name. Useful if a caller builds
|
|
24
|
+
// specs programmatically and omits the branch for some entities.
|
|
25
|
+
parsed.push({ name });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -301,9 +301,24 @@
|
|
|
301
301
|
"args": {},
|
|
302
302
|
"description": "Show current focus context with entity details",
|
|
303
303
|
"examples": [
|
|
304
|
-
"<%= config.bin %> <%= command.id %>"
|
|
304
|
+
"<%= config.bin %> <%= command.id %>",
|
|
305
|
+
"<%= config.bin %> <%= command.id %> --json",
|
|
306
|
+
"<%= config.bin %> <%= command.id %> --json --all"
|
|
305
307
|
],
|
|
306
|
-
"flags": {
|
|
308
|
+
"flags": {
|
|
309
|
+
"all": {
|
|
310
|
+
"description": "Include all registered entities, not just focused ones (implies --json or adds to text output)",
|
|
311
|
+
"name": "all",
|
|
312
|
+
"allowNo": false,
|
|
313
|
+
"type": "boolean"
|
|
314
|
+
},
|
|
315
|
+
"json": {
|
|
316
|
+
"description": "Emit structured JSON for programmatic consumption (includes per-entity branch + hasUncommitted)",
|
|
317
|
+
"name": "json",
|
|
318
|
+
"allowNo": false,
|
|
319
|
+
"type": "boolean"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
307
322
|
"hasDynamicHelp": false,
|
|
308
323
|
"hiddenAliases": [],
|
|
309
324
|
"id": "context",
|
|
@@ -2186,17 +2201,26 @@
|
|
|
2186
2201
|
"examples": [
|
|
2187
2202
|
"<%= config.bin %> worktree create workflow/deploy-batch",
|
|
2188
2203
|
"<%= config.bin %> worktree create workflow/deploy-batch --from master --install",
|
|
2189
|
-
"<%= config.bin %> worktree create feature/story-42 --base-dir /home/user/worktrees"
|
|
2204
|
+
"<%= config.bin %> worktree create feature/story-42 --base-dir /home/user/worktrees",
|
|
2205
|
+
"<%= config.bin %> worktree create seo-lake --entity serverless-api:feature/seo-lake --entity sign",
|
|
2206
|
+
"GUT_WORKTREE_DIR=/tmp/gut-worktrees <%= config.bin %> worktree create feature/ci-run"
|
|
2190
2207
|
],
|
|
2191
2208
|
"flags": {
|
|
2192
2209
|
"base-dir": {
|
|
2193
|
-
"description": "Root directory for worktrees",
|
|
2210
|
+
"description": "Root directory for worktrees (default: ~/.local/share/gut/worktrees, override via GUT_WORKTREE_DIR env var; explicit --base-dir wins)",
|
|
2194
2211
|
"name": "base-dir",
|
|
2195
|
-
"default": "/
|
|
2212
|
+
"default": "/root/.local/share/gut/worktrees",
|
|
2196
2213
|
"hasDynamicHelp": false,
|
|
2197
2214
|
"multiple": false,
|
|
2198
2215
|
"type": "option"
|
|
2199
2216
|
},
|
|
2217
|
+
"entity": {
|
|
2218
|
+
"description": "Entity to include, optionally pinned to a branch via \"name:branch\". Repeatable. Overrides current focus when provided.",
|
|
2219
|
+
"name": "entity",
|
|
2220
|
+
"hasDynamicHelp": false,
|
|
2221
|
+
"multiple": true,
|
|
2222
|
+
"type": "option"
|
|
2223
|
+
},
|
|
2200
2224
|
"from": {
|
|
2201
2225
|
"description": "Base branch to create from (defaults to current branch)",
|
|
2202
2226
|
"name": "from",
|
|
@@ -2227,5 +2251,5 @@
|
|
|
2227
2251
|
]
|
|
2228
2252
|
}
|
|
2229
2253
|
},
|
|
2230
|
-
"version": "0.1.
|
|
2254
|
+
"version": "0.1.13"
|
|
2231
2255
|
}
|
package/package.json
CHANGED