@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.
- package/README.md +1 -1
- package/dist/base-command.d.ts +22 -0
- package/dist/base-command.js +99 -0
- package/dist/commands/add.d.ts +14 -0
- package/dist/commands/add.js +70 -0
- package/dist/commands/affected.d.ts +23 -0
- package/dist/commands/affected.js +323 -0
- package/dist/commands/audit.d.ts +33 -0
- package/dist/commands/audit.js +594 -0
- package/dist/commands/back.d.ts +6 -0
- package/dist/commands/back.js +29 -0
- package/dist/commands/checkout.d.ts +14 -0
- package/dist/commands/checkout.js +124 -0
- package/dist/commands/commit.d.ts +11 -0
- package/dist/commands/commit.js +107 -0
- package/dist/commands/context.d.ts +6 -0
- package/dist/commands/context.js +32 -0
- package/dist/commands/contexts.d.ts +7 -0
- package/dist/commands/contexts.js +88 -0
- package/dist/commands/deps.d.ts +10 -0
- package/dist/commands/deps.js +100 -0
- package/dist/commands/entity/add.d.ts +16 -0
- package/dist/commands/entity/add.js +103 -0
- package/dist/commands/entity/clone-all.d.ts +17 -0
- package/dist/commands/entity/clone-all.js +127 -0
- package/dist/commands/entity/clone.d.ts +15 -0
- package/dist/commands/entity/clone.js +106 -0
- package/dist/commands/entity/list.d.ts +11 -0
- package/dist/commands/entity/list.js +80 -0
- package/dist/commands/entity/remove.d.ts +12 -0
- package/dist/commands/entity/remove.js +54 -0
- package/dist/commands/extract.d.ts +35 -0
- package/dist/commands/extract.js +483 -0
- package/dist/commands/focus.d.ts +19 -0
- package/dist/commands/focus.js +137 -0
- package/dist/commands/graph.d.ts +18 -0
- package/dist/commands/graph.js +273 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +75 -0
- package/dist/commands/insights.d.ts +21 -0
- package/dist/commands/insights.js +465 -0
- package/dist/commands/patterns.d.ts +40 -0
- package/dist/commands/patterns.js +405 -0
- package/dist/commands/pull.d.ts +11 -0
- package/dist/commands/pull.js +121 -0
- package/dist/commands/push.d.ts +11 -0
- package/dist/commands/push.js +97 -0
- package/dist/commands/quick-setup.d.ts +20 -0
- package/dist/commands/quick-setup.js +417 -0
- package/dist/commands/recent.d.ts +9 -0
- package/dist/commands/recent.js +51 -0
- package/dist/commands/related.d.ts +23 -0
- package/dist/commands/related.js +255 -0
- package/dist/commands/repos.d.ts +17 -0
- package/dist/commands/repos.js +184 -0
- package/dist/commands/stack.d.ts +10 -0
- package/dist/commands/stack.js +78 -0
- package/dist/commands/status.d.ts +13 -0
- package/dist/commands/status.js +193 -0
- package/dist/commands/sync.d.ts +11 -0
- package/dist/commands/sync.js +139 -0
- package/dist/commands/ticket/focus.d.ts +20 -0
- package/dist/commands/ticket/focus.js +217 -0
- package/dist/commands/ticket/get.d.ts +15 -0
- package/dist/commands/ticket/get.js +168 -0
- package/dist/commands/ticket/hint.d.ts +16 -0
- package/dist/commands/ticket/hint.js +147 -0
- package/dist/commands/ticket/index.d.ts +10 -0
- package/dist/commands/ticket/index.js +60 -0
- package/dist/commands/ticket/list.d.ts +13 -0
- package/dist/commands/ticket/list.js +120 -0
- package/dist/commands/ticket/sync.d.ts +14 -0
- package/dist/commands/ticket/sync.js +85 -0
- package/dist/commands/ticket/update.d.ts +17 -0
- package/dist/commands/ticket/update.js +142 -0
- package/dist/commands/unfocus.d.ts +6 -0
- package/dist/commands/unfocus.js +19 -0
- package/dist/commands/used-by.d.ts +13 -0
- package/dist/commands/used-by.js +110 -0
- package/dist/commands/workspace.d.ts +22 -0
- package/dist/commands/workspace.js +372 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +16 -0
- package/dist/models/entity.model.d.ts +234 -0
- package/dist/models/entity.model.js +1 -0
- package/dist/models/ticket.model.d.ts +117 -0
- package/dist/models/ticket.model.js +43 -0
- package/dist/services/auth.service.d.ts +15 -0
- package/dist/services/auth.service.js +26 -0
- package/dist/services/config.service.d.ts +34 -0
- package/dist/services/config.service.js +234 -0
- package/dist/services/entity.service.d.ts +20 -0
- package/dist/services/entity.service.js +127 -0
- package/dist/services/focus.service.d.ts +71 -0
- package/dist/services/focus.service.js +614 -0
- package/dist/services/git.service.d.ts +39 -0
- package/dist/services/git.service.js +188 -0
- package/dist/services/gut-api.service.d.ts +53 -0
- package/dist/services/gut-api.service.js +99 -0
- package/dist/services/ticket.service.d.ts +84 -0
- package/dist/services/ticket.service.js +207 -0
- package/dist/utils/display.d.ts +26 -0
- package/dist/utils/display.js +145 -0
- package/dist/utils/filesystem.d.ts +32 -0
- package/dist/utils/filesystem.js +198 -0
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.js +14 -0
- package/dist/utils/validation.d.ts +22 -0
- package/dist/utils/validation.js +192 -0
- package/oclif.manifest.json +2008 -0
- package/package.json +11 -2
|
@@ -0,0 +1,255 @@
|
|
|
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 path from 'node:path';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import { BaseCommand } from '../base-command.js';
|
|
8
|
+
export default class Related extends BaseCommand {
|
|
9
|
+
static args = {
|
|
10
|
+
entity: Args.string({
|
|
11
|
+
description: 'Entity name (uses current focus if not provided)',
|
|
12
|
+
name: 'entity',
|
|
13
|
+
required: false,
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
static description = 'Find entities related to a specific entity or current focus';
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> <%= command.id %> my-app',
|
|
19
|
+
'<%= config.bin %> <%= command.id %> --threshold 0.3',
|
|
20
|
+
'<%= config.bin %> <%= command.id %> --type dependencies',
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
detailed: Flags.boolean({
|
|
24
|
+
char: 'd',
|
|
25
|
+
default: false,
|
|
26
|
+
description: 'Show detailed relation information',
|
|
27
|
+
}),
|
|
28
|
+
threshold: Flags.string({
|
|
29
|
+
char: 't',
|
|
30
|
+
default: '0.2',
|
|
31
|
+
description: 'Minimum relation score (0-1)',
|
|
32
|
+
}),
|
|
33
|
+
type: Flags.string({
|
|
34
|
+
default: 'all',
|
|
35
|
+
description: 'Type of relation to check',
|
|
36
|
+
options: ['all', 'dependencies', 'git', 'files'],
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
async run() {
|
|
40
|
+
const { args, flags } = await this.parse(Related);
|
|
41
|
+
const { detailed, threshold, type } = flags;
|
|
42
|
+
const thresholdNum = Number.parseFloat(threshold);
|
|
43
|
+
const spinner = ora();
|
|
44
|
+
try {
|
|
45
|
+
let targetEntity;
|
|
46
|
+
if (args.entity) {
|
|
47
|
+
targetEntity = await this.entityService.getEntity(args.entity);
|
|
48
|
+
if (!targetEntity) {
|
|
49
|
+
this.error(`Entity '${args.entity}' not found`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const focus = await this.focusService.getCurrentFocus();
|
|
54
|
+
if (!focus || !focus.entities || focus.entities.length === 0) {
|
|
55
|
+
this.error('No entity specified and no current focus');
|
|
56
|
+
}
|
|
57
|
+
targetEntity = await this.entityService.getEntity(focus.entities[0].name);
|
|
58
|
+
}
|
|
59
|
+
if (!targetEntity) {
|
|
60
|
+
this.error('Could not determine target entity');
|
|
61
|
+
}
|
|
62
|
+
spinner.start(`Analyzing relations for ${targetEntity.name}`);
|
|
63
|
+
const allEntities = await this.entityService.listEntities();
|
|
64
|
+
const otherEntities = allEntities.filter(e => e.name !== targetEntity.name);
|
|
65
|
+
const relations = [];
|
|
66
|
+
for (const entity of otherEntities) {
|
|
67
|
+
const relation = await this.analyzeRelation(targetEntity, entity, type);
|
|
68
|
+
if (relation.relations.score >= thresholdNum) {
|
|
69
|
+
relations.push(relation);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
spinner.succeed(`Found ${relations.length} related entities`);
|
|
73
|
+
if (relations.length === 0) {
|
|
74
|
+
this.log(chalk.yellow('No related entities found with the specified threshold'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
relations.sort((a, b) => b.relations.score - a.relations.score);
|
|
78
|
+
this.printRelations(targetEntity, relations, detailed);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
spinner.fail();
|
|
82
|
+
this.error(error instanceof Error ? error.message : String(error));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async analyzeRelation(source, target, type) {
|
|
86
|
+
const relations = {
|
|
87
|
+
dependencies: [],
|
|
88
|
+
gitReferences: [],
|
|
89
|
+
score: 0,
|
|
90
|
+
sharedFiles: [],
|
|
91
|
+
};
|
|
92
|
+
if (type === 'all' || type === 'dependencies') {
|
|
93
|
+
relations.dependencies = await this.findDependencyRelations(source, target);
|
|
94
|
+
}
|
|
95
|
+
if (type === 'all' || type === 'files') {
|
|
96
|
+
relations.sharedFiles = await this.findSharedFiles(source, target);
|
|
97
|
+
}
|
|
98
|
+
if (type === 'all' || type === 'git') {
|
|
99
|
+
relations.gitReferences = await this.findGitReferences(source, target);
|
|
100
|
+
}
|
|
101
|
+
relations.score = this.calculateScore(relations);
|
|
102
|
+
return { entity: target, relations };
|
|
103
|
+
}
|
|
104
|
+
calculateScore(relations) {
|
|
105
|
+
let score = 0;
|
|
106
|
+
score += relations.dependencies.length * 0.4;
|
|
107
|
+
score += Math.min(relations.sharedFiles.length * 0.05, 0.3);
|
|
108
|
+
score += Math.min(relations.gitReferences.length * 0.1, 0.3);
|
|
109
|
+
return Math.min(score, 1);
|
|
110
|
+
}
|
|
111
|
+
extractDependencies(pkg) {
|
|
112
|
+
const deps = [];
|
|
113
|
+
if (pkg.dependencies) {
|
|
114
|
+
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
115
|
+
deps.push({ name, type: 'dependency', version: version });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (pkg.devDependencies) {
|
|
119
|
+
for (const [name, version] of Object.entries(pkg.devDependencies)) {
|
|
120
|
+
deps.push({ name, type: 'devDependency', version: version });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (pkg.peerDependencies) {
|
|
124
|
+
for (const [name, version] of Object.entries(pkg.peerDependencies)) {
|
|
125
|
+
deps.push({ name, type: 'peerDependency', version: version });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return deps;
|
|
129
|
+
}
|
|
130
|
+
async findDependencyRelations(source, target) {
|
|
131
|
+
const relations = [];
|
|
132
|
+
const sourcePkgPath = path.join(source.path, 'package.json');
|
|
133
|
+
const targetPkgPath = path.join(target.path, 'package.json');
|
|
134
|
+
if (!fs.existsSync(sourcePkgPath) || !fs.existsSync(targetPkgPath)) {
|
|
135
|
+
return relations;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const sourcePkg = JSON.parse(fs.readFileSync(sourcePkgPath, 'utf8'));
|
|
139
|
+
const targetPkg = JSON.parse(fs.readFileSync(targetPkgPath, 'utf8'));
|
|
140
|
+
const sourceDeps = this.extractDependencies(sourcePkg);
|
|
141
|
+
const targetName = targetPkg.name;
|
|
142
|
+
if (targetName && sourceDeps.some(d => d.name === targetName)) {
|
|
143
|
+
relations.push(`depends on ${targetName}`);
|
|
144
|
+
}
|
|
145
|
+
const targetDeps = this.extractDependencies(targetPkg);
|
|
146
|
+
const sourceName = sourcePkg.name;
|
|
147
|
+
if (sourceName && targetDeps.some(d => d.name === sourceName)) {
|
|
148
|
+
relations.push(`dependency of ${targetName}`);
|
|
149
|
+
}
|
|
150
|
+
const sharedDeps = sourceDeps.filter(sd => targetDeps.some(td => td.name === sd.name));
|
|
151
|
+
if (sharedDeps.length > 0) {
|
|
152
|
+
relations.push(`${sharedDeps.length} shared dependencies`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Ignore parsing errors
|
|
157
|
+
}
|
|
158
|
+
return relations;
|
|
159
|
+
}
|
|
160
|
+
async findGitReferences(source, target) {
|
|
161
|
+
const relations = [];
|
|
162
|
+
try {
|
|
163
|
+
if (fs.existsSync(path.join(source.path, '.git'))) {
|
|
164
|
+
const logs = execSync(`git log --grep="${target.name}" --oneline`, {
|
|
165
|
+
cwd: source.path,
|
|
166
|
+
encoding: 'utf8',
|
|
167
|
+
stdio: 'pipe',
|
|
168
|
+
}).toString();
|
|
169
|
+
const mentions = logs.split('\n').filter((l) => l.trim()).length;
|
|
170
|
+
if (mentions > 0) {
|
|
171
|
+
relations.push(`${mentions} git mentions`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Ignore git errors
|
|
177
|
+
}
|
|
178
|
+
return relations;
|
|
179
|
+
}
|
|
180
|
+
async findSharedFiles(source, target) {
|
|
181
|
+
const relations = [];
|
|
182
|
+
try {
|
|
183
|
+
const sourceFiles = this.getFileList(source.path);
|
|
184
|
+
const targetFiles = this.getFileList(target.path);
|
|
185
|
+
const sharedPatterns = sourceFiles.filter(sf => targetFiles.some(tf => this.isSimilarFile(sf, tf)));
|
|
186
|
+
if (sharedPatterns.length > 0) {
|
|
187
|
+
relations.push(`${sharedPatterns.length} similar files`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Ignore file system errors
|
|
192
|
+
}
|
|
193
|
+
return relations;
|
|
194
|
+
}
|
|
195
|
+
getFileList(dir, baseDir = dir) {
|
|
196
|
+
const files = [];
|
|
197
|
+
if (!fs.existsSync(dir))
|
|
198
|
+
return files;
|
|
199
|
+
try {
|
|
200
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
const fullPath = path.join(dir, entry.name);
|
|
203
|
+
if (entry.name === 'node_modules' || entry.name === '.git')
|
|
204
|
+
continue;
|
|
205
|
+
if (entry.isDirectory()) {
|
|
206
|
+
files.push(...this.getFileList(fullPath, baseDir));
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
210
|
+
files.push(relativePath);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Ignore permission errors
|
|
216
|
+
}
|
|
217
|
+
return files;
|
|
218
|
+
}
|
|
219
|
+
isSimilarFile(file1, file2) {
|
|
220
|
+
const base1 = path.basename(file1);
|
|
221
|
+
const base2 = path.basename(file2);
|
|
222
|
+
return base1 === base2
|
|
223
|
+
&& (base1.includes('config')
|
|
224
|
+
|| base1.includes('test')
|
|
225
|
+
|| base1.includes('spec')
|
|
226
|
+
|| base1 === 'README.md'
|
|
227
|
+
|| base1 === 'package.json');
|
|
228
|
+
}
|
|
229
|
+
printRelations(source, relations, detailed) {
|
|
230
|
+
this.log('');
|
|
231
|
+
this.log(chalk.bold(`Relations for ${source.name}:`));
|
|
232
|
+
this.log('');
|
|
233
|
+
for (const [index, rel] of relations.entries()) {
|
|
234
|
+
const scoreColor = rel.relations.score > 0.5
|
|
235
|
+
? chalk.green
|
|
236
|
+
: (rel.relations.score > 0.3
|
|
237
|
+
? chalk.yellow
|
|
238
|
+
: chalk.dim);
|
|
239
|
+
this.log(`${index + 1}. ${chalk.bold(rel.entity.name)} ${scoreColor(`(score: ${rel.relations.score.toFixed(2)})`)}`);
|
|
240
|
+
if (detailed) {
|
|
241
|
+
if (rel.relations.dependencies.length > 0) {
|
|
242
|
+
this.log(chalk.dim(` Dependencies: ${rel.relations.dependencies.join(', ')}`));
|
|
243
|
+
}
|
|
244
|
+
if (rel.relations.sharedFiles.length > 0) {
|
|
245
|
+
this.log(chalk.dim(` Shared: ${rel.relations.sharedFiles.join(', ')}`));
|
|
246
|
+
}
|
|
247
|
+
if (rel.relations.gitReferences.length > 0) {
|
|
248
|
+
this.log(chalk.dim(` Git: ${rel.relations.gitReferences.join(', ')}`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
this.log('');
|
|
253
|
+
this.log(chalk.dim('Tip: Use --detailed flag for more information'));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
export default class Repos extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
accessible: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
status: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
private buildRepoInfoLine;
|
|
13
|
+
private displayGitStatus;
|
|
14
|
+
private displayRelationships;
|
|
15
|
+
private displayRepoInfo;
|
|
16
|
+
private displayRepos;
|
|
17
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { BaseCommand } from '../base-command.js';
|
|
5
|
+
export default class Repos extends BaseCommand {
|
|
6
|
+
static description = 'List accessible repositories in the workspace';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> <%= command.id %>',
|
|
9
|
+
'<%= config.bin %> <%= command.id %> --type client',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --status',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
12
|
+
];
|
|
13
|
+
static flags = {
|
|
14
|
+
accessible: Flags.boolean({
|
|
15
|
+
char: 'a',
|
|
16
|
+
default: false,
|
|
17
|
+
description: 'show only accessible repositories (with valid paths)',
|
|
18
|
+
}),
|
|
19
|
+
json: Flags.boolean({
|
|
20
|
+
default: false,
|
|
21
|
+
description: 'output as JSON',
|
|
22
|
+
}),
|
|
23
|
+
status: Flags.boolean({
|
|
24
|
+
char: 's',
|
|
25
|
+
default: false,
|
|
26
|
+
description: 'show git status for each repository',
|
|
27
|
+
}),
|
|
28
|
+
type: Flags.string({
|
|
29
|
+
char: 't',
|
|
30
|
+
description: 'filter by entity type',
|
|
31
|
+
options: ['client', 'prospect', 'company', 'initiative', 'system', 'delivery', 'module', 'service', 'tool'],
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { flags } = await this.parse(Repos);
|
|
36
|
+
let entities = this.entityService.getAllEntities();
|
|
37
|
+
// Filter by type if specified
|
|
38
|
+
if (flags.type) {
|
|
39
|
+
entities = entities.filter(e => e.type === flags.type);
|
|
40
|
+
}
|
|
41
|
+
// Filter by accessibility if specified
|
|
42
|
+
if (flags.accessible) {
|
|
43
|
+
entities = entities.filter(e => {
|
|
44
|
+
const entityPath = this.entityService.resolveEntityPath(e);
|
|
45
|
+
return fs.existsSync(entityPath);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (entities.length === 0) {
|
|
49
|
+
this.error('No repositories found matching criteria');
|
|
50
|
+
}
|
|
51
|
+
// Collect repository information
|
|
52
|
+
const repos = [];
|
|
53
|
+
for (const entity of entities) {
|
|
54
|
+
const entityPath = this.entityService.resolveEntityPath(entity);
|
|
55
|
+
const repoInfo = {
|
|
56
|
+
absolutePath: entityPath,
|
|
57
|
+
exists: fs.existsSync(entityPath),
|
|
58
|
+
isGitRepo: false,
|
|
59
|
+
metadata: entity.metadata || {},
|
|
60
|
+
name: entity.name,
|
|
61
|
+
path: entity.path,
|
|
62
|
+
status: null,
|
|
63
|
+
type: entity.type,
|
|
64
|
+
};
|
|
65
|
+
if (repoInfo.exists) {
|
|
66
|
+
try {
|
|
67
|
+
repoInfo.isGitRepo = await this.gitService.isRepository(entityPath);
|
|
68
|
+
if (repoInfo.isGitRepo && flags.status) {
|
|
69
|
+
repoInfo.status = await this.gitService.getStatus(entityPath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Ignore git errors for non-git directories
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
repos.push(repoInfo);
|
|
77
|
+
}
|
|
78
|
+
// Output results
|
|
79
|
+
if (flags.json) {
|
|
80
|
+
this.log(JSON.stringify(repos, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await this.displayRepos(repos, flags.status);
|
|
84
|
+
}
|
|
85
|
+
buildRepoInfoLine(repo, isFocused) {
|
|
86
|
+
const emoji = this.getTypeEmoji(repo.type);
|
|
87
|
+
const focusIndicator = isFocused ? chalk.yellow('ā') : ' ';
|
|
88
|
+
const accessIndicator = repo.exists ? (repo.isGitRepo ? 'š' : 'š') : 'ā';
|
|
89
|
+
let line = ` ${focusIndicator} ${emoji} ${chalk.cyan(repo.name)} ${accessIndicator}`;
|
|
90
|
+
line += ` ${chalk.dim(repo.path)}`;
|
|
91
|
+
if (repo.type === 'client' && repo.metadata.business?.status) {
|
|
92
|
+
line += ` ${chalk.dim(`(${repo.metadata.business.status})`)}`;
|
|
93
|
+
}
|
|
94
|
+
return line;
|
|
95
|
+
}
|
|
96
|
+
displayGitStatus(status) {
|
|
97
|
+
if (status.hasChanges) {
|
|
98
|
+
if (status.changes.length > 0) {
|
|
99
|
+
this.log(` ${chalk.yellow('M')} ${status.changes.length} modified`);
|
|
100
|
+
}
|
|
101
|
+
if (status.untracked.length > 0) {
|
|
102
|
+
this.log(` ${chalk.green('A')} ${status.untracked.length} untracked`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
this.log(` ${chalk.dim('Clean')}`);
|
|
107
|
+
}
|
|
108
|
+
if (status.ahead > 0 || status.behind > 0) {
|
|
109
|
+
const ahead = status.ahead > 0 ? `ā${status.ahead}` : '';
|
|
110
|
+
const behind = status.behind > 0 ? `ā${status.behind}` : '';
|
|
111
|
+
this.log(` ${chalk.blue('Remote:')} ${ahead} ${behind}`.trim());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
displayRelationships(repo) {
|
|
115
|
+
if (!repo.metadata.relationships) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const rel = repo.metadata.relationships;
|
|
119
|
+
const deps = rel.dependent_systems?.length || 0;
|
|
120
|
+
const similar = rel.similar_entities?.length || 0;
|
|
121
|
+
if (deps > 0 || similar > 0) {
|
|
122
|
+
const info = [];
|
|
123
|
+
if (deps > 0)
|
|
124
|
+
info.push(`${deps} deps`);
|
|
125
|
+
if (similar > 0)
|
|
126
|
+
info.push(`${similar} similar`);
|
|
127
|
+
this.log(` ${chalk.dim('Relationships:')} ${chalk.dim(info.join(', '))}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
displayRepoInfo(repo, showStatus, isFocused) {
|
|
131
|
+
const line = this.buildRepoInfoLine(repo, isFocused);
|
|
132
|
+
this.log(line);
|
|
133
|
+
if (showStatus && repo.status) {
|
|
134
|
+
this.displayGitStatus(repo.status);
|
|
135
|
+
}
|
|
136
|
+
this.displayRelationships(repo);
|
|
137
|
+
}
|
|
138
|
+
async displayRepos(repos, showStatus) {
|
|
139
|
+
const currentFocus = await this.focusService.getCurrentFocus();
|
|
140
|
+
const focusedEntityNames = currentFocus?.entities?.map(e => e.name) || [];
|
|
141
|
+
this.log(chalk.bold('\nš Workspace Repositories'));
|
|
142
|
+
this.log(chalk.dim('ā'.repeat(50)));
|
|
143
|
+
// Group by type
|
|
144
|
+
const byType = {};
|
|
145
|
+
for (const repo of repos) {
|
|
146
|
+
if (!byType[repo.type])
|
|
147
|
+
byType[repo.type] = [];
|
|
148
|
+
byType[repo.type].push(repo);
|
|
149
|
+
}
|
|
150
|
+
const typeOrder = ['client', 'prospect', 'company', 'initiative', 'system', 'delivery', 'module', 'service', 'tool'];
|
|
151
|
+
for (const type of typeOrder) {
|
|
152
|
+
if (!byType[type] || byType[type].length === 0)
|
|
153
|
+
continue;
|
|
154
|
+
this.log(`\n${chalk.bold(type.toUpperCase())}:`);
|
|
155
|
+
for (const repo of byType[type]) {
|
|
156
|
+
this.displayRepoInfo(repo, showStatus, focusedEntityNames.includes(repo.name));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Summary
|
|
160
|
+
const totalRepos = repos.length;
|
|
161
|
+
const gitRepos = repos.filter(r => r.isGitRepo).length;
|
|
162
|
+
const accessibleRepos = repos.filter(r => r.exists).length;
|
|
163
|
+
const focusedRepos = repos.filter(r => focusedEntityNames.includes(r.name)).length;
|
|
164
|
+
this.log(chalk.bold('\nš Repository Summary:'));
|
|
165
|
+
this.log(` Total entities: ${totalRepos}`);
|
|
166
|
+
this.log(` Accessible: ${accessibleRepos}`);
|
|
167
|
+
this.log(` Git repositories: ${gitRepos}`);
|
|
168
|
+
if (focusedRepos > 0) {
|
|
169
|
+
this.log(` Currently focused: ${focusedRepos}`);
|
|
170
|
+
}
|
|
171
|
+
// Show access patterns
|
|
172
|
+
if (repos.some(r => r.metadata.business)) {
|
|
173
|
+
this.log(chalk.bold('\nš Access Information:'));
|
|
174
|
+
const clientRepos = repos.filter(r => r.type === 'client');
|
|
175
|
+
const privateRepos = repos.filter(r => r.path.includes('private') || r.path.includes('business'));
|
|
176
|
+
if (clientRepos.length > 0) {
|
|
177
|
+
this.log(` Client repositories: ${clientRepos.length}`);
|
|
178
|
+
}
|
|
179
|
+
if (privateRepos.length > 0) {
|
|
180
|
+
this.log(` Private/Business: ${privateRepos.length}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
export default class Stack extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
clear: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
private formatTimeAgo;
|
|
10
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { BaseCommand } from '../base-command.js';
|
|
4
|
+
export default class Stack extends BaseCommand {
|
|
5
|
+
static description = 'Show and manage the focus stack';
|
|
6
|
+
static examples = [
|
|
7
|
+
'<%= config.bin %> <%= command.id %>',
|
|
8
|
+
'<%= config.bin %> <%= command.id %> --clear',
|
|
9
|
+
];
|
|
10
|
+
static flags = {
|
|
11
|
+
clear: Flags.boolean({
|
|
12
|
+
char: 'c',
|
|
13
|
+
description: 'clear the focus stack',
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { flags } = await this.parse(Stack);
|
|
18
|
+
if (flags.clear) {
|
|
19
|
+
// Clear the focus stack
|
|
20
|
+
const clearedCount = this.focusService.clearFocusStack();
|
|
21
|
+
if (clearedCount === 0) {
|
|
22
|
+
this.log('Focus stack is already empty');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.log(`ā Cleared ${clearedCount} items from focus stack`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Show current focus stack
|
|
29
|
+
const currentFocus = await this.focusService.getCurrentFocus();
|
|
30
|
+
const focusStack = this.focusService.getFocusStack();
|
|
31
|
+
this.log(chalk.bold('\nš Focus Stack'));
|
|
32
|
+
this.log(chalk.dim('ā'.repeat(50)));
|
|
33
|
+
// Show current focus
|
|
34
|
+
if (currentFocus) {
|
|
35
|
+
const description = await this.focusService.getFocusDescription();
|
|
36
|
+
this.log(`${chalk.green('āø Current:')} ${description}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
this.log(`${chalk.dim('āø Current:')} No focus set`);
|
|
40
|
+
}
|
|
41
|
+
// Show stack items
|
|
42
|
+
if (focusStack.length > 0) {
|
|
43
|
+
this.log(`\n${chalk.yellow('Stack:')}`);
|
|
44
|
+
for (const [index, focus] of focusStack.entries()) {
|
|
45
|
+
const entities = focus.entities?.map(e => e.name).join(', ') || focus.name;
|
|
46
|
+
const modeText = focus.mode ? ` (${focus.mode})` : '';
|
|
47
|
+
const timeAgo = this.formatTimeAgo(focus.timestamp || 0);
|
|
48
|
+
this.log(` ${chalk.dim(`${index + 1}.`)} ${entities}${modeText} ${chalk.dim(`- ${timeAgo}`)}`);
|
|
49
|
+
}
|
|
50
|
+
this.log(chalk.dim('\nUse "gut back" to pop from stack and return to previous focus'));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.log(`\n${chalk.dim('Stack is empty')}`);
|
|
54
|
+
}
|
|
55
|
+
// Show usage tips
|
|
56
|
+
this.log(chalk.dim('\nā'.repeat(50)));
|
|
57
|
+
this.log(chalk.dim('Tips:'));
|
|
58
|
+
this.log(chalk.dim('⢠Use "gut focus <entity> --remember" to push current focus to stack'));
|
|
59
|
+
this.log(chalk.dim('⢠Use "gut back" to return to previous focus'));
|
|
60
|
+
this.log(chalk.dim('⢠Use "gut stack --clear" to clear the stack'));
|
|
61
|
+
}
|
|
62
|
+
formatTimeAgo(timestamp) {
|
|
63
|
+
if (!timestamp)
|
|
64
|
+
return 'unknown';
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const diff = now - timestamp;
|
|
67
|
+
const minutes = Math.floor(diff / (1000 * 60));
|
|
68
|
+
const hours = Math.floor(minutes / 60);
|
|
69
|
+
const days = Math.floor(hours / 24);
|
|
70
|
+
if (days > 0)
|
|
71
|
+
return `${days}d ago`;
|
|
72
|
+
if (hours > 0)
|
|
73
|
+
return `${hours}h ago`;
|
|
74
|
+
if (minutes > 0)
|
|
75
|
+
return `${minutes}m ago`;
|
|
76
|
+
return 'just now';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
export default class Status extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
private displayEnhancedStatus;
|
|
12
|
+
private displayEntityStatus;
|
|
13
|
+
}
|