@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,483 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import { execSync } from 'node:child_process';
4
+ import * as fs from 'node:fs';
5
+ import * as os from 'node:os';
6
+ import * as path from 'node:path';
7
+ import ora from 'ora';
8
+ import { BaseCommand } from '../base-command.js';
9
+ export default class Extract extends BaseCommand {
10
+ static args = {
11
+ folderPath: Args.string({
12
+ description: 'Path to the folder to extract from the current repository',
13
+ name: 'folderPath',
14
+ required: true,
15
+ }),
16
+ };
17
+ static description = 'Extract a folder from the current repository into its own repository with full git history';
18
+ static examples = [
19
+ '<%= config.bin %> <%= command.id %> packages/my-package --gitlab-token <token> --project-path my-group/my-package',
20
+ '<%= config.bin %> <%= command.id %> src/components --gitlab-token <token> --project-path my-group/frontend/components --target-path .',
21
+ '<%= config.bin %> <%= command.id %> libs/shared --gitlab-token <token> --project-path company/backend/libs/shared-utils',
22
+ ];
23
+ static flags = {
24
+ 'dry-run': Flags.boolean({
25
+ default: false,
26
+ description: 'Show what would be done without actually doing it',
27
+ }),
28
+ force: Flags.boolean({
29
+ char: 'f',
30
+ default: false,
31
+ description: 'Force extraction even if target directory exists',
32
+ }),
33
+ 'gitlab-token': Flags.string({
34
+ description: 'GitLab personal access token for creating projects',
35
+ required: false,
36
+ }),
37
+ 'gitlab-url': Flags.string({
38
+ default: 'https://gitlab.com',
39
+ description: 'GitLab instance URL',
40
+ }),
41
+ 'keep-temp': Flags.boolean({
42
+ default: false,
43
+ description: 'Keep temporary directory after extraction',
44
+ }),
45
+ 'project-path': Flags.string({
46
+ description: 'GitLab project path (group/project-name)',
47
+ required: false,
48
+ }),
49
+ 'target-path': Flags.string({
50
+ default: '.',
51
+ description: 'Target path in the new repository (default: same as source folder)',
52
+ }),
53
+ 'temp-dir': Flags.string({
54
+ description: 'Custom temporary directory path',
55
+ }),
56
+ };
57
+ get requiresInit() {
58
+ return false; // This command can work outside of gut workspace
59
+ }
60
+ async catch(error) {
61
+ if (error.code === 'OCLIF_REQUIRED_ARG_ERROR' ||
62
+ (error.args && error.args.some((arg) => arg.name === 'folderPath'))) {
63
+ this.error(`Please specify a folder path to extract.
64
+
65
+ Usage: gut extract <folder-path>
66
+
67
+ Examples:
68
+ gut extract packages/my-package
69
+ gut extract src/components
70
+ gut extract libs/shared
71
+
72
+ GitLab project paths support nested groups:
73
+ my-group/my-project
74
+ company/frontend/components
75
+ organization/team/backend/services
76
+
77
+ Use --help for more information.`);
78
+ }
79
+ throw error;
80
+ }
81
+ async run() {
82
+ const { args, flags } = await this.parse(Extract);
83
+ const { folderPath } = args;
84
+ const { 'dry-run': dryRun, force, 'gitlab-token': gitlabToken, 'gitlab-url': gitlabUrl, 'keep-temp': keepTemp, 'project-path': projectPath, 'target-path': targetPath, 'temp-dir': customTempDir, } = flags;
85
+ const spinner = ora();
86
+ try {
87
+ // Validate we're in a git repository
88
+ if (!await this.gitService.isRepository(process.cwd())) {
89
+ this.error('Current directory is not a git repository. Please run this command from within a git repository.');
90
+ }
91
+ // Validate folder path is provided
92
+ if (!folderPath || folderPath.trim() === '') {
93
+ this.error('Please specify a folder path to extract. Example: gut extract packages/my-package');
94
+ }
95
+ // Validate folder exists
96
+ const sourcePath = path.resolve(folderPath);
97
+ if (!fs.existsSync(sourcePath)) {
98
+ this.error(`Folder does not exist: ${sourcePath}\nPlease check the path and try again.`);
99
+ }
100
+ // Validate it's actually a directory
101
+ if (!fs.statSync(sourcePath).isDirectory()) {
102
+ this.error(`Path is not a directory: ${sourcePath}\nPlease specify a folder path.`);
103
+ }
104
+ // Get user inputs if not provided
105
+ const config = await this.collectConfiguration({
106
+ gitlabToken,
107
+ gitlabUrl,
108
+ projectPath,
109
+ });
110
+ // Check if GitLab project exists first
111
+ const existingProject = await this.checkExistingProject(config, spinner);
112
+ if (existingProject) {
113
+ this.log(chalk.green('āœ“ GitLab project already exists - skipping extraction'));
114
+ this.log(chalk.dim(` Repository URL: ${existingProject.web_url}`));
115
+ this.log(chalk.dim(` SSH URL: ${existingProject.ssh_url_to_repo}`));
116
+ return;
117
+ }
118
+ if (dryRun) {
119
+ this.log(chalk.yellow('DRY RUN - No actual changes will be made'));
120
+ this.log(`Would extract: ${sourcePath}`);
121
+ this.log(`Target repository: ${config.projectPath}`);
122
+ this.log(`GitLab URL: ${config.gitlabUrl}`);
123
+ this.log(`Target path in new repo: ${targetPath}`);
124
+ this.log('Would create new GitLab project');
125
+ return;
126
+ }
127
+ // Create temporary directory
128
+ const tempDir = customTempDir || path.join(os.tmpdir(), `gut-extract-${Date.now()}`);
129
+ spinner.start('Setting up temporary workspace...');
130
+ fs.mkdirSync(tempDir, { recursive: true });
131
+ spinner.succeed('Temporary workspace created');
132
+ try {
133
+ // Step 1: Clone current repository to temp
134
+ await this.cloneToTemp(tempDir, spinner);
135
+ // Step 2: Get all branches and filter each one
136
+ const branches = await this.getAllBranches(tempDir, spinner);
137
+ await this.filterAllBranches(tempDir, folderPath, targetPath, branches, spinner);
138
+ // Step 3: Create GitLab project
139
+ const projectInfo = await this.createGitLabProject(config, spinner);
140
+ // Step 4: Add remote and push all branches
141
+ await this.pushToNewRepository(tempDir, projectInfo.ssh_url_to_repo, branches, spinner);
142
+ // Step 5: Clone new repository to final location
143
+ await this.cloneNewRepository(projectInfo, folderPath, force, spinner);
144
+ this.log(chalk.green('āœ“ Successfully extracted folder to new repository!'));
145
+ this.log(chalk.dim(` Repository URL: ${projectInfo.web_url}`));
146
+ this.log(chalk.dim(` SSH URL: ${projectInfo.ssh_url_to_repo}`));
147
+ }
148
+ finally {
149
+ // Cleanup temp directory unless requested to keep
150
+ if (!keepTemp) {
151
+ spinner.start('Cleaning up temporary files...');
152
+ execSync(`rm -rf "${tempDir}"`, { stdio: 'pipe' });
153
+ spinner.succeed('Cleanup completed');
154
+ }
155
+ else {
156
+ this.log(chalk.yellow(`Temporary directory preserved at: ${tempDir}`));
157
+ }
158
+ }
159
+ }
160
+ catch (error) {
161
+ spinner.fail();
162
+ this.error(error instanceof Error ? error.message : String(error));
163
+ }
164
+ }
165
+ async checkExistingProject(config, spinner) {
166
+ spinner.start('Checking if GitLab project exists...');
167
+ try {
168
+ const checkResponse = await fetch(`${config.gitlabUrl}/api/v4/projects/${encodeURIComponent(config.projectPath)}`, {
169
+ headers: {
170
+ 'PRIVATE-TOKEN': config.gitlabToken,
171
+ },
172
+ });
173
+ if (checkResponse.ok) {
174
+ const existingProject = await checkResponse.json();
175
+ spinner.succeed(`GitLab project already exists: ${existingProject.web_url}`);
176
+ return existingProject;
177
+ }
178
+ spinner.succeed('GitLab project does not exist - will create new one');
179
+ return null;
180
+ }
181
+ catch (error) {
182
+ spinner.warn(`Could not check project existence: ${error instanceof Error ? error.message : String(error)}`);
183
+ return null;
184
+ }
185
+ }
186
+ async cloneNewRepository(projectInfo, originalFolderPath, force, spinner) {
187
+ const folderName = path.basename(originalFolderPath);
188
+ const targetPath = path.resolve(folderName);
189
+ if (fs.existsSync(targetPath)) {
190
+ if (!force) {
191
+ this.error(`Directory ${targetPath} already exists. Use --force to overwrite.`);
192
+ }
193
+ spinner.start(`Removing existing directory: ${targetPath}`);
194
+ execSync(`rm -rf "${targetPath}"`, { stdio: 'pipe' });
195
+ spinner.succeed('Existing directory removed');
196
+ }
197
+ spinner.start('Cloning new repository...');
198
+ execSync(`git clone "${projectInfo.ssh_url_to_repo}" "${targetPath}"`, { stdio: 'pipe' });
199
+ spinner.succeed(`New repository cloned to: ${targetPath}`);
200
+ // Force pull to ensure all branches are available locally
201
+ spinner.start('Updating local repository...');
202
+ execSync('git fetch --all', { cwd: targetPath, stdio: 'pipe' });
203
+ spinner.succeed('Repository updated');
204
+ }
205
+ async cloneToTemp(tempDir, spinner) {
206
+ spinner.start('Cloning current repository...');
207
+ const currentRepo = path.basename(process.cwd());
208
+ const tempRepoPath = path.join(tempDir, currentRepo);
209
+ const remoteUrl = await this.gitService.getRemoteUrl(process.cwd());
210
+ if (!remoteUrl) {
211
+ // If no remote, create a local clone
212
+ execSync(`cp -R . "${tempRepoPath}"`, { stdio: 'pipe' });
213
+ }
214
+ else {
215
+ execSync(`git clone "${remoteUrl}" "${tempRepoPath}"`, { stdio: 'pipe' });
216
+ }
217
+ spinner.succeed('Repository cloned to temporary location');
218
+ }
219
+ async collectConfiguration(provided) {
220
+ const config = { ...provided };
221
+ if (!config.gitlabToken) {
222
+ // Try to get from environment variable first
223
+ config.gitlabToken = process.env.GITLAB_TOKEN || process.env.GL_TOKEN;
224
+ if (!config.gitlabToken) {
225
+ this.error('GitLab token is required. Provide via --gitlab-token flag or GITLAB_TOKEN environment variable.');
226
+ }
227
+ }
228
+ if (!config.projectPath) {
229
+ this.error('GitLab project path is required. Provide via --project-path flag (e.g., my-group/my-project).');
230
+ }
231
+ return config;
232
+ }
233
+ async createGitLabProject(config, spinner) {
234
+ const pathParts = config.projectPath.split('/');
235
+ const projectName = pathParts.at(-1);
236
+ const groupPath = pathParts.slice(0, -1);
237
+ if (groupPath.length === 0) {
238
+ throw new Error('Project path must include at least one group (e.g., my-group/my-project)');
239
+ }
240
+ // Ensure all nested groups exist
241
+ const finalGroupId = await this.ensureGroupPath(config, groupPath, spinner);
242
+ // Create the project
243
+ spinner.start(`Creating GitLab project: ${projectName}`);
244
+ const createResponse = await fetch(`${config.gitlabUrl}/api/v4/projects`, {
245
+ body: JSON.stringify({
246
+ initialize_with_readme: false,
247
+ name: projectName,
248
+ namespace_id: finalGroupId,
249
+ path: projectName,
250
+ visibility: 'private',
251
+ }),
252
+ headers: {
253
+ 'Content-Type': 'application/json',
254
+ 'PRIVATE-TOKEN': config.gitlabToken,
255
+ },
256
+ method: 'POST',
257
+ });
258
+ if (!createResponse.ok) {
259
+ const errorText = await createResponse.text();
260
+ let errorData;
261
+ try {
262
+ errorData = JSON.parse(errorText);
263
+ }
264
+ catch {
265
+ throw new Error(`Failed to create GitLab project: ${errorText}`);
266
+ }
267
+ if (errorData.message && typeof errorData.message === 'object' && errorData.message.path?.includes('already been taken')) {
268
+ // Project was created concurrently, try to fetch it
269
+ const retryResponse = await fetch(`${config.gitlabUrl}/api/v4/projects/${encodeURIComponent(config.projectPath)}`, {
270
+ headers: {
271
+ 'PRIVATE-TOKEN': config.gitlabToken,
272
+ },
273
+ });
274
+ if (retryResponse.ok) {
275
+ const existingProject = await retryResponse.json();
276
+ spinner.succeed(`Using GitLab project (created concurrently): ${existingProject.web_url}`);
277
+ return existingProject;
278
+ }
279
+ }
280
+ throw new Error(`Failed to create GitLab project: ${typeof errorData.message === 'string' ? errorData.message : errorText}`);
281
+ }
282
+ const project = await createResponse.json();
283
+ spinner.succeed(`GitLab project created: ${project.web_url}`);
284
+ return project;
285
+ }
286
+ async ensureGroupPath(config, groupPath, spinner) {
287
+ let currentParentId = null;
288
+ let currentPath = '';
289
+ for (const groupName of groupPath) {
290
+ currentPath = currentPath ? `${currentPath}/${groupName}` : groupName;
291
+ spinner.start(`Checking group: ${currentPath}`);
292
+ // Try to find existing group
293
+ const groupResponse = await fetch(`${config.gitlabUrl}/api/v4/groups/${encodeURIComponent(currentPath)}`, {
294
+ headers: {
295
+ 'PRIVATE-TOKEN': config.gitlabToken,
296
+ },
297
+ });
298
+ if (groupResponse.ok) {
299
+ // Group exists
300
+ const group = await groupResponse.json();
301
+ currentParentId = group.id;
302
+ spinner.succeed(`Found existing group: ${currentPath}`);
303
+ }
304
+ else if (groupResponse.status === 404) {
305
+ // Group doesn't exist, create it
306
+ spinner.start(`Creating group: ${groupName}`);
307
+ const createGroupBody = {
308
+ name: groupName,
309
+ path: groupName,
310
+ visibility: 'private',
311
+ };
312
+ if (currentParentId !== null) {
313
+ createGroupBody.parent_id = currentParentId;
314
+ }
315
+ const createResponse = await fetch(`${config.gitlabUrl}/api/v4/groups`, {
316
+ body: JSON.stringify(createGroupBody),
317
+ headers: {
318
+ 'Content-Type': 'application/json',
319
+ 'PRIVATE-TOKEN': config.gitlabToken,
320
+ },
321
+ method: 'POST',
322
+ });
323
+ if (!createResponse.ok) {
324
+ const errorText = await createResponse.text();
325
+ let errorData;
326
+ try {
327
+ errorData = JSON.parse(errorText);
328
+ }
329
+ catch {
330
+ throw new Error(`Failed to create group '${groupName}': ${errorText}`);
331
+ }
332
+ if (errorData.message && typeof errorData.message === 'object' && errorData.message.path?.includes('already been taken')) {
333
+ // Group was created by another process, try to fetch it
334
+ const retryResponse = await fetch(`${config.gitlabUrl}/api/v4/groups/${encodeURIComponent(currentPath)}`, {
335
+ headers: {
336
+ 'PRIVATE-TOKEN': config.gitlabToken,
337
+ },
338
+ });
339
+ if (retryResponse.ok) {
340
+ const group = await retryResponse.json();
341
+ currentParentId = group.id;
342
+ spinner.succeed(`Found group (created concurrently): ${currentPath}`);
343
+ continue;
344
+ }
345
+ }
346
+ throw new Error(`Failed to create group '${groupName}': ${typeof errorData.message === 'string' ? errorData.message : errorText}`);
347
+ }
348
+ const newGroup = await createResponse.json();
349
+ currentParentId = newGroup.id;
350
+ spinner.succeed(`Created group: ${currentPath}`);
351
+ }
352
+ else {
353
+ // Other error
354
+ const errorText = await groupResponse.text();
355
+ throw new Error(`Failed to access group '${currentPath}': ${errorText}`);
356
+ }
357
+ }
358
+ if (currentParentId === null) {
359
+ throw new Error('Failed to resolve final group ID');
360
+ }
361
+ return currentParentId;
362
+ }
363
+ async filterAllBranches(tempDir, folderPath, targetPath, branches, spinner) {
364
+ const repoPath = path.join(tempDir, path.basename(process.cwd()));
365
+ // Ensure we have the correct relative path for git filtering
366
+ let relativeFolderPath;
367
+ if (path.isAbsolute(folderPath)) {
368
+ relativeFolderPath = path.relative(process.cwd(), folderPath);
369
+ }
370
+ else {
371
+ relativeFolderPath = folderPath;
372
+ }
373
+ // Normalize path separators for git commands
374
+ relativeFolderPath = relativeFolderPath.replaceAll('\\', '/');
375
+ this.log(chalk.dim(`Filtering path: ${relativeFolderPath} -> ${targetPath}`));
376
+ // Use git filter-repo (modern replacement for filter-branch)
377
+ // filter-repo processes all branches at once, which is more efficient
378
+ spinner.start('Filtering all branches...');
379
+ try {
380
+ if (targetPath === '.') {
381
+ // Extract to root - use subdirectory filter approach
382
+ execSync(`git filter-repo --subdirectory-filter "${relativeFolderPath}" --force`, {
383
+ cwd: repoPath,
384
+ stdio: 'pipe',
385
+ });
386
+ }
387
+ else {
388
+ // Extract and rename path
389
+ execSync(`git filter-repo --path "${relativeFolderPath}" --path-rename "${relativeFolderPath}:${targetPath}" --force`, {
390
+ cwd: repoPath,
391
+ stdio: 'pipe',
392
+ });
393
+ }
394
+ spinner.succeed('All branches filtered using git filter-repo');
395
+ }
396
+ catch {
397
+ // Fallback to filter-branch if filter-repo is not available
398
+ spinner.warn('git filter-repo not available, using git filter-branch (slower)');
399
+ for (const branch of branches) {
400
+ spinner.start(`Processing branch: ${branch}`);
401
+ try {
402
+ // Checkout branch
403
+ execSync(`git checkout -B ${branch} origin/${branch}`, {
404
+ cwd: repoPath,
405
+ stdio: 'pipe',
406
+ });
407
+ if (targetPath === '.') {
408
+ // Extract folder contents to root
409
+ execSync(`git filter-branch --prune-empty --subdirectory-filter "${relativeFolderPath}" --force`, {
410
+ cwd: repoPath,
411
+ stdio: 'pipe',
412
+ });
413
+ }
414
+ else {
415
+ // Extract and move to target path
416
+ execSync(`git filter-branch --prune-empty --tree-filter 'if [ -d "${relativeFolderPath}" ]; then mkdir -p "${targetPath}"; mv "${relativeFolderPath}"/* "${targetPath}/" 2>/dev/null || true; rm -rf "${relativeFolderPath}"; fi' --force`, {
417
+ cwd: repoPath,
418
+ stdio: 'pipe',
419
+ });
420
+ }
421
+ spinner.succeed(`Processed branch: ${branch}`);
422
+ }
423
+ catch (error) {
424
+ spinner.warn(`Skipped branch ${branch}: ${error instanceof Error ? error.message : String(error)}`);
425
+ }
426
+ }
427
+ }
428
+ }
429
+ async getAllBranches(tempDir, spinner) {
430
+ spinner.start('Discovering all branches...');
431
+ const repoPath = path.join(tempDir, path.basename(process.cwd()));
432
+ // Get all remote branches
433
+ const remoteBranches = execSync('git branch -r', {
434
+ cwd: repoPath,
435
+ encoding: 'utf8',
436
+ }).split('\n')
437
+ .map(branch => branch.trim())
438
+ .filter(branch => branch && !branch.includes('HEAD'))
439
+ .map(branch => branch.replace('origin/', ''));
440
+ // Get local branches
441
+ const localBranches = execSync('git branch', {
442
+ cwd: repoPath,
443
+ encoding: 'utf8',
444
+ }).split('\n')
445
+ .map(branch => branch.trim().replace('* ', ''))
446
+ .filter(branch => branch);
447
+ // Combine and deduplicate
448
+ const allBranches = [...new Set([...localBranches, ...remoteBranches])];
449
+ // Ensure master/main is processed first
450
+ const priorityBranches = ['master', 'main'];
451
+ const sortedBranches = [];
452
+ for (const priority of priorityBranches) {
453
+ if (allBranches.includes(priority)) {
454
+ sortedBranches.push(priority);
455
+ }
456
+ }
457
+ // Add remaining branches
458
+ for (const branch of allBranches) {
459
+ if (!priorityBranches.includes(branch)) {
460
+ sortedBranches.push(branch);
461
+ }
462
+ }
463
+ spinner.succeed(`Found ${sortedBranches.length} branches to process (master/main first)`);
464
+ return sortedBranches;
465
+ }
466
+ async pushToNewRepository(tempDir, repositoryUrl, branches, spinner) {
467
+ const repoPath = path.join(tempDir, path.basename(process.cwd()));
468
+ spinner.start('Adding new remote...');
469
+ execSync(`git remote add new-origin "${repositoryUrl}"`, { cwd: repoPath, stdio: 'pipe' });
470
+ spinner.succeed('Remote added');
471
+ for (const branch of branches) {
472
+ spinner.start(`Pushing branch: ${branch}`);
473
+ try {
474
+ execSync(`git checkout ${branch}`, { cwd: repoPath, stdio: 'pipe' });
475
+ execSync(`git push new-origin ${branch}`, { cwd: repoPath, stdio: 'pipe' });
476
+ spinner.succeed(`Pushed branch: ${branch}`);
477
+ }
478
+ catch (error) {
479
+ spinner.warn(`Failed to push branch ${branch}: ${error instanceof Error ? error.message : String(error)}`);
480
+ }
481
+ }
482
+ }
483
+ }
@@ -0,0 +1,19 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Focus extends BaseCommand {
3
+ static args: {
4
+ entityName: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ entityTypeOrName: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static examples: string[];
9
+ static flags: {
10
+ add: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ clear: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ duration: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ mode: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ remember: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ static strict: boolean;
17
+ run(): Promise<void>;
18
+ private isEntityType;
19
+ }
@@ -0,0 +1,137 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../base-command.js';
3
+ import { ENTITY_TYPES } from '../models/entity.model.js';
4
+ export default class Focus extends BaseCommand {
5
+ static args = {
6
+ entityName: Args.string({
7
+ description: 'Entity name (when first arg is entity type)',
8
+ name: 'entityName',
9
+ required: false,
10
+ }),
11
+ entityTypeOrName: Args.string({
12
+ description: 'Entity type (client/prospect/company/initiative) or entity name',
13
+ name: 'entityTypeOrName',
14
+ required: false,
15
+ }),
16
+ };
17
+ static description = 'Set focus on one or more entities with optional type and mode';
18
+ static examples = [
19
+ '<%= config.bin %> <%= command.id %> my-app',
20
+ '<%= config.bin %> <%= command.id %> client mindtools',
21
+ '<%= config.bin %> <%= command.id %> client mindtools --mode delivery',
22
+ '<%= config.bin %> <%= command.id %> prospect jazida --mode research',
23
+ '<%= config.bin %> <%= command.id %> my-app auth-service',
24
+ '<%= config.bin %> <%= command.id %> --clear',
25
+ ];
26
+ static flags = {
27
+ add: Flags.boolean({
28
+ char: 'a',
29
+ description: 'add to current focus instead of replacing',
30
+ }),
31
+ clear: Flags.boolean({
32
+ char: 'c',
33
+ description: 'clear current focus',
34
+ exclusive: ['add', 'mode'],
35
+ }),
36
+ duration: Flags.string({
37
+ char: 'd',
38
+ description: 'focus duration (e.g., 2h, 90m)',
39
+ }),
40
+ mode: Flags.string({
41
+ char: 'm',
42
+ description: 'focus mode (delivery, strategy, audit, debug, research, proposal)',
43
+ options: ['delivery', 'strategy', 'audit', 'debug', 'research', 'proposal'],
44
+ }),
45
+ remember: Flags.boolean({
46
+ char: 'r',
47
+ description: 'remember current focus before switching',
48
+ }),
49
+ };
50
+ static strict = false; // Allow multiple arguments
51
+ async run() {
52
+ const { argv, flags } = await this.parse(Focus);
53
+ // Clear focus
54
+ if (flags.clear) {
55
+ await this.focusService.clearFocus();
56
+ this.log('āœ“ Focus cleared');
57
+ return;
58
+ }
59
+ // Show current focus if no arguments
60
+ if (argv.length === 0) {
61
+ const focus = await this.focusService.getCurrentFocus();
62
+ if (!focus) {
63
+ this.log('No current focus');
64
+ this.log('\nSet focus with: gut focus <entity-name>');
65
+ this.log('Or: gut focus <entity-type> <entity-name>');
66
+ this.log('\nExamples:');
67
+ this.log(' gut focus client mindtools --mode delivery');
68
+ this.log(' gut focus prospect jazida --mode research');
69
+ return;
70
+ }
71
+ const entities = await this.focusService.getFocusedEntities();
72
+ this.log(`\nšŸŽÆ Current focus: ${await this.focusService.getFocusDescription()}`);
73
+ if (focus.mode) {
74
+ this.log(`šŸ“‹ Mode: ${focus.mode}`);
75
+ }
76
+ if (entities.length > 0) {
77
+ this.log('\nFocused entities:');
78
+ for (const entity of entities) {
79
+ this.log(` ${this.getTypeEmoji(entity.type)} ${entity.name} (${entity.type}) - ${entity.path}`);
80
+ }
81
+ }
82
+ return;
83
+ }
84
+ try {
85
+ // Parse arguments - support both "entity-name" and "entity-type entity-name" formats
86
+ let entityNames = [];
87
+ let entityType;
88
+ if (argv.length >= 2 && this.isEntityType(String(argv[0]))) {
89
+ // Format: gut focus client mindtools
90
+ entityType = argv[0];
91
+ entityNames = argv.slice(1).map(String);
92
+ }
93
+ else {
94
+ // Format: gut focus mindtools (backward compatibility)
95
+ entityNames = argv.map(String);
96
+ }
97
+ if (flags.remember) {
98
+ // Save current focus to stack before switching
99
+ await this.focusService.pushFocusToStack();
100
+ }
101
+ if (flags.add) {
102
+ // Add to existing focus
103
+ await this.focusService.addToFocus(entityNames, {
104
+ duration: flags.duration,
105
+ entityType,
106
+ mode: flags.mode,
107
+ });
108
+ this.log(`āœ“ Added to focus: ${entityNames.join(', ')}`);
109
+ }
110
+ else {
111
+ // Set new focus
112
+ await this.focusService.setFocus(entityNames, {
113
+ duration: flags.duration,
114
+ entityType,
115
+ mode: flags.mode,
116
+ });
117
+ const modeText = flags.mode ? ` (${flags.mode} mode)` : '';
118
+ this.log(`āœ“ Focus set to: ${entityNames.join(', ')}${modeText}`);
119
+ }
120
+ // Show what's now focused
121
+ const entities = await this.focusService.getFocusedEntities();
122
+ if (entities.length > 1) {
123
+ this.log(`\nNow focused on ${entities.length} entities:`);
124
+ for (const entity of entities) {
125
+ this.log(` ${this.getTypeEmoji(entity.type)} ${entity.name} (${entity.type})`);
126
+ }
127
+ }
128
+ }
129
+ catch (error) {
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ this.error(message);
132
+ }
133
+ }
134
+ isEntityType(value) {
135
+ return ENTITY_TYPES.includes(value);
136
+ }
137
+ }
@@ -0,0 +1,18 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Graph extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ depth: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'focus-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ private buildRelationshipGraph;
12
+ private displayAsciiGraph;
13
+ private displayDotGraph;
14
+ private getEdgeColor;
15
+ private getNodeColor;
16
+ private getRelationshipIcon;
17
+ private inferRelationships;
18
+ }