@hyperdrive.bot/gut 0.1.6 → 0.1.9

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 (111) hide show
  1. package/README.md +1 -1
  2. package/dist/base-command.d.ts +22 -0
  3. package/dist/base-command.js +99 -0
  4. package/dist/commands/add.d.ts +14 -0
  5. package/dist/commands/add.js +70 -0
  6. package/dist/commands/affected.d.ts +23 -0
  7. package/dist/commands/affected.js +323 -0
  8. package/dist/commands/audit.d.ts +33 -0
  9. package/dist/commands/audit.js +594 -0
  10. package/dist/commands/back.d.ts +6 -0
  11. package/dist/commands/back.js +29 -0
  12. package/dist/commands/checkout.d.ts +14 -0
  13. package/dist/commands/checkout.js +124 -0
  14. package/dist/commands/commit.d.ts +11 -0
  15. package/dist/commands/commit.js +107 -0
  16. package/dist/commands/context.d.ts +6 -0
  17. package/dist/commands/context.js +32 -0
  18. package/dist/commands/contexts.d.ts +7 -0
  19. package/dist/commands/contexts.js +88 -0
  20. package/dist/commands/deps.d.ts +10 -0
  21. package/dist/commands/deps.js +100 -0
  22. package/dist/commands/entity/add.d.ts +16 -0
  23. package/dist/commands/entity/add.js +103 -0
  24. package/dist/commands/entity/clone-all.d.ts +17 -0
  25. package/dist/commands/entity/clone-all.js +127 -0
  26. package/dist/commands/entity/clone.d.ts +15 -0
  27. package/dist/commands/entity/clone.js +106 -0
  28. package/dist/commands/entity/list.d.ts +11 -0
  29. package/dist/commands/entity/list.js +80 -0
  30. package/dist/commands/entity/remove.d.ts +12 -0
  31. package/dist/commands/entity/remove.js +54 -0
  32. package/dist/commands/extract.d.ts +35 -0
  33. package/dist/commands/extract.js +483 -0
  34. package/dist/commands/focus.d.ts +19 -0
  35. package/dist/commands/focus.js +137 -0
  36. package/dist/commands/graph.d.ts +18 -0
  37. package/dist/commands/graph.js +273 -0
  38. package/dist/commands/init.d.ts +11 -0
  39. package/dist/commands/init.js +75 -0
  40. package/dist/commands/insights.d.ts +21 -0
  41. package/dist/commands/insights.js +465 -0
  42. package/dist/commands/patterns.d.ts +40 -0
  43. package/dist/commands/patterns.js +405 -0
  44. package/dist/commands/pull.d.ts +11 -0
  45. package/dist/commands/pull.js +121 -0
  46. package/dist/commands/push.d.ts +11 -0
  47. package/dist/commands/push.js +97 -0
  48. package/dist/commands/quick-setup.d.ts +20 -0
  49. package/dist/commands/quick-setup.js +417 -0
  50. package/dist/commands/recent.d.ts +9 -0
  51. package/dist/commands/recent.js +51 -0
  52. package/dist/commands/related.d.ts +23 -0
  53. package/dist/commands/related.js +255 -0
  54. package/dist/commands/repos.d.ts +17 -0
  55. package/dist/commands/repos.js +184 -0
  56. package/dist/commands/stack.d.ts +10 -0
  57. package/dist/commands/stack.js +78 -0
  58. package/dist/commands/status.d.ts +13 -0
  59. package/dist/commands/status.js +193 -0
  60. package/dist/commands/sync.d.ts +11 -0
  61. package/dist/commands/sync.js +139 -0
  62. package/dist/commands/ticket/focus.d.ts +20 -0
  63. package/dist/commands/ticket/focus.js +217 -0
  64. package/dist/commands/ticket/get.d.ts +15 -0
  65. package/dist/commands/ticket/get.js +168 -0
  66. package/dist/commands/ticket/hint.d.ts +16 -0
  67. package/dist/commands/ticket/hint.js +147 -0
  68. package/dist/commands/ticket/index.d.ts +10 -0
  69. package/dist/commands/ticket/index.js +60 -0
  70. package/dist/commands/ticket/list.d.ts +13 -0
  71. package/dist/commands/ticket/list.js +120 -0
  72. package/dist/commands/ticket/sync.d.ts +14 -0
  73. package/dist/commands/ticket/sync.js +85 -0
  74. package/dist/commands/ticket/update.d.ts +17 -0
  75. package/dist/commands/ticket/update.js +142 -0
  76. package/dist/commands/unfocus.d.ts +6 -0
  77. package/dist/commands/unfocus.js +19 -0
  78. package/dist/commands/used-by.d.ts +13 -0
  79. package/dist/commands/used-by.js +110 -0
  80. package/dist/commands/workspace.d.ts +22 -0
  81. package/dist/commands/workspace.js +372 -0
  82. package/dist/index.d.ts +14 -0
  83. package/dist/index.js +16 -0
  84. package/dist/models/entity.model.d.ts +234 -0
  85. package/dist/models/entity.model.js +1 -0
  86. package/dist/models/ticket.model.d.ts +117 -0
  87. package/dist/models/ticket.model.js +43 -0
  88. package/dist/services/auth.service.d.ts +15 -0
  89. package/dist/services/auth.service.js +26 -0
  90. package/dist/services/config.service.d.ts +34 -0
  91. package/dist/services/config.service.js +234 -0
  92. package/dist/services/entity.service.d.ts +20 -0
  93. package/dist/services/entity.service.js +127 -0
  94. package/dist/services/focus.service.d.ts +71 -0
  95. package/dist/services/focus.service.js +614 -0
  96. package/dist/services/git.service.d.ts +39 -0
  97. package/dist/services/git.service.js +188 -0
  98. package/dist/services/gut-api.service.d.ts +53 -0
  99. package/dist/services/gut-api.service.js +99 -0
  100. package/dist/services/ticket.service.d.ts +84 -0
  101. package/dist/services/ticket.service.js +207 -0
  102. package/dist/utils/display.d.ts +26 -0
  103. package/dist/utils/display.js +145 -0
  104. package/dist/utils/filesystem.d.ts +32 -0
  105. package/dist/utils/filesystem.js +198 -0
  106. package/dist/utils/index.d.ts +13 -0
  107. package/dist/utils/index.js +14 -0
  108. package/dist/utils/validation.d.ts +22 -0
  109. package/dist/utils/validation.js +192 -0
  110. package/oclif.manifest.json +2008 -0
  111. 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
+ }