@hyperdrive.bot/gut 0.1.8 → 0.1.10
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 +1048 -1
- package/dist/base-command.d.ts +22 -0
- package/dist/base-command.js +99 -0
- package/dist/commands/add.d.ts +14 -0
- package/dist/commands/add.js +70 -0
- package/dist/commands/affected.d.ts +23 -0
- package/dist/commands/affected.js +323 -0
- package/dist/commands/audit.d.ts +33 -0
- package/dist/commands/audit.js +594 -0
- package/dist/commands/back.d.ts +6 -0
- package/dist/commands/back.js +29 -0
- package/dist/commands/checkout.d.ts +14 -0
- package/dist/commands/checkout.js +124 -0
- package/dist/commands/commit.d.ts +11 -0
- package/dist/commands/commit.js +107 -0
- package/dist/commands/context.d.ts +6 -0
- package/dist/commands/context.js +32 -0
- package/dist/commands/contexts.d.ts +7 -0
- package/dist/commands/contexts.js +88 -0
- package/dist/commands/deps.d.ts +10 -0
- package/dist/commands/deps.js +100 -0
- package/dist/commands/entity/add.d.ts +16 -0
- package/dist/commands/entity/add.js +103 -0
- package/dist/commands/entity/clone-all.d.ts +18 -0
- package/dist/commands/entity/clone-all.js +166 -0
- package/dist/commands/entity/clone.d.ts +17 -0
- package/dist/commands/entity/clone.js +132 -0
- package/dist/commands/entity/list.d.ts +11 -0
- package/dist/commands/entity/list.js +80 -0
- package/dist/commands/entity/remove.d.ts +12 -0
- package/dist/commands/entity/remove.js +54 -0
- package/dist/commands/extract.d.ts +35 -0
- package/dist/commands/extract.js +483 -0
- package/dist/commands/focus.d.ts +19 -0
- package/dist/commands/focus.js +137 -0
- package/dist/commands/graph.d.ts +18 -0
- package/dist/commands/graph.js +273 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +75 -0
- package/dist/commands/insights.d.ts +21 -0
- package/dist/commands/insights.js +465 -0
- package/dist/commands/patterns.d.ts +40 -0
- package/dist/commands/patterns.js +405 -0
- package/dist/commands/pull.d.ts +11 -0
- package/dist/commands/pull.js +121 -0
- package/dist/commands/push.d.ts +11 -0
- package/dist/commands/push.js +97 -0
- package/dist/commands/quick-setup.d.ts +20 -0
- package/dist/commands/quick-setup.js +417 -0
- package/dist/commands/recent.d.ts +9 -0
- package/dist/commands/recent.js +51 -0
- package/dist/commands/related.d.ts +23 -0
- package/dist/commands/related.js +255 -0
- package/dist/commands/repos.d.ts +17 -0
- package/dist/commands/repos.js +184 -0
- package/dist/commands/stack.d.ts +10 -0
- package/dist/commands/stack.js +78 -0
- package/dist/commands/status.d.ts +13 -0
- package/dist/commands/status.js +193 -0
- package/dist/commands/sync.d.ts +11 -0
- package/dist/commands/sync.js +139 -0
- package/dist/commands/ticket/focus.d.ts +20 -0
- package/dist/commands/ticket/focus.js +217 -0
- package/dist/commands/ticket/get.d.ts +15 -0
- package/dist/commands/ticket/get.js +168 -0
- package/dist/commands/ticket/hint.d.ts +16 -0
- package/dist/commands/ticket/hint.js +147 -0
- package/dist/commands/ticket/index.d.ts +10 -0
- package/dist/commands/ticket/index.js +60 -0
- package/dist/commands/ticket/list.d.ts +13 -0
- package/dist/commands/ticket/list.js +120 -0
- package/dist/commands/ticket/sync.d.ts +14 -0
- package/dist/commands/ticket/sync.js +85 -0
- package/dist/commands/ticket/update.d.ts +17 -0
- package/dist/commands/ticket/update.js +142 -0
- package/dist/commands/unfocus.d.ts +6 -0
- package/dist/commands/unfocus.js +19 -0
- package/dist/commands/used-by.d.ts +13 -0
- package/dist/commands/used-by.js +110 -0
- package/dist/commands/workspace.d.ts +22 -0
- package/dist/commands/workspace.js +372 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +16 -0
- package/dist/models/entity.model.d.ts +234 -0
- package/dist/models/entity.model.js +1 -0
- package/dist/models/ticket.model.d.ts +117 -0
- package/dist/models/ticket.model.js +43 -0
- package/dist/services/auth.service.d.ts +15 -0
- package/dist/services/auth.service.js +26 -0
- package/dist/services/config.service.d.ts +34 -0
- package/dist/services/config.service.js +234 -0
- package/dist/services/entity.service.d.ts +20 -0
- package/dist/services/entity.service.js +127 -0
- package/dist/services/focus.service.d.ts +71 -0
- package/dist/services/focus.service.js +614 -0
- package/dist/services/git.service.d.ts +39 -0
- package/dist/services/git.service.js +188 -0
- package/dist/services/gut-api.service.d.ts +53 -0
- package/dist/services/gut-api.service.js +99 -0
- package/dist/services/ticket.service.d.ts +84 -0
- package/dist/services/ticket.service.js +207 -0
- package/dist/utils/display.d.ts +26 -0
- package/dist/utils/display.js +145 -0
- package/dist/utils/filesystem.d.ts +32 -0
- package/dist/utils/filesystem.js +198 -0
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.js +14 -0
- package/dist/utils/validation.d.ts +22 -0
- package/dist/utils/validation.js +192 -0
- package/oclif.manifest.json +2006 -0
- package/package.json +11 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { BaseCommand } from '../base-command.js';
|
|
4
|
+
export default class Status extends BaseCommand {
|
|
5
|
+
static description = 'Show git status for focused entities';
|
|
6
|
+
static examples = [
|
|
7
|
+
'<%= config.bin %> <%= command.id %>',
|
|
8
|
+
'<%= config.bin %> <%= command.id %> --all',
|
|
9
|
+
'<%= config.bin %> <%= command.id %> --verbose',
|
|
10
|
+
];
|
|
11
|
+
static flags = {
|
|
12
|
+
all: Flags.boolean({
|
|
13
|
+
char: 'a',
|
|
14
|
+
description: 'show status for all entities',
|
|
15
|
+
}),
|
|
16
|
+
json: Flags.boolean({
|
|
17
|
+
description: 'output as JSON',
|
|
18
|
+
}),
|
|
19
|
+
verbose: Flags.boolean({
|
|
20
|
+
char: 'v',
|
|
21
|
+
description: 'show detailed status',
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { flags } = await this.parse(Status);
|
|
26
|
+
// Get entities to check
|
|
27
|
+
const entities = flags.all
|
|
28
|
+
? this.entityService.getAllEntities()
|
|
29
|
+
: await this.focusService.getFocusedEntities();
|
|
30
|
+
if (entities.length === 0) {
|
|
31
|
+
if (flags.all) {
|
|
32
|
+
this.error('No entities configured');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.error('No entities focused. Use "gut focus <entity>" first.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Collect status for each entity
|
|
39
|
+
const statuses = [];
|
|
40
|
+
for (const entity of entities) {
|
|
41
|
+
const entityPath = this.entityService.resolveEntityPath(entity);
|
|
42
|
+
try {
|
|
43
|
+
// Check if it's a git repository
|
|
44
|
+
const isRepo = await this.gitService.isRepository(entityPath);
|
|
45
|
+
if (isRepo) {
|
|
46
|
+
const status = await this.gitService.getStatus(entityPath);
|
|
47
|
+
statuses.push({
|
|
48
|
+
...status,
|
|
49
|
+
entity: entity.name,
|
|
50
|
+
error: undefined,
|
|
51
|
+
hasChanges: status.hasChanges,
|
|
52
|
+
type: entity.type,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
statuses.push({
|
|
57
|
+
ahead: 0,
|
|
58
|
+
behind: 0,
|
|
59
|
+
branch: '',
|
|
60
|
+
changes: [],
|
|
61
|
+
entity: entity.name,
|
|
62
|
+
error: 'Not a git repository',
|
|
63
|
+
hasChanges: false,
|
|
64
|
+
path: entityPath,
|
|
65
|
+
type: entity.type,
|
|
66
|
+
untracked: [],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
statuses.push({
|
|
72
|
+
ahead: 0,
|
|
73
|
+
behind: 0,
|
|
74
|
+
branch: '',
|
|
75
|
+
changes: [],
|
|
76
|
+
entity: entity.name,
|
|
77
|
+
error: error instanceof Error ? error.message : String(error),
|
|
78
|
+
hasChanges: false,
|
|
79
|
+
path: entityPath,
|
|
80
|
+
type: entity.type,
|
|
81
|
+
untracked: [],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Output results
|
|
86
|
+
if (flags.json) {
|
|
87
|
+
this.log(JSON.stringify(statuses, null, 2));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Enhanced workspace-style display
|
|
91
|
+
await this.displayEnhancedStatus(statuses, flags.verbose);
|
|
92
|
+
}
|
|
93
|
+
async displayEnhancedStatus(statuses, verbose) {
|
|
94
|
+
const currentFocus = await this.focusService.getCurrentFocus();
|
|
95
|
+
// Header with workspace context
|
|
96
|
+
this.log(chalk.bold('\n=== DevSquad Workspace Status ==='));
|
|
97
|
+
if (currentFocus) {
|
|
98
|
+
const focusDescription = await this.focusService.getFocusDescription();
|
|
99
|
+
this.log(`📍 Current focus: ${chalk.cyan(focusDescription)}`);
|
|
100
|
+
if (currentFocus.mode) {
|
|
101
|
+
this.log(`🎯 Mode: ${chalk.yellow(currentFocus.mode)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.log(`📍 Current focus: ${chalk.dim('none')}`);
|
|
106
|
+
}
|
|
107
|
+
// Last sync info (placeholder for now)
|
|
108
|
+
this.log(`🔄 Last sync: ${chalk.dim('just now')}`);
|
|
109
|
+
this.log('');
|
|
110
|
+
// Group statuses by entity type
|
|
111
|
+
const byType = {};
|
|
112
|
+
for (const status of statuses) {
|
|
113
|
+
if (!byType[status.type])
|
|
114
|
+
byType[status.type] = [];
|
|
115
|
+
byType[status.type].push(status);
|
|
116
|
+
}
|
|
117
|
+
// Display entities by type
|
|
118
|
+
const typeOrder = ['client', 'prospect', 'company', 'initiative', 'system', 'delivery', 'module', 'service', 'tool'];
|
|
119
|
+
for (const type of typeOrder) {
|
|
120
|
+
if (!byType[type] || byType[type].length === 0)
|
|
121
|
+
continue;
|
|
122
|
+
const entities = byType[type];
|
|
123
|
+
const dirtyCount = entities.filter((e) => e.hasChanges).length;
|
|
124
|
+
if (dirtyCount > 0) {
|
|
125
|
+
this.log(chalk.bold(`${type.toUpperCase()}:`));
|
|
126
|
+
for (const status of entities) {
|
|
127
|
+
if (status.hasChanges) {
|
|
128
|
+
this.displayEntityStatus(status, verbose);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.log('');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Show clean entities if verbose
|
|
135
|
+
if (verbose) {
|
|
136
|
+
const cleanEntities = statuses.filter(s => !s.hasChanges && !s.error);
|
|
137
|
+
if (cleanEntities.length > 0) {
|
|
138
|
+
this.log(chalk.dim('CLEAN:'));
|
|
139
|
+
for (const status of cleanEntities) {
|
|
140
|
+
this.log(` ${this.getTypeEmoji(status.type)} ${status.entity}/ ${chalk.dim('(clean)')}`);
|
|
141
|
+
}
|
|
142
|
+
this.log('');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Enhanced summary
|
|
146
|
+
const dirtyRepos = statuses.filter(s => s.hasChanges).length;
|
|
147
|
+
const totalRepos = statuses.filter(s => !s.error).length;
|
|
148
|
+
const readyToCommit = statuses.filter(s => s.hasChanges).map(s => s.entity);
|
|
149
|
+
this.log(chalk.bold('=== Summary ==='));
|
|
150
|
+
if (dirtyRepos === 0) {
|
|
151
|
+
this.log(`✨ All ${totalRepos} repositories are clean`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
this.log(`✨ ${dirtyRepos} ${dirtyRepos === 1 ? 'repo' : 'repos'} with changes`);
|
|
155
|
+
if (readyToCommit.length > 0) {
|
|
156
|
+
this.log(`🚀 Ready to commit across: ${chalk.cyan(readyToCommit.join(', '))}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Suggested actions
|
|
160
|
+
if (dirtyRepos > 0) {
|
|
161
|
+
this.log('');
|
|
162
|
+
this.log(chalk.dim('Suggested actions:'));
|
|
163
|
+
this.log(chalk.dim('• gut add . # Stage all changes'));
|
|
164
|
+
this.log(chalk.dim('• gut commit -m "..." # Commit across repos'));
|
|
165
|
+
this.log(chalk.dim('• gut push # Push to remotes'));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
displayEntityStatus(status, verbose) {
|
|
169
|
+
const changesText = status.changes.length > 0 ? `${status.changes.length} changes` : '';
|
|
170
|
+
const untrackedText = status.untracked.length > 0 ? `${status.untracked.length} untracked` : '';
|
|
171
|
+
const statusText = [changesText, untrackedText].filter(Boolean).join(', ');
|
|
172
|
+
this.log(` ${status.entity}/ ${chalk.green('🟢')} ${statusText}`);
|
|
173
|
+
if (verbose) {
|
|
174
|
+
// Show specific files
|
|
175
|
+
if (status.changes.length > 0) {
|
|
176
|
+
for (const change of status.changes.slice(0, 3)) {
|
|
177
|
+
this.log(` M ${change}`);
|
|
178
|
+
}
|
|
179
|
+
if (status.changes.length > 3) {
|
|
180
|
+
this.log(` ... and ${status.changes.length - 3} more`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (status.untracked.length > 0) {
|
|
184
|
+
for (const file of status.untracked.slice(0, 2)) {
|
|
185
|
+
this.log(` A ${file}`);
|
|
186
|
+
}
|
|
187
|
+
if (status.untracked.length > 2) {
|
|
188
|
+
this.log(` ... and ${status.untracked.length - 2} more`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
export default class Sync extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
'no-push': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
rebase: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { BaseCommand } from '../base-command.js';
|
|
5
|
+
export default class Sync extends BaseCommand {
|
|
6
|
+
static description = 'Synchronize repositories with remote (fetch, merge/rebase, push)';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> <%= command.id %>',
|
|
9
|
+
'<%= config.bin %> <%= command.id %> --rebase',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --force',
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
force: Flags.boolean({
|
|
14
|
+
char: 'f',
|
|
15
|
+
default: false,
|
|
16
|
+
description: 'Force push after sync',
|
|
17
|
+
}),
|
|
18
|
+
'no-push': Flags.boolean({
|
|
19
|
+
default: false,
|
|
20
|
+
description: 'Skip the push step',
|
|
21
|
+
}),
|
|
22
|
+
rebase: Flags.boolean({
|
|
23
|
+
char: 'r',
|
|
24
|
+
default: false,
|
|
25
|
+
description: 'Use rebase instead of merge',
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
async run() {
|
|
29
|
+
const { flags } = await this.parse(Sync);
|
|
30
|
+
const focusedEntities = await this.focusService.getFocusedEntities();
|
|
31
|
+
if (focusedEntities.length === 0) {
|
|
32
|
+
this.error('No entities are focused. Use "gut focus <entity>" first.');
|
|
33
|
+
}
|
|
34
|
+
this.log(chalk.bold('\n🔄 Synchronizing repositories with remote\n'));
|
|
35
|
+
const results = {
|
|
36
|
+
failed: [],
|
|
37
|
+
partial: [],
|
|
38
|
+
success: [],
|
|
39
|
+
};
|
|
40
|
+
for (const entity of focusedEntities) {
|
|
41
|
+
const spinner = ora(`Syncing ${chalk.cyan(entity.name)}`).start();
|
|
42
|
+
let currentStep = 'fetch';
|
|
43
|
+
try {
|
|
44
|
+
// Check if there's a remote configured
|
|
45
|
+
const remoteOutput = await this.gitService.exec(['remote', '-v'], { cwd: entity.path });
|
|
46
|
+
if (!remoteOutput || remoteOutput.trim().length === 0) {
|
|
47
|
+
spinner.warn(chalk.yellow(`${entity.name}: No remote configured`));
|
|
48
|
+
results.failed.push({ error: 'No remote configured', repo: entity.name, step: 'check' });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Step 1: Fetch
|
|
52
|
+
spinner.text = `${chalk.cyan(entity.name)}: Fetching from remote...`;
|
|
53
|
+
await this.gitService.fetch(entity.path);
|
|
54
|
+
// Step 2: Check status
|
|
55
|
+
currentStep = 'status';
|
|
56
|
+
const status = await this.gitService.getStatus(entity.path);
|
|
57
|
+
const hasUncommitted = status.changes.length > 0;
|
|
58
|
+
if (hasUncommitted) {
|
|
59
|
+
spinner.warn(chalk.yellow(`${entity.name}: Has uncommitted changes (fetch completed)`));
|
|
60
|
+
results.partial.push({ repo: entity.name, step: 'Fetched only - uncommitted changes present' });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// Step 3: Pull (merge or rebase)
|
|
64
|
+
currentStep = 'pull';
|
|
65
|
+
spinner.text = `${chalk.cyan(entity.name)}: ${flags.rebase ? 'Rebasing' : 'Merging'}...`;
|
|
66
|
+
const pullOptions = {
|
|
67
|
+
rebase: flags.rebase,
|
|
68
|
+
};
|
|
69
|
+
await this.gitService.pull(entity.path, pullOptions);
|
|
70
|
+
// Step 4: Push (if not skipped)
|
|
71
|
+
if (!flags['no-push']) {
|
|
72
|
+
currentStep = 'push';
|
|
73
|
+
spinner.text = `${chalk.cyan(entity.name)}: Pushing to remote...`;
|
|
74
|
+
const pushOptions = {
|
|
75
|
+
force: flags.force,
|
|
76
|
+
};
|
|
77
|
+
await this.gitService.push(entity.path, pushOptions);
|
|
78
|
+
}
|
|
79
|
+
spinner.succeed(chalk.green(`✓ ${entity.name}: Fully synchronized`));
|
|
80
|
+
results.success.push(entity.name);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
84
|
+
if (errorMessage.includes('Already up to date') && currentStep === 'pull') {
|
|
85
|
+
// Not an error, just nothing to pull
|
|
86
|
+
if (flags['no-push']) {
|
|
87
|
+
spinner.succeed(chalk.green(`✓ ${entity.name}: Up-to-date`));
|
|
88
|
+
results.success.push(entity.name);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
try {
|
|
92
|
+
currentStep = 'push';
|
|
93
|
+
spinner.text = `${chalk.cyan(entity.name)}: Pushing to remote...`;
|
|
94
|
+
const pushOptions = { force: flags.force };
|
|
95
|
+
await this.gitService.push(entity.path, pushOptions);
|
|
96
|
+
spinner.succeed(chalk.green(`✓ ${entity.name}: Fully synchronized`));
|
|
97
|
+
results.success.push(entity.name);
|
|
98
|
+
}
|
|
99
|
+
catch (pushError) {
|
|
100
|
+
const pushErrorMessage = pushError instanceof Error ? pushError.message : String(pushError);
|
|
101
|
+
spinner.warn(chalk.yellow(`${entity.name}: Up-to-date locally, push failed`));
|
|
102
|
+
results.partial.push({ repo: entity.name, step: `Pull up-to-date, push failed: ${pushErrorMessage}` });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
spinner.fail(chalk.red(`✗ ${entity.name}: Failed at ${currentStep}`));
|
|
108
|
+
results.failed.push({ error: errorMessage, repo: entity.name, step: currentStep });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Summary
|
|
113
|
+
this.log(chalk.bold('\n📊 Sync Summary'));
|
|
114
|
+
this.log(chalk.dim('─'.repeat(50)));
|
|
115
|
+
if (results.success.length > 0) {
|
|
116
|
+
this.log(chalk.green(`✓ Fully synced: ${results.success.length} entities`));
|
|
117
|
+
for (const repo of results.success) {
|
|
118
|
+
this.log(chalk.green(` - ${repo}`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (results.partial.length > 0) {
|
|
122
|
+
this.log(chalk.yellow(`⚠ Partially synced: ${results.partial.length} entities`));
|
|
123
|
+
for (const partial of results.partial) {
|
|
124
|
+
this.log(chalk.yellow(` - ${partial.repo}: ${partial.step}`));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (results.failed.length > 0) {
|
|
128
|
+
this.log(chalk.red(`✗ Failed: ${results.failed.length} entities`));
|
|
129
|
+
for (const failure of results.failed) {
|
|
130
|
+
this.log(chalk.red(` - ${failure.repo}: ${failure.error} (at ${failure.step})`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.log(chalk.dim('\n─'.repeat(50)));
|
|
134
|
+
this.log(chalk.dim('Sync performs: fetch → pull → push'));
|
|
135
|
+
if (results.partial.length > 0 || results.failed.length > 0) {
|
|
136
|
+
this.log(chalk.dim('Tip: Use "gut status" to check repository states'));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class TicketFocus extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
ticketId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
checkout: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
'manifest-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
protected get requiresInit(): boolean;
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
private cloneEntities;
|
|
17
|
+
private runGutAdd;
|
|
18
|
+
private checkoutBranch;
|
|
19
|
+
private formatConfidence;
|
|
20
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { BaseCommand } from '../../base-command.js';
|
|
7
|
+
import { getStatusEmoji } from '../../models/ticket.model.js';
|
|
8
|
+
import { TicketService } from '../../services/ticket.service.js';
|
|
9
|
+
export default class TicketFocus extends BaseCommand {
|
|
10
|
+
static args = {
|
|
11
|
+
ticketId: Args.string({
|
|
12
|
+
description: 'ticket ID to focus on (e.g., PROJ-1234)',
|
|
13
|
+
name: 'ticketId',
|
|
14
|
+
required: true
|
|
15
|
+
})
|
|
16
|
+
};
|
|
17
|
+
static description = 'Focus on a ticket - downloads manifest and clones required entities';
|
|
18
|
+
static examples = [
|
|
19
|
+
'<%= config.bin %> <%= command.id %> PROJ-1234',
|
|
20
|
+
'<%= config.bin %> <%= command.id %> PROJ-1234 --checkout',
|
|
21
|
+
'<%= config.bin %> <%= command.id %> PROJ-1234 --manifest-only',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> PROJ-1234 --output ./my-ticket'
|
|
23
|
+
];
|
|
24
|
+
static flags = {
|
|
25
|
+
checkout: Flags.boolean({
|
|
26
|
+
char: 'c',
|
|
27
|
+
default: true,
|
|
28
|
+
description: 'checkout the ticket branch after cloning'
|
|
29
|
+
}),
|
|
30
|
+
json: Flags.boolean({
|
|
31
|
+
char: 'j',
|
|
32
|
+
description: 'output as JSON'
|
|
33
|
+
}),
|
|
34
|
+
'manifest-only': Flags.boolean({
|
|
35
|
+
char: 'm',
|
|
36
|
+
description: 'only download manifest, do not clone entities'
|
|
37
|
+
}),
|
|
38
|
+
output: Flags.string({
|
|
39
|
+
char: 'o',
|
|
40
|
+
description: 'output directory for manifest (default: focus/)'
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
get requiresInit() {
|
|
44
|
+
return true; // Focus requires gut workspace to be initialized
|
|
45
|
+
}
|
|
46
|
+
async run() {
|
|
47
|
+
const { args, flags } = await this.parse(TicketFocus);
|
|
48
|
+
const ticketService = new TicketService(this.configService);
|
|
49
|
+
if (!ticketService.isConfigured()) {
|
|
50
|
+
this.error('API not configured. Set GUT_API_ENDPOINT and GUT_TENANT_ID environment variables.');
|
|
51
|
+
}
|
|
52
|
+
const spinner = ora('Getting ticket focus context...').start();
|
|
53
|
+
try {
|
|
54
|
+
// Get focus context from API
|
|
55
|
+
const focusResponse = await ticketService.focusTicket(args.ticketId);
|
|
56
|
+
spinner.succeed('Got focus context');
|
|
57
|
+
if (flags.json) {
|
|
58
|
+
this.log(JSON.stringify(focusResponse, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Print ticket summary
|
|
62
|
+
this.log('');
|
|
63
|
+
this.log(chalk.bold(`🎯 Focusing on: ${focusResponse.ticketId}`));
|
|
64
|
+
this.log(` ${getStatusEmoji(focusResponse.status)} Status: ${focusResponse.status}`);
|
|
65
|
+
this.log(` 📍 Phase: ${focusResponse.phase}`);
|
|
66
|
+
this.log(` 📊 Confidence: ${this.formatConfidence(focusResponse.confidence)}`);
|
|
67
|
+
this.log(` 🌿 Branch: ${focusResponse.branch}`);
|
|
68
|
+
this.log('');
|
|
69
|
+
// Determine output directory
|
|
70
|
+
const outputDir = flags.output || this.configService.getFocusDir();
|
|
71
|
+
// Download manifest
|
|
72
|
+
const manifestPath = path.join(outputDir, `${args.ticketId}.yaml`);
|
|
73
|
+
spinner.start('Downloading manifest...');
|
|
74
|
+
await ticketService.downloadManifest(focusResponse.manifestUrl, manifestPath);
|
|
75
|
+
spinner.succeed(`Manifest saved to ${manifestPath}`);
|
|
76
|
+
// Print entities
|
|
77
|
+
if (focusResponse.entities.length > 0) {
|
|
78
|
+
this.log('');
|
|
79
|
+
this.log(chalk.bold('📦 Required Entities:'));
|
|
80
|
+
for (const entity of focusResponse.entities) {
|
|
81
|
+
this.log(` • ${entity}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Print acceptance criteria
|
|
85
|
+
if (focusResponse.acceptanceCriteria && focusResponse.acceptanceCriteria.length > 0) {
|
|
86
|
+
this.log('');
|
|
87
|
+
this.log(chalk.bold('✅ Acceptance Criteria:'));
|
|
88
|
+
for (const criterion of focusResponse.acceptanceCriteria) {
|
|
89
|
+
this.log(` ☐ ${criterion}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Clone entities unless manifest-only
|
|
93
|
+
if (!flags['manifest-only'] && focusResponse.entities.length > 0) {
|
|
94
|
+
this.log('');
|
|
95
|
+
await this.cloneEntities(focusResponse.entities, spinner);
|
|
96
|
+
// Checkout branch
|
|
97
|
+
if (flags.checkout && focusResponse.branch) {
|
|
98
|
+
this.log('');
|
|
99
|
+
await this.checkoutBranch(focusResponse.entities, focusResponse.branch, spinner);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Update gut focus to the entities
|
|
103
|
+
if (focusResponse.entities.length > 0) {
|
|
104
|
+
const entityNames = focusResponse.entities.map(e => {
|
|
105
|
+
// Extract entity name from @type/name format
|
|
106
|
+
const match = e.match(/@[^/]+\/(.+)/);
|
|
107
|
+
return match ? match[1] : e;
|
|
108
|
+
});
|
|
109
|
+
await this.focusService.setFocus(entityNames, {
|
|
110
|
+
mode: 'delivery'
|
|
111
|
+
});
|
|
112
|
+
this.log('');
|
|
113
|
+
this.log(chalk.green(`✓ Focus set to ${entityNames.length} entities`));
|
|
114
|
+
}
|
|
115
|
+
// Final summary
|
|
116
|
+
this.log('');
|
|
117
|
+
this.log(chalk.dim('─'.repeat(50)));
|
|
118
|
+
this.log(chalk.bold('Next steps:'));
|
|
119
|
+
this.log(` 1. Review the manifest: ${chalk.cyan(`cat ${manifestPath}`)}`);
|
|
120
|
+
this.log(` 2. Check focused entities: ${chalk.cyan('gut status')}`);
|
|
121
|
+
this.log(` 3. Start working on the ticket`);
|
|
122
|
+
if (focusResponse.status === 'blocked' || focusResponse.status === 'needs_clarity') {
|
|
123
|
+
this.log(` 4. Add hints if needed: ${chalk.cyan(`gut ticket hint ${args.ticketId} "your hint"`)}`);
|
|
124
|
+
}
|
|
125
|
+
this.log('');
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
spinner.fail('Failed');
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
+
this.error(`Failed to focus on ticket: ${message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async cloneEntities(entities, spinner) {
|
|
134
|
+
spinner.start(`Cloning ${entities.length} entities...`);
|
|
135
|
+
for (const entity of entities) {
|
|
136
|
+
// Extract entity name for gut add
|
|
137
|
+
const match = entity.match(/@([^/]+)\/(.+)/);
|
|
138
|
+
if (!match) {
|
|
139
|
+
this.log(chalk.yellow(` ⚠️ Could not parse entity: ${entity}`));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const [, entityType, entityName] = match;
|
|
143
|
+
// Check if entity already exists in config
|
|
144
|
+
const config = this.configService.getConfig();
|
|
145
|
+
const existingEntity = config.entities.find(e => e.name === entityName || e.name === entity);
|
|
146
|
+
if (existingEntity) {
|
|
147
|
+
spinner.info(` ${entity} already configured`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// Use gut add to clone the entity
|
|
151
|
+
try {
|
|
152
|
+
await this.runGutAdd(entityType, entityName);
|
|
153
|
+
spinner.succeed(` ${entity} cloned`);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
157
|
+
this.log(chalk.yellow(` ⚠️ Could not clone ${entity}: ${msg}`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
spinner.succeed('Entities ready');
|
|
161
|
+
}
|
|
162
|
+
async runGutAdd(entityType, entityName) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const child = spawn('gut', ['add', entityType, entityName], {
|
|
165
|
+
stdio: 'pipe'
|
|
166
|
+
});
|
|
167
|
+
let stderr = '';
|
|
168
|
+
child.stderr?.on('data', (data) => {
|
|
169
|
+
stderr += data.toString();
|
|
170
|
+
});
|
|
171
|
+
child.on('close', (code) => {
|
|
172
|
+
if (code === 0) {
|
|
173
|
+
resolve();
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
reject(new Error(stderr || `Exit code ${code}`));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
child.on('error', reject);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async checkoutBranch(entities, branch, spinner) {
|
|
183
|
+
spinner.start(`Checking out branch: ${branch}`);
|
|
184
|
+
const focusedEntities = await this.focusService.getFocusedEntities();
|
|
185
|
+
for (const entity of focusedEntities) {
|
|
186
|
+
try {
|
|
187
|
+
// Check if branch exists, create if not
|
|
188
|
+
await this.gitService.checkout(entity.path, branch);
|
|
189
|
+
spinner.succeed(` ${entity.name}: checked out ${branch}`);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
// Branch might not exist yet - create it
|
|
193
|
+
try {
|
|
194
|
+
await this.gitService.createBranch(entity.path, branch);
|
|
195
|
+
spinner.succeed(` ${entity.name}: created and checked out ${branch}`);
|
|
196
|
+
}
|
|
197
|
+
catch (createError) {
|
|
198
|
+
const msg = createError instanceof Error ? createError.message : String(createError);
|
|
199
|
+
this.log(chalk.yellow(` ⚠️ ${entity.name}: ${msg}`));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
spinner.succeed(`Branch ${branch} ready`);
|
|
204
|
+
}
|
|
205
|
+
formatConfidence(confidence) {
|
|
206
|
+
if (confidence >= 90) {
|
|
207
|
+
return chalk.green(`${confidence}%`);
|
|
208
|
+
}
|
|
209
|
+
if (confidence >= 70) {
|
|
210
|
+
return chalk.yellow(`${confidence}%`);
|
|
211
|
+
}
|
|
212
|
+
if (confidence >= 50) {
|
|
213
|
+
return chalk.hex('#FFA500')(`${confidence}%`);
|
|
214
|
+
}
|
|
215
|
+
return chalk.red(`${confidence}%`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class TicketGet extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
ticketId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
protected get requiresInit(): boolean;
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
private printTicketDetails;
|
|
14
|
+
private formatConfidence;
|
|
15
|
+
}
|