@hyperdrive.bot/gut 0.1.10 → 0.1.11
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 +3 -3
- package/dist/base-command.d.ts +2 -0
- package/dist/base-command.js +3 -0
- package/dist/commands/auth/login.d.ts +10 -0
- package/dist/commands/auth/login.js +103 -0
- package/dist/commands/auth/logout.d.ts +6 -0
- package/dist/commands/auth/logout.js +39 -0
- package/dist/commands/auth/status.d.ts +9 -0
- package/dist/commands/auth/status.js +87 -0
- package/dist/commands/ticket/config.d.ts +13 -0
- package/dist/commands/ticket/config.js +22 -0
- package/dist/commands/ticket/sync.d.ts +1 -0
- package/dist/commands/ticket/sync.js +62 -8
- package/dist/commands/worktree/create.d.ts +15 -0
- package/dist/commands/worktree/create.js +140 -0
- package/dist/models/entity.model.d.ts +16 -0
- package/dist/models/ticket.model.d.ts +2 -0
- package/dist/services/git.service.d.ts +11 -0
- package/dist/services/git.service.js +57 -0
- package/dist/services/git.service.test.d.ts +1 -0
- package/dist/services/git.service.test.js +101 -0
- package/dist/services/gut-api.service.d.ts +20 -1
- package/dist/services/gut-api.service.js +30 -2
- package/dist/services/tenant.service.d.ts +14 -0
- package/dist/services/tenant.service.js +24 -0
- package/dist/services/ticket.service.d.ts +3 -1
- package/dist/services/ticket.service.js +7 -3
- package/dist/services/worktree.service.d.ts +16 -0
- package/dist/services/worktree.service.js +60 -0
- package/oclif.manifest.json +229 -4
- package/package.json +16 -5
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.11 linux-x64 node-v22.22.0
|
|
22
22
|
$ gut --help [COMMAND]
|
|
23
23
|
USAGE
|
|
24
24
|
$ gut COMMAND
|
|
@@ -310,7 +310,7 @@ ARGUMENTS
|
|
|
310
310
|
NAME Name of the entity to clone
|
|
311
311
|
|
|
312
312
|
FLAGS
|
|
313
|
-
-b, --branch=<value>
|
|
313
|
+
-b, --branch=<value> Branch to clone (auto-detects workspace branch, falls back to master)
|
|
314
314
|
-d, --depth=<value> Create a shallow clone with specified depth
|
|
315
315
|
-f, --force Force clone even if directory exists
|
|
316
316
|
-p, --path=<value> Custom path to clone to (relative to workspace)
|
|
@@ -335,7 +335,7 @@ USAGE
|
|
|
335
335
|
$ gut entity clone-all [-b <value>] [-d <value>] [-f] [-p] [--skip-existing]
|
|
336
336
|
|
|
337
337
|
FLAGS
|
|
338
|
-
-b, --branch=<value>
|
|
338
|
+
-b, --branch=<value> Branch to clone for all entities (auto-detects workspace branch, falls back to master)
|
|
339
339
|
-d, --depth=<value> Create shallow clones with specified depth
|
|
340
340
|
-f, --force Force clone even if directories exist
|
|
341
341
|
-p, --parallel Clone entities in parallel
|
package/dist/base-command.d.ts
CHANGED
|
@@ -5,12 +5,14 @@ import { EntityService } from './services/entity.service.js';
|
|
|
5
5
|
import { FocusService } from './services/focus.service.js';
|
|
6
6
|
import { GitService } from './services/git.service.js';
|
|
7
7
|
import { TicketService } from './services/ticket.service.js';
|
|
8
|
+
import { WorktreeService } from './services/worktree.service.js';
|
|
8
9
|
export declare abstract class BaseCommand extends Command {
|
|
9
10
|
protected configService: ConfigService;
|
|
10
11
|
protected entityService: EntityService;
|
|
11
12
|
protected focusService: FocusService;
|
|
12
13
|
protected gitService: GitService;
|
|
13
14
|
protected ticketService: TicketService;
|
|
15
|
+
protected worktreeService: WorktreeService;
|
|
14
16
|
protected get requiresInit(): boolean;
|
|
15
17
|
protected formatPath(path: string): string;
|
|
16
18
|
protected getStatusIcon(hasChanges: boolean): string;
|
package/dist/base-command.js
CHANGED
|
@@ -4,6 +4,7 @@ import { EntityService } from './services/entity.service.js';
|
|
|
4
4
|
import { FocusService } from './services/focus.service.js';
|
|
5
5
|
import { GitService } from './services/git.service.js';
|
|
6
6
|
import { TicketService } from './services/ticket.service.js';
|
|
7
|
+
import { WorktreeService } from './services/worktree.service.js';
|
|
7
8
|
const ENTITY_TYPE_EMOJI = {
|
|
8
9
|
client: '🏢',
|
|
9
10
|
company: '🏛️',
|
|
@@ -22,6 +23,7 @@ export class BaseCommand extends Command {
|
|
|
22
23
|
focusService;
|
|
23
24
|
gitService;
|
|
24
25
|
ticketService;
|
|
26
|
+
worktreeService;
|
|
25
27
|
get requiresInit() {
|
|
26
28
|
// Override in commands that don't require initialization
|
|
27
29
|
return true;
|
|
@@ -47,6 +49,7 @@ export class BaseCommand extends Command {
|
|
|
47
49
|
this.focusService = new FocusService(this.configService);
|
|
48
50
|
this.gitService = new GitService();
|
|
49
51
|
this.ticketService = new TicketService(this.configService);
|
|
52
|
+
this.worktreeService = new WorktreeService(this.configService, this.gitService, this.focusService);
|
|
50
53
|
// Check workspace initialization for commands that require it
|
|
51
54
|
if (this.requiresInit && !this.configService.isInitialized()) {
|
|
52
55
|
this.error('Workspace not initialized. Run "gut init" first.');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Login extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
port: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { buildAuthUrl, buildStoredCredentials, exchangeCodeForTokens, generateCodeChallenge, generateCodeVerifier, getAWSCredentialsFromIdentityPool, getCredentialsPath, saveCredentials, startCallbackServer, } from '@hyperdrive.bot/cli-auth';
|
|
7
|
+
import { createTenantService } from '../../services/tenant.service.js';
|
|
8
|
+
export default class Login extends Command {
|
|
9
|
+
static description = 'Authenticate with Gut using OAuth 2.0 PKCE flow';
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> <%= command.id %>',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --domain acme.hyperdrive.bot',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --port 9876',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
domain: Flags.string({
|
|
17
|
+
char: 'd',
|
|
18
|
+
description: 'Tenant domain (e.g., acme.hyperdrive.bot)',
|
|
19
|
+
}),
|
|
20
|
+
port: Flags.integer({
|
|
21
|
+
char: 'p',
|
|
22
|
+
default: 8766, // Different from hyperdrive's 8765 to avoid conflicts
|
|
23
|
+
description: 'Local callback server port',
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
async run() {
|
|
27
|
+
const { flags } = await this.parse(Login);
|
|
28
|
+
const tenantService = createTenantService(flags.domain);
|
|
29
|
+
this.log(chalk.blue('🚀 Starting Gut authentication...'));
|
|
30
|
+
this.log('');
|
|
31
|
+
// Step 1: Resolve tenant configuration from bootstrap
|
|
32
|
+
let tenantConfig;
|
|
33
|
+
const spinner = ora('Fetching tenant configuration...').start();
|
|
34
|
+
try {
|
|
35
|
+
let tenantDomain = flags.domain;
|
|
36
|
+
if (!tenantDomain) {
|
|
37
|
+
spinner.stop();
|
|
38
|
+
const savedTenantDomain = tenantService.getTenantDomain();
|
|
39
|
+
const answers = await inquirer.prompt([
|
|
40
|
+
{
|
|
41
|
+
default: savedTenantDomain || undefined,
|
|
42
|
+
message: 'Enter your tenant domain:',
|
|
43
|
+
name: 'tenant',
|
|
44
|
+
type: 'input',
|
|
45
|
+
validate: (input) => input.trim().length > 0 || 'Tenant domain is required',
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
tenantDomain = answers.tenant;
|
|
49
|
+
spinner.start('Fetching tenant configuration...');
|
|
50
|
+
}
|
|
51
|
+
tenantConfig = await tenantService.fetchTenantConfig(tenantDomain);
|
|
52
|
+
spinner.succeed(chalk.green(`Tenant found: ${tenantConfig.displayName}`));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
spinner.fail(chalk.red('Failed to fetch tenant configuration'));
|
|
56
|
+
this.log('');
|
|
57
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
58
|
+
this.error(errorMessage + '\n\n' +
|
|
59
|
+
chalk.yellow('💡 Tips:\n') +
|
|
60
|
+
chalk.gray(' - Check your tenant domain spelling\n') +
|
|
61
|
+
chalk.gray(' - Set GUT_TENANT_DOMAIN environment variable\n'));
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
// Generate PKCE parameters
|
|
65
|
+
const codeVerifier = generateCodeVerifier();
|
|
66
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
67
|
+
// Start local callback server
|
|
68
|
+
const authCodePromise = startCallbackServer(flags.port, 300000, 'Gut');
|
|
69
|
+
// Open browser for authentication
|
|
70
|
+
const authUrl = buildAuthUrl(tenantConfig, codeChallenge, flags.port);
|
|
71
|
+
this.log(chalk.yellow('📱 Opening browser for authentication...'));
|
|
72
|
+
this.log(chalk.gray(`If browser doesn't open, visit: ${authUrl}`));
|
|
73
|
+
this.log('');
|
|
74
|
+
await open(authUrl);
|
|
75
|
+
spinner.start('Waiting for authentication...');
|
|
76
|
+
// Wait for callback with auth code
|
|
77
|
+
const code = await authCodePromise;
|
|
78
|
+
spinner.succeed(chalk.green('Authentication callback received!'));
|
|
79
|
+
// Exchange code for tokens
|
|
80
|
+
spinner.start('Exchanging authorization code for tokens...');
|
|
81
|
+
const tokens = await exchangeCodeForTokens(tenantConfig, code, codeVerifier, flags.port);
|
|
82
|
+
spinner.succeed(chalk.green('Tokens obtained successfully!'));
|
|
83
|
+
// Get AWS credentials from Cognito Identity Pool
|
|
84
|
+
spinner.start('Obtaining AWS credentials...');
|
|
85
|
+
const awsCredentials = await getAWSCredentialsFromIdentityPool(tenantConfig, tokens.id_token);
|
|
86
|
+
spinner.succeed(chalk.green('AWS credentials obtained!'));
|
|
87
|
+
// Save credentials
|
|
88
|
+
spinner.start('Saving credentials...');
|
|
89
|
+
const storedCredentials = buildStoredCredentials(tokens, awsCredentials, tenantConfig);
|
|
90
|
+
saveCredentials(storedCredentials, tenantConfig.tenantDomain, 'gut');
|
|
91
|
+
spinner.succeed(chalk.green('Credentials saved!'));
|
|
92
|
+
this.log('');
|
|
93
|
+
this.log(chalk.green('✅ Successfully authenticated!'));
|
|
94
|
+
this.log(chalk.gray('You can now use all Gut CLI commands.'));
|
|
95
|
+
this.log('');
|
|
96
|
+
this.log(chalk.gray(`✓ Credentials saved to ${getCredentialsPath(tenantConfig.tenantDomain, 'gut')}`));
|
|
97
|
+
this.log(chalk.gray('💡 Credentials refresh automatically when needed.'));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
this.error(chalk.red(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { AuthService } from '../../services/auth.service.js';
|
|
5
|
+
export default class Logout extends Command {
|
|
6
|
+
static description = 'Remove stored credentials and logout';
|
|
7
|
+
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
8
|
+
async run() {
|
|
9
|
+
const authService = new AuthService();
|
|
10
|
+
// Check if credentials exist
|
|
11
|
+
const credentials = authService.loadCredentials();
|
|
12
|
+
if (!credentials) {
|
|
13
|
+
this.log(chalk.yellow('⚠️ No active session found'));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
// Confirm logout
|
|
17
|
+
const { confirm } = await inquirer.prompt([
|
|
18
|
+
{
|
|
19
|
+
default: false,
|
|
20
|
+
message: 'Are you sure you want to logout?',
|
|
21
|
+
name: 'confirm',
|
|
22
|
+
type: 'confirm',
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
if (!confirm) {
|
|
26
|
+
this.log(chalk.gray('Logout cancelled'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
authService.clearCredentials();
|
|
31
|
+
this.log('');
|
|
32
|
+
this.log(chalk.green('✅ Successfully logged out!'));
|
|
33
|
+
this.log(chalk.gray('Run `gut auth login` to authenticate again.'));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
this.error(chalk.red(`Logout failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { AuthService } from '../../services/auth.service.js';
|
|
4
|
+
export default class Status extends Command {
|
|
5
|
+
static description = 'Show current authentication status';
|
|
6
|
+
static examples = [
|
|
7
|
+
'<%= config.bin %> <%= command.id %>',
|
|
8
|
+
'<%= config.bin %> <%= command.id %> --verbose',
|
|
9
|
+
];
|
|
10
|
+
static flags = {
|
|
11
|
+
verbose: Flags.boolean({
|
|
12
|
+
char: 'v',
|
|
13
|
+
default: false,
|
|
14
|
+
description: 'Show detailed credential information',
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { flags } = await this.parse(Status);
|
|
19
|
+
const authService = new AuthService();
|
|
20
|
+
const credentials = authService.loadCredentials();
|
|
21
|
+
if (!credentials) {
|
|
22
|
+
this.log(chalk.yellow('⚠️ Not authenticated'));
|
|
23
|
+
this.log('');
|
|
24
|
+
this.log(chalk.gray('Run `gut auth login` to authenticate.'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const isExpired = authService.isExpired(credentials);
|
|
28
|
+
const needsRefresh = authService.needsRefresh(credentials);
|
|
29
|
+
this.log(chalk.blue('🔐 Authentication Status'));
|
|
30
|
+
this.log('');
|
|
31
|
+
// Status
|
|
32
|
+
if (isExpired) {
|
|
33
|
+
this.log(chalk.red('Status: ✗ Expired'));
|
|
34
|
+
}
|
|
35
|
+
else if (needsRefresh) {
|
|
36
|
+
this.log(chalk.yellow('Status: ⚠ Expiring soon (will auto-refresh)'));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
this.log(chalk.green('Status: ✓ Authenticated'));
|
|
40
|
+
}
|
|
41
|
+
this.log('');
|
|
42
|
+
// Basic info
|
|
43
|
+
this.log(chalk.white('Tenant:'), chalk.cyan(credentials.tenantDomain));
|
|
44
|
+
this.log(chalk.white('Tenant ID:'), chalk.gray(credentials.tenantId));
|
|
45
|
+
this.log(chalk.white('Region:'), chalk.gray(credentials.region));
|
|
46
|
+
// Expiration
|
|
47
|
+
const expiration = new Date(credentials.awsCredentials.expiration);
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const timeRemaining = expiration.getTime() - now.getTime();
|
|
50
|
+
if (timeRemaining > 0) {
|
|
51
|
+
const minutes = Math.floor(timeRemaining / (1000 * 60));
|
|
52
|
+
const hours = Math.floor(minutes / 60);
|
|
53
|
+
const remainingMinutes = minutes % 60;
|
|
54
|
+
if (hours > 0) {
|
|
55
|
+
this.log(chalk.white('Expires in:'), chalk.gray(`${hours}h ${remainingMinutes}m`));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.log(chalk.white('Expires in:'), chalk.gray(`${remainingMinutes}m`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.log(chalk.white('Expired:'), chalk.red('Yes'));
|
|
63
|
+
}
|
|
64
|
+
if (flags.verbose) {
|
|
65
|
+
this.log('');
|
|
66
|
+
this.log(chalk.blue('📋 Detailed Information'));
|
|
67
|
+
this.log('');
|
|
68
|
+
this.log(chalk.white('API URL:'), chalk.gray(credentials.apiUrl));
|
|
69
|
+
this.log(chalk.white('Obtained at:'), chalk.gray(credentials.obtainedAt));
|
|
70
|
+
this.log(chalk.white('Expiration:'), chalk.gray(expiration.toISOString()));
|
|
71
|
+
if (credentials.cognitoConfig) {
|
|
72
|
+
this.log('');
|
|
73
|
+
this.log(chalk.white('Cognito Client ID:'), chalk.gray(credentials.cognitoConfig.clientId));
|
|
74
|
+
this.log(chalk.white('User Pool ID:'), chalk.gray(credentials.cognitoConfig.userPoolId));
|
|
75
|
+
}
|
|
76
|
+
this.log('');
|
|
77
|
+
this.log(chalk.white('Credentials file:'), chalk.gray(authService.getCredentialsDir()));
|
|
78
|
+
}
|
|
79
|
+
this.log('');
|
|
80
|
+
if (isExpired) {
|
|
81
|
+
this.log(chalk.yellow('💡 Run `gut auth login` to re-authenticate.'));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.log(chalk.gray('💡 Credentials refresh automatically when needed.'));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class TicketConfig extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'secret-arn': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
show: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base-command.js';
|
|
3
|
+
export default class TicketConfig extends BaseCommand {
|
|
4
|
+
static description = 'Configure ticket source (JIRA, GitHub, etc.) for this tenant';
|
|
5
|
+
static examples = [
|
|
6
|
+
'<%= config.bin %> ticket config --show',
|
|
7
|
+
'<%= config.bin %> ticket config --type jira --url https://company.atlassian.net --secret-arn arn:aws:secretsmanager:...',
|
|
8
|
+
];
|
|
9
|
+
static flags = {
|
|
10
|
+
'secret-arn': Flags.string({ description: 'AWS Secrets Manager ARN for credentials' }),
|
|
11
|
+
json: Flags.boolean({ char: 'j', description: 'output as JSON' }),
|
|
12
|
+
show: Flags.boolean({ description: 'show current configuration' }),
|
|
13
|
+
type: Flags.string({
|
|
14
|
+
description: 'source type',
|
|
15
|
+
options: ['jira', 'github', 'linear'],
|
|
16
|
+
}),
|
|
17
|
+
url: Flags.string({ description: 'base URL (e.g., https://company.atlassian.net)' }),
|
|
18
|
+
};
|
|
19
|
+
async run() {
|
|
20
|
+
this.error('ticket config is not yet implemented — API service pending');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -8,6 +8,7 @@ export default class TicketSync extends BaseCommand {
|
|
|
8
8
|
static flags: {
|
|
9
9
|
direction: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
'no-enrich': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
};
|
|
12
13
|
protected get requiresInit(): boolean;
|
|
13
14
|
run(): Promise<void>;
|
|
@@ -6,16 +6,24 @@ import { TicketService } from '../../services/ticket.service.js';
|
|
|
6
6
|
export default class TicketSync extends BaseCommand {
|
|
7
7
|
static args = {
|
|
8
8
|
ticketId: Args.string({
|
|
9
|
-
description: 'ticket ID to sync',
|
|
9
|
+
description: 'ticket ID to sync (e.g., PROJ-1234)',
|
|
10
10
|
name: 'ticketId',
|
|
11
11
|
required: true
|
|
12
12
|
})
|
|
13
13
|
};
|
|
14
|
-
static description =
|
|
14
|
+
static description = `Sync ticket state with external source (JIRA, GitHub, etc.)
|
|
15
|
+
|
|
16
|
+
For pull direction:
|
|
17
|
+
- If ticket exists in gut: updates from source
|
|
18
|
+
- If ticket doesn't exist: creates it and queues enrichment
|
|
19
|
+
|
|
20
|
+
For push direction:
|
|
21
|
+
- Updates source system with gut ticket state`;
|
|
15
22
|
static examples = [
|
|
16
23
|
'<%= config.bin %> <%= command.id %> PROJ-1234',
|
|
17
24
|
'<%= config.bin %> <%= command.id %> PROJ-1234 --direction push',
|
|
18
|
-
'<%= config.bin %> <%= command.id %> PROJ-1234 --direction pull'
|
|
25
|
+
'<%= config.bin %> <%= command.id %> PROJ-1234 --direction pull',
|
|
26
|
+
'<%= config.bin %> <%= command.id %> PROJ-1234 --direction pull --no-enrich'
|
|
19
27
|
];
|
|
20
28
|
static flags = {
|
|
21
29
|
direction: Flags.string({
|
|
@@ -27,6 +35,10 @@ export default class TicketSync extends BaseCommand {
|
|
|
27
35
|
json: Flags.boolean({
|
|
28
36
|
char: 'j',
|
|
29
37
|
description: 'output as JSON'
|
|
38
|
+
}),
|
|
39
|
+
'no-enrich': Flags.boolean({
|
|
40
|
+
default: false,
|
|
41
|
+
description: 'skip enrichment queue when pulling new tickets'
|
|
30
42
|
})
|
|
31
43
|
};
|
|
32
44
|
get requiresInit() {
|
|
@@ -39,34 +51,60 @@ export default class TicketSync extends BaseCommand {
|
|
|
39
51
|
this.error('API not configured. Set GUT_API_ENDPOINT and GUT_TENANT_ID environment variables.');
|
|
40
52
|
}
|
|
41
53
|
const direction = flags.direction;
|
|
54
|
+
const noEnrich = flags['no-enrich'];
|
|
42
55
|
const directionEmoji = direction === 'push' ? '⬆️' : '⬇️';
|
|
43
56
|
const directionText = direction === 'push'
|
|
44
57
|
? 'gut → source'
|
|
45
58
|
: 'source → gut';
|
|
46
59
|
const spinner = ora(`${directionEmoji} Syncing ticket (${directionText})...`).start();
|
|
47
60
|
try {
|
|
48
|
-
const response = await ticketService.syncTicket(args.ticketId, direction);
|
|
61
|
+
const response = await ticketService.syncTicket(args.ticketId, direction, { noEnrich });
|
|
49
62
|
if (flags.json) {
|
|
50
63
|
spinner.stop();
|
|
51
64
|
this.log(JSON.stringify(response, null, 2));
|
|
52
65
|
return;
|
|
53
66
|
}
|
|
54
|
-
|
|
67
|
+
// Different success messages based on what happened
|
|
68
|
+
if (response.created) {
|
|
69
|
+
spinner.succeed(chalk.green('Created new ticket from source'));
|
|
70
|
+
}
|
|
71
|
+
else if (response.synced) {
|
|
55
72
|
spinner.succeed('Sync completed');
|
|
56
73
|
}
|
|
57
74
|
else {
|
|
58
75
|
spinner.warn('Sync completed with warnings');
|
|
59
76
|
}
|
|
60
77
|
this.log('');
|
|
61
|
-
|
|
78
|
+
// Show ticket info with create/update indicator
|
|
79
|
+
const statusIcon = response.created ? chalk.green('🆕') : chalk.blue('🔄');
|
|
80
|
+
this.log(chalk.bold(`${statusIcon} Ticket: ${response.ticketId}`));
|
|
62
81
|
this.log(` 🔗 Source: ${response.source.type}`);
|
|
63
82
|
this.log(` 🌐 URL: ${response.source.externalUrl}`);
|
|
83
|
+
// Show enrichment status for pull operations
|
|
84
|
+
if (direction === 'pull') {
|
|
85
|
+
if (response.enrichmentQueued) {
|
|
86
|
+
this.log(` 🔍 Enrichment: ${chalk.cyan('Queued')}`);
|
|
87
|
+
}
|
|
88
|
+
else if (response.created && noEnrich) {
|
|
89
|
+
this.log(` 🔍 Enrichment: ${chalk.yellow('Skipped (--no-enrich)')}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
64
92
|
if (response.actions.length > 0) {
|
|
65
93
|
this.log('');
|
|
66
94
|
this.log(chalk.bold('📝 Actions performed:'));
|
|
67
95
|
for (const action of response.actions) {
|
|
68
96
|
const isError = action.toLowerCase().includes('error');
|
|
69
|
-
const
|
|
97
|
+
const isWarning = action.includes('⚠️');
|
|
98
|
+
let icon;
|
|
99
|
+
if (isError) {
|
|
100
|
+
icon = chalk.red('✗');
|
|
101
|
+
}
|
|
102
|
+
else if (isWarning) {
|
|
103
|
+
icon = chalk.yellow('!');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
icon = chalk.green('✓');
|
|
107
|
+
}
|
|
70
108
|
this.log(` ${icon} ${action}`);
|
|
71
109
|
}
|
|
72
110
|
}
|
|
@@ -75,11 +113,27 @@ export default class TicketSync extends BaseCommand {
|
|
|
75
113
|
this.log(chalk.dim(' No actions performed'));
|
|
76
114
|
}
|
|
77
115
|
this.log('');
|
|
116
|
+
// Show next steps for new tickets
|
|
117
|
+
if (response.created && response.enrichmentQueued) {
|
|
118
|
+
this.log(chalk.dim('💡 Next: Run ') + chalk.cyan(`gut ticket status ${response.ticketId}`) + chalk.dim(' to check enrichment progress'));
|
|
119
|
+
}
|
|
120
|
+
else if (response.created && !response.enrichmentQueued) {
|
|
121
|
+
this.log(chalk.dim('💡 Next: Run ') + chalk.cyan(`gut ticket sync ${response.ticketId} --pull`) + chalk.dim(' to trigger enrichment'));
|
|
122
|
+
}
|
|
78
123
|
}
|
|
79
124
|
catch (error) {
|
|
80
125
|
spinner.fail('Sync failed');
|
|
81
126
|
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
-
|
|
127
|
+
// Provide helpful error messages
|
|
128
|
+
if (message.includes('404') && direction === 'push') {
|
|
129
|
+
this.error(`Ticket not found in gut. Use ${chalk.cyan('--direction pull')} to create from source.`);
|
|
130
|
+
}
|
|
131
|
+
else if (message.includes('404') && direction === 'pull') {
|
|
132
|
+
this.error(`Ticket not found in source system. Check the ticket ID: ${args.ticketId}`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
this.error(`Failed to sync ticket: ${message}`);
|
|
136
|
+
}
|
|
83
137
|
}
|
|
84
138
|
}
|
|
85
139
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class WorktreeCreate 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
|
+
'base-dir': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
from: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
install: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
private runPnpmInstall;
|
|
15
|
+
}
|