@hyperdrive.bot/gut 0.1.9 → 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.
Files changed (35) hide show
  1. package/README.md +1048 -1
  2. package/dist/base-command.d.ts +2 -0
  3. package/dist/base-command.js +3 -0
  4. package/dist/commands/auth/login.d.ts +10 -0
  5. package/dist/commands/auth/login.js +103 -0
  6. package/dist/commands/auth/logout.d.ts +6 -0
  7. package/dist/commands/auth/logout.js +39 -0
  8. package/dist/commands/auth/status.d.ts +9 -0
  9. package/dist/commands/auth/status.js +87 -0
  10. package/dist/commands/entity/clone-all.d.ts +2 -1
  11. package/dist/commands/entity/clone-all.js +49 -10
  12. package/dist/commands/entity/clone.d.ts +3 -1
  13. package/dist/commands/entity/clone.js +45 -19
  14. package/dist/commands/ticket/config.d.ts +13 -0
  15. package/dist/commands/ticket/config.js +22 -0
  16. package/dist/commands/ticket/sync.d.ts +1 -0
  17. package/dist/commands/ticket/sync.js +62 -8
  18. package/dist/commands/worktree/create.d.ts +15 -0
  19. package/dist/commands/worktree/create.js +140 -0
  20. package/dist/models/entity.model.d.ts +16 -0
  21. package/dist/models/ticket.model.d.ts +2 -0
  22. package/dist/services/git.service.d.ts +11 -0
  23. package/dist/services/git.service.js +57 -0
  24. package/dist/services/git.service.test.d.ts +1 -0
  25. package/dist/services/git.service.test.js +101 -0
  26. package/dist/services/gut-api.service.d.ts +20 -1
  27. package/dist/services/gut-api.service.js +30 -2
  28. package/dist/services/tenant.service.d.ts +14 -0
  29. package/dist/services/tenant.service.js +24 -0
  30. package/dist/services/ticket.service.d.ts +3 -1
  31. package/dist/services/ticket.service.js +7 -3
  32. package/dist/services/worktree.service.d.ts +16 -0
  33. package/dist/services/worktree.service.js +60 -0
  34. package/oclif.manifest.json +231 -8
  35. package/package.json +16 -5
@@ -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;
@@ -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,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Logout extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -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,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Status extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -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
+ }
@@ -3,13 +3,14 @@ export default class CloneAll extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
- branch: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  depth: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
8
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
9
  parallel: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
10
  'skip-existing': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  };
12
12
  run(): Promise<void>;
13
+ private detectWorkspaceBranch;
13
14
  private cloneEntity;
14
15
  private cloneInParallel;
15
16
  private cloneSequentially;
@@ -16,8 +16,7 @@ export default class CloneAll extends BaseCommand {
16
16
  static flags = {
17
17
  branch: Flags.string({
18
18
  char: 'b',
19
- default: 'main',
20
- description: 'Branch to clone for all entities',
19
+ description: 'Branch to clone for all entities (auto-detects workspace branch, falls back to master)',
21
20
  }),
22
21
  depth: Flags.integer({
23
22
  char: 'd',
@@ -40,7 +39,10 @@ export default class CloneAll extends BaseCommand {
40
39
  };
41
40
  async run() {
42
41
  const { flags } = await this.parse(CloneAll);
43
- const { branch, depth, force, parallel, 'skip-existing': skipExisting } = flags;
42
+ const { depth, force, parallel, 'skip-existing': skipExisting } = flags;
43
+ // Determine branch: explicit flag > workspace branch > master
44
+ const branch = flags.branch ?? this.detectWorkspaceBranch() ?? 'master';
45
+ this.log(chalk.dim(`Using branch: ${branch}`));
44
46
  const entities = await this.entityService.listEntities();
45
47
  const entitiesToClone = entities.filter(e => e.repository);
46
48
  if (entitiesToClone.length === 0) {
@@ -52,6 +54,20 @@ export default class CloneAll extends BaseCommand {
52
54
  await (parallel ? this.cloneInParallel(entitiesToClone, branch, depth, skipExisting, force, results) : this.cloneSequentially(entitiesToClone, branch, depth, skipExisting, force, results));
53
55
  this.printSummary(results);
54
56
  }
57
+ detectWorkspaceBranch() {
58
+ try {
59
+ const workspaceRoot = this.configService.getWorkspaceRoot();
60
+ const branch = execSync('git branch --show-current', {
61
+ cwd: workspaceRoot,
62
+ encoding: 'utf8',
63
+ stdio: ['pipe', 'pipe', 'pipe'],
64
+ }).trim();
65
+ return branch || undefined;
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ }
55
71
  async cloneEntity(entity, branch, depth, skipExisting, force) {
56
72
  const spinner = ora(`Cloning ${entity.name}`).start();
57
73
  try {
@@ -72,15 +88,38 @@ export default class CloneAll extends BaseCommand {
72
88
  if (!fs.existsSync(parentDir)) {
73
89
  fs.mkdirSync(parentDir, { recursive: true });
74
90
  }
75
- let cloneCommand = 'git clone';
76
- if (branch && branch !== 'main') {
77
- cloneCommand += ` -b ${branch}`;
91
+ const repo = entity.repository || entity.repo;
92
+ const tryClone = (b) => {
93
+ let cmd = `git clone -b ${b}`;
94
+ if (depth)
95
+ cmd += ` --depth ${depth}`;
96
+ cmd += ` ${repo} ${clonePath}`;
97
+ try {
98
+ execSync(cmd, { stdio: 'pipe' });
99
+ return true;
100
+ }
101
+ catch {
102
+ if (fs.existsSync(clonePath)) {
103
+ fs.rmSync(clonePath, { force: true, recursive: true });
104
+ }
105
+ return false;
106
+ }
107
+ };
108
+ let cloned = false;
109
+ if (branch && branch !== 'master') {
110
+ cloned = tryClone(branch);
111
+ if (!cloned) {
112
+ spinner.text = `Branch '${branch}' not found for ${entity.name}, trying master...`;
113
+ cloned = tryClone('master');
114
+ }
115
+ }
116
+ else {
117
+ cloned = tryClone('master');
78
118
  }
79
- if (depth) {
80
- cloneCommand += ` --depth ${depth}`;
119
+ if (!cloned) {
120
+ spinner.fail(`Failed to clone ${entity.name}`);
121
+ return { entity, message: 'Clone failed on all branches', success: false };
81
122
  }
82
- cloneCommand += ` ${entity.repository || entity.repo} ${clonePath}`;
83
- execSync(cloneCommand, { stdio: 'pipe' });
84
123
  const gitBranch = execSync('git branch --show-current', {
85
124
  cwd: clonePath,
86
125
  encoding: 'utf8',
@@ -6,10 +6,12 @@ export default class Clone extends BaseCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
- branch: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  depth: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  path: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
13
  };
14
14
  run(): Promise<void>;
15
+ private detectWorkspaceBranch;
16
+ private tryClone;
15
17
  }
@@ -22,8 +22,7 @@ export default class Clone extends BaseCommand {
22
22
  static flags = {
23
23
  branch: Flags.string({
24
24
  char: 'b',
25
- default: 'main',
26
- description: 'Branch to clone',
25
+ description: 'Branch to clone (auto-detects workspace branch, falls back to master)',
27
26
  }),
28
27
  depth: Flags.integer({
29
28
  char: 'd',
@@ -42,7 +41,8 @@ export default class Clone extends BaseCommand {
42
41
  async run() {
43
42
  const { args, flags } = await this.parse(Clone);
44
43
  const { name } = args;
45
- const { branch, depth, force, path: customPath } = flags;
44
+ const { depth, force, path: customPath } = flags;
45
+ const explicitBranch = flags.branch;
46
46
  const spinner = ora();
47
47
  try {
48
48
  const entity = await this.entityService.getEntity(name);
@@ -68,22 +68,14 @@ export default class Clone extends BaseCommand {
68
68
  if (!fs.existsSync(parentDir)) {
69
69
  fs.mkdirSync(parentDir, { recursive: true });
70
70
  }
71
- let cloneCommand = 'git clone';
72
- if (branch && branch !== 'main') {
73
- cloneCommand += ` -b ${branch}`;
74
- }
75
- if (depth) {
76
- cloneCommand += ` --depth ${depth}`;
77
- }
78
- cloneCommand += ` ${entity.repository} ${clonePath}`;
79
- spinner.start(`Cloning ${name} from ${entity.repository}`);
80
- try {
81
- execSync(cloneCommand, { stdio: 'pipe' });
82
- spinner.succeed(`Successfully cloned ${name}`);
83
- }
84
- catch (error) {
85
- spinner.fail(`Failed to clone ${name}`);
86
- this.error(error instanceof Error ? error.message : String(error));
71
+ // Determine branch: explicit flag > workspace branch > master
72
+ const branch = explicitBranch ?? this.detectWorkspaceBranch(workspaceRoot);
73
+ const cloned = branch && branch !== 'master'
74
+ ? this.tryClone(entity.repository, clonePath, branch, depth, spinner, name)
75
+ || this.tryClone(entity.repository, clonePath, 'master', depth, spinner, name)
76
+ : this.tryClone(entity.repository, clonePath, 'master', depth, spinner, name);
77
+ if (!cloned) {
78
+ this.error(`Failed to clone ${name} from ${entity.repository}`);
87
79
  }
88
80
  if (customPath && customPath !== entity.path) {
89
81
  entity.path = customPath;
@@ -103,4 +95,38 @@ export default class Clone extends BaseCommand {
103
95
  this.error(error instanceof Error ? error.message : String(error));
104
96
  }
105
97
  }
98
+ detectWorkspaceBranch(workspaceRoot) {
99
+ try {
100
+ const branch = execSync('git branch --show-current', {
101
+ cwd: workspaceRoot,
102
+ encoding: 'utf8',
103
+ stdio: ['pipe', 'pipe', 'pipe'],
104
+ }).trim();
105
+ return branch || undefined;
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ }
111
+ tryClone(repository, clonePath, branch, depth, spinner, name) {
112
+ let cmd = `git clone -b ${branch}`;
113
+ if (depth) {
114
+ cmd += ` --depth ${depth}`;
115
+ }
116
+ cmd += ` ${repository} ${clonePath}`;
117
+ spinner.start(`Cloning ${name} (branch: ${branch})`);
118
+ try {
119
+ execSync(cmd, { stdio: 'pipe' });
120
+ spinner.succeed(`Cloned ${name} (branch: ${branch})`);
121
+ return true;
122
+ }
123
+ catch {
124
+ spinner.warn(`Branch '${branch}' not found for ${name}, trying fallback...`);
125
+ // Clean up partial clone if any
126
+ if (fs.existsSync(clonePath)) {
127
+ execSync(`rm -rf ${clonePath}`, { stdio: 'pipe' });
128
+ }
129
+ return false;
130
+ }
131
+ }
106
132
  }
@@ -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>;