@hyperdrive.bot/gut 0.1.8 → 0.1.10

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 +1048 -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 +18 -0
  25. package/dist/commands/entity/clone-all.js +166 -0
  26. package/dist/commands/entity/clone.d.ts +17 -0
  27. package/dist/commands/entity/clone.js +132 -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 +2006 -0
  111. package/package.json +11 -2
@@ -0,0 +1,594 @@
1
+ import { Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import * as fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { BaseCommand } from '../base-command.js';
6
+ export default class Audit extends BaseCommand {
7
+ static description = 'Access and change audit across workspace';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %>',
10
+ '<%= config.bin %> <%= command.id %> --entity mindtools',
11
+ '<%= config.bin %> <%= command.id %> --security',
12
+ '<%= config.bin %> <%= command.id %> --compliance',
13
+ ];
14
+ static flags = {
15
+ access: Flags.boolean({
16
+ char: 'a',
17
+ default: false,
18
+ description: 'audit access patterns and permissions',
19
+ }),
20
+ changes: Flags.boolean({
21
+ default: false,
22
+ description: 'audit recent changes and activity',
23
+ }),
24
+ compliance: Flags.boolean({
25
+ char: 'c',
26
+ default: false,
27
+ description: 'focus on compliance audit',
28
+ }),
29
+ entity: Flags.string({
30
+ char: 'e',
31
+ description: 'audit specific entity',
32
+ }),
33
+ json: Flags.boolean({
34
+ default: false,
35
+ description: 'output as JSON',
36
+ }),
37
+ security: Flags.boolean({
38
+ char: 's',
39
+ default: false,
40
+ description: 'focus on security audit',
41
+ }),
42
+ };
43
+ async run() {
44
+ const { flags } = await this.parse(Audit);
45
+ let entities = [];
46
+ if (flags.entity) {
47
+ const entity = this.entityService.findEntity(flags.entity);
48
+ if (!entity) {
49
+ this.error(`Entity '${flags.entity}' not found`);
50
+ }
51
+ entities = [entity];
52
+ }
53
+ else {
54
+ entities = this.entityService.getAllEntities();
55
+ }
56
+ if (entities.length === 0) {
57
+ this.error('No entities found to audit');
58
+ }
59
+ const auditResults = await this.performAudit(entities, flags);
60
+ if (flags.json) {
61
+ this.log(JSON.stringify(auditResults, null, 2));
62
+ return;
63
+ }
64
+ this.displayAuditResults(auditResults);
65
+ }
66
+ async auditEntity(entity, flags) {
67
+ const entityPath = this.entityService.resolveEntityPath(entity);
68
+ const exists = fs.existsSync(entityPath);
69
+ const audit = {
70
+ accessIssues: [],
71
+ complianceIssues: [],
72
+ entity: entity.name,
73
+ exists,
74
+ fileAudit: { issues: [], recommendations: [], structureScore: 0 },
75
+ gitAudit: { isGitRepo: false, issues: [] },
76
+ issues: [],
77
+ metadataAudit: { hasMetadataFile: false, issues: [], metadataCompleteness: 0 },
78
+ path: entity.path,
79
+ securityIssues: [],
80
+ timestamp: new Date().toISOString(),
81
+ type: entity.type,
82
+ };
83
+ if (!audit.exists) {
84
+ audit.issues.push({
85
+ message: 'Entity path does not exist',
86
+ path: entityPath,
87
+ severity: 'high',
88
+ type: 'missing_entity',
89
+ });
90
+ return audit;
91
+ }
92
+ // Metadata audit
93
+ audit.metadataAudit = await this.auditMetadata(entity);
94
+ // Git audit
95
+ if (await this.gitService.isRepository(entityPath)) {
96
+ audit.gitAudit = await this.auditGitRepository(entityPath);
97
+ }
98
+ // File structure audit
99
+ audit.fileAudit = await this.auditFileStructure(entityPath, entity.type);
100
+ // Security audit
101
+ if (flags.security || !this.hasSpecificAuditType(flags)) {
102
+ audit.securityIssues = await this.performSecurityAudit(entityPath);
103
+ }
104
+ // Compliance audit
105
+ if (flags.compliance || !this.hasSpecificAuditType(flags)) {
106
+ audit.complianceIssues = await this.performComplianceAudit(entity);
107
+ }
108
+ // Access audit
109
+ if (flags.access || !this.hasSpecificAuditType(flags)) {
110
+ audit.accessIssues = await this.performAccessAudit(entity);
111
+ }
112
+ return audit;
113
+ }
114
+ async auditFileStructure(entityPath, entityType) {
115
+ const audit = {
116
+ issues: [],
117
+ recommendations: [],
118
+ structureScore: 0,
119
+ };
120
+ try {
121
+ const files = fs.readdirSync(entityPath);
122
+ // Check for common files based on entity type
123
+ const expectedFiles = this.getExpectedFiles(entityType);
124
+ for (const expectedFile of expectedFiles) {
125
+ if (!files.includes(expectedFile.name)) {
126
+ audit.issues.push({
127
+ message: `Missing ${expectedFile.description}: ${expectedFile.name}`,
128
+ severity: expectedFile.required ? 'medium' : 'low',
129
+ type: 'missing_file',
130
+ });
131
+ }
132
+ }
133
+ // Check for documentation
134
+ const hasReadme = files.some(f => f.toLowerCase().startsWith('readme'));
135
+ if (!hasReadme) {
136
+ audit.recommendations.push('Add README.md file for documentation');
137
+ }
138
+ // Check for configuration files
139
+ const hasConfig = files.some(f => f.includes('config') || f.includes('.env'));
140
+ audit.hasConfiguration = hasConfig;
141
+ audit.structureScore = this.calculateStructureScore(files, expectedFiles);
142
+ }
143
+ catch (error) {
144
+ const message = error instanceof Error ? error.message : String(error);
145
+ audit.issues.push({
146
+ message: `File structure audit failed: ${message}`,
147
+ severity: 'high',
148
+ type: 'file_audit_error',
149
+ });
150
+ }
151
+ return audit;
152
+ }
153
+ async auditGitRepository(repoPath) {
154
+ const audit = {
155
+ isGitRepo: true,
156
+ issues: [],
157
+ };
158
+ try {
159
+ // Check git status
160
+ const status = await this.gitService.getStatus(repoPath);
161
+ audit.status = status;
162
+ // Check for uncommitted changes
163
+ if (status.hasChanges) {
164
+ audit.issues.push({
165
+ message: `Repository has ${status.changes.length} uncommitted changes`,
166
+ severity: 'low',
167
+ type: 'uncommitted_changes',
168
+ });
169
+ }
170
+ // Check remote configuration
171
+ try {
172
+ const remotes = await this.gitService.exec(['remote', '-v'], { cwd: repoPath });
173
+ audit.hasRemote = remotes.length > 0;
174
+ if (!audit.hasRemote) {
175
+ audit.issues.push({
176
+ message: 'Repository has no remote configured',
177
+ severity: 'medium',
178
+ type: 'no_remote',
179
+ });
180
+ }
181
+ }
182
+ catch {
183
+ audit.hasRemote = false;
184
+ }
185
+ // Check branch information
186
+ try {
187
+ const branch = await this.gitService.exec(['branch', '--show-current'], { cwd: repoPath });
188
+ audit.currentBranch = branch.trim();
189
+ if ((audit.currentBranch === 'master' || audit.currentBranch === 'main') // Check if working directly on main branch
190
+ && status.hasChanges) {
191
+ audit.issues.push({
192
+ message: 'Working directly on main/master branch with uncommitted changes',
193
+ severity: 'medium',
194
+ type: 'working_on_main',
195
+ });
196
+ }
197
+ }
198
+ catch {
199
+ // Ignore branch check errors
200
+ }
201
+ }
202
+ catch (error) {
203
+ const message = error instanceof Error ? error.message : String(error);
204
+ audit.issues.push({
205
+ message: `Git audit failed: ${message}`,
206
+ severity: 'high',
207
+ type: 'git_error',
208
+ });
209
+ }
210
+ return audit;
211
+ }
212
+ async auditMetadata(entity) {
213
+ const issues = [];
214
+ const metadata = entity.metadata || {};
215
+ // Check required metadata fields
216
+ if (entity.type === 'client') {
217
+ if (!metadata.business?.primary_contact) {
218
+ issues.push({
219
+ field: 'business.primary_contact',
220
+ message: 'Client entity missing primary contact information',
221
+ severity: 'medium',
222
+ type: 'missing_metadata',
223
+ });
224
+ }
225
+ if (!metadata.business?.contract_value) {
226
+ issues.push({
227
+ field: 'business.contract_value',
228
+ message: 'Client entity missing contract value',
229
+ severity: 'low',
230
+ type: 'missing_metadata',
231
+ });
232
+ }
233
+ }
234
+ // Check metadata file existence
235
+ const entityPath = this.entityService.resolveEntityPath(entity);
236
+ const metadataPath = path.join(entityPath, '.entity.yaml');
237
+ return {
238
+ hasMetadataFile: fs.existsSync(metadataPath),
239
+ issues,
240
+ metadataCompleteness: this.calculateMetadataCompleteness(metadata, entity.type),
241
+ };
242
+ }
243
+ calculateMetadataCompleteness(metadata, entityType) {
244
+ const requiredFields = this.getRequiredMetadataFields(entityType);
245
+ let completedFields = 0;
246
+ for (const field of requiredFields) {
247
+ if (this.hasNestedField(metadata, field)) {
248
+ completedFields++;
249
+ }
250
+ }
251
+ return requiredFields.length > 0 ? completedFields / requiredFields.length : 1;
252
+ }
253
+ calculateStructureScore(files, expectedFiles) {
254
+ let score = 0;
255
+ const totalExpected = expectedFiles.length;
256
+ for (const expected of expectedFiles) {
257
+ if (files.includes(expected.name)) {
258
+ score += expected.required ? 2 : 1;
259
+ }
260
+ }
261
+ return totalExpected > 0 ? score / (totalExpected * 2) : 1;
262
+ }
263
+ displayAuditResults(results) {
264
+ this.log(chalk.bold('\n🔍 Workspace Audit Report'));
265
+ this.log(chalk.dim('─'.repeat(50)));
266
+ this.log(`📅 Generated: ${new Date(results.timestamp).toLocaleString()}`);
267
+ this.log(`📊 Scope: ${results.scope.entities.length} entities`);
268
+ this.log(`🎯 Audit Types: ${results.scope.auditTypes.join(', ')}`);
269
+ this.log('');
270
+ // Summary
271
+ this.log(chalk.bold('📋 Summary:'));
272
+ const { summary } = results;
273
+ if (summary.issuesFound === 0) {
274
+ this.log(chalk.green('✅ No issues found'));
275
+ }
276
+ else {
277
+ this.log(`⚠️ Total Issues: ${chalk.yellow(summary.issuesFound)}`);
278
+ if (summary.securityIssues > 0) {
279
+ this.log(`🔒 Security Issues: ${chalk.red(summary.securityIssues)}`);
280
+ }
281
+ if (summary.complianceIssues > 0) {
282
+ this.log(`📋 Compliance Issues: ${chalk.yellow(summary.complianceIssues)}`);
283
+ }
284
+ if (summary.accessIssues > 0) {
285
+ this.log(`🔐 Access Issues: ${chalk.red(summary.accessIssues)}`);
286
+ }
287
+ }
288
+ this.log('');
289
+ // Detailed findings
290
+ for (const finding of results.findings) {
291
+ this.displayEntityAudit(finding);
292
+ }
293
+ // Recommendations
294
+ this.displayRecommendations(results);
295
+ }
296
+ displayEntityAudit(audit) {
297
+ const emoji = this.getTypeEmoji(audit.type);
298
+ const issueCount = audit.issues.length + audit.securityIssues.length
299
+ + audit.complianceIssues.length + audit.accessIssues.length;
300
+ const statusIcon = issueCount === 0
301
+ ? chalk.green('✅')
302
+ : (issueCount < 3 ? chalk.yellow('⚠️') : chalk.red('❌'));
303
+ this.log(`${emoji} ${chalk.bold(audit.entity)} ${statusIcon}`);
304
+ if (!audit.exists) {
305
+ this.log(` ${chalk.red('❌ Entity path does not exist')}`);
306
+ return;
307
+ }
308
+ // Show critical issues
309
+ const criticalIssues = [
310
+ ...audit.issues,
311
+ ...audit.securityIssues,
312
+ ...audit.complianceIssues,
313
+ ...audit.accessIssues,
314
+ ].filter(issue => issue.severity === 'high');
315
+ if (criticalIssues.length > 0) {
316
+ this.log(` ${chalk.red('🚨 Critical Issues:')}`);
317
+ for (const issue of criticalIssues) {
318
+ this.log(` • ${issue.message}`);
319
+ }
320
+ }
321
+ // Show metadata completeness
322
+ if (audit.metadataAudit?.metadataCompleteness !== undefined) {
323
+ const completeness = Math.round(audit.metadataAudit.metadataCompleteness * 100);
324
+ const color = completeness >= 80 ? chalk.green : (completeness >= 60 ? chalk.yellow : chalk.red);
325
+ this.log(` 📊 Metadata: ${color(`${completeness}% complete`)}`);
326
+ }
327
+ // Show git status
328
+ if (audit.gitAudit?.isGitRepo) {
329
+ const gitStatus = audit.gitAudit.status?.hasChanges
330
+ ? chalk.yellow('uncommitted changes')
331
+ : chalk.green('clean');
332
+ this.log(` 📁 Git: ${gitStatus}`);
333
+ }
334
+ this.log('');
335
+ }
336
+ displayRecommendations(results) {
337
+ const allRecommendations = [];
338
+ // Collect recommendations from all findings
339
+ for (const finding of results.findings) {
340
+ if (finding.fileAudit?.recommendations) {
341
+ allRecommendations.push(...finding.fileAudit.recommendations);
342
+ }
343
+ }
344
+ if (allRecommendations.length > 0) {
345
+ this.log(chalk.bold('💡 Recommendations:'));
346
+ const uniqueRecommendations = [...new Set(allRecommendations)];
347
+ for (const rec of uniqueRecommendations) {
348
+ this.log(` • ${rec}`);
349
+ }
350
+ }
351
+ }
352
+ getAllFiles(dirPath, maxDepth = 2) {
353
+ const files = [];
354
+ const traverse = (currentPath, depth) => {
355
+ if (depth > maxDepth)
356
+ return;
357
+ try {
358
+ const items = fs.readdirSync(currentPath);
359
+ for (const item of items) {
360
+ if (item.startsWith('.') && item !== '.entity.yaml')
361
+ continue;
362
+ const itemPath = path.join(currentPath, item);
363
+ const stats = fs.statSync(itemPath);
364
+ if (stats.isFile()) {
365
+ files.push(path.relative(dirPath, itemPath));
366
+ }
367
+ else if (stats.isDirectory()) {
368
+ traverse(itemPath, depth + 1);
369
+ }
370
+ }
371
+ }
372
+ catch {
373
+ // Ignore permission errors
374
+ }
375
+ };
376
+ traverse(dirPath, 0);
377
+ return files;
378
+ }
379
+ // Helper methods
380
+ getAuditTypes(flags) {
381
+ const types = [];
382
+ if (flags.security)
383
+ types.push('security');
384
+ if (flags.compliance)
385
+ types.push('compliance');
386
+ if (flags.access)
387
+ types.push('access');
388
+ if (flags.changes)
389
+ types.push('changes');
390
+ if (types.length === 0)
391
+ types.push('general');
392
+ return types;
393
+ }
394
+ getExpectedFiles(entityType) {
395
+ const common = [
396
+ { description: 'Entity metadata', name: '.entity.yaml', required: true },
397
+ { description: 'Documentation', name: 'README.md', required: false },
398
+ ];
399
+ const typeSpecific = {
400
+ client: [
401
+ { description: 'Project configuration', name: 'package.json', required: false },
402
+ { description: 'Git ignore rules', name: '.gitignore', required: true },
403
+ ],
404
+ system: [
405
+ { description: 'Project configuration', name: 'package.json', required: true },
406
+ { description: 'Git ignore rules', name: '.gitignore', required: true },
407
+ ],
408
+ };
409
+ return [...common, ...(typeSpecific[entityType] || [])];
410
+ }
411
+ getRequiredMetadataFields(entityType) {
412
+ const fields = {
413
+ client: ['business.primary_contact', 'business.status', 'relationships.dependent_systems'],
414
+ company: ['business.status'],
415
+ initiative: ['relationships.related_initiatives'],
416
+ prospect: ['business.status', 'relationships.similar_entities'],
417
+ system: ['relationships.dependent_systems'],
418
+ };
419
+ return fields[entityType] || [];
420
+ }
421
+ hasNestedField(obj, fieldPath) {
422
+ const parts = fieldPath.split('.');
423
+ let current = obj;
424
+ for (const part of parts) {
425
+ if (!current || typeof current !== 'object' || !(part in current)) {
426
+ return false;
427
+ }
428
+ current = current[part];
429
+ }
430
+ return current !== undefined && current !== null && current !== '';
431
+ }
432
+ hasSpecificAuditType(flags) {
433
+ return flags.security || flags.compliance || flags.access || flags.changes;
434
+ }
435
+ async performAccessAudit(entity) {
436
+ const issues = [];
437
+ // Check path accessibility
438
+ const entityPath = this.entityService.resolveEntityPath(entity);
439
+ try {
440
+ const stats = fs.statSync(entityPath);
441
+ // Check if path is accessible
442
+ if (!stats.isDirectory()) {
443
+ issues.push({
444
+ message: 'Entity path is not a directory',
445
+ severity: 'high',
446
+ type: 'invalid_path',
447
+ });
448
+ }
449
+ // Check permissions (basic check)
450
+ try {
451
+ fs.accessSync(entityPath, fs.constants.R_OK | fs.constants.W_OK);
452
+ }
453
+ catch {
454
+ issues.push({
455
+ message: 'Insufficient permissions for entity path',
456
+ severity: 'high',
457
+ type: 'permission_issue',
458
+ });
459
+ }
460
+ }
461
+ catch (error) {
462
+ const message = error instanceof Error ? error.message : String(error);
463
+ issues.push({
464
+ message: `Cannot access entity path: ${message}`,
465
+ severity: 'high',
466
+ type: 'access_error',
467
+ });
468
+ }
469
+ // Check for private/business separation
470
+ if (entity.type === 'client') {
471
+ const hasPrivate = entity.path.includes('private') || entity.path.includes('business');
472
+ const metadata = entity.metadata || {};
473
+ if (metadata.business && !hasPrivate) {
474
+ issues.push({
475
+ message: 'Client entity with business data should have private/business separation',
476
+ severity: 'medium',
477
+ type: 'missing_private_separation',
478
+ });
479
+ }
480
+ }
481
+ return issues;
482
+ }
483
+ async performAudit(entities, flags) {
484
+ const results = {
485
+ findings: [],
486
+ scope: {
487
+ auditTypes: this.getAuditTypes(flags),
488
+ entities: entities.map(e => e.name),
489
+ },
490
+ summary: {
491
+ accessIssues: 0,
492
+ complianceIssues: 0,
493
+ issuesFound: 0,
494
+ securityIssues: 0,
495
+ totalEntities: entities.length,
496
+ },
497
+ timestamp: new Date().toISOString(),
498
+ };
499
+ for (const entity of entities) {
500
+ const entityAudit = await this.auditEntity(entity, flags);
501
+ results.findings.push(entityAudit);
502
+ // Update summary
503
+ results.summary.issuesFound += entityAudit.issues.length;
504
+ results.summary.securityIssues += entityAudit.securityIssues.length;
505
+ results.summary.complianceIssues += entityAudit.complianceIssues.length;
506
+ results.summary.accessIssues += entityAudit.accessIssues.length;
507
+ }
508
+ return results;
509
+ }
510
+ async performComplianceAudit(entity) {
511
+ const issues = [];
512
+ const metadata = entity.metadata || {};
513
+ // Check business compliance for client entities
514
+ if (entity.type === 'client') {
515
+ if (!metadata.business?.status) {
516
+ issues.push({
517
+ message: 'Client entity missing business status',
518
+ severity: 'medium',
519
+ type: 'missing_business_status',
520
+ });
521
+ }
522
+ // Check for required documentation
523
+ const entityPath = this.entityService.resolveEntityPath(entity);
524
+ const requiredDocs = ['README.md', '.entity.yaml'];
525
+ for (const doc of requiredDocs) {
526
+ const docPath = path.join(entityPath, doc);
527
+ if (!fs.existsSync(docPath)) {
528
+ issues.push({
529
+ message: `Required documentation missing: ${doc}`,
530
+ severity: 'medium',
531
+ type: 'missing_documentation',
532
+ });
533
+ }
534
+ }
535
+ }
536
+ // Check metadata completeness
537
+ const completeness = this.calculateMetadataCompleteness(metadata, entity.type);
538
+ if (completeness < 0.7) {
539
+ issues.push({
540
+ message: `Entity metadata only ${Math.round(completeness * 100)}% complete`,
541
+ severity: 'low',
542
+ type: 'incomplete_metadata',
543
+ });
544
+ }
545
+ return issues;
546
+ }
547
+ async performSecurityAudit(entityPath) {
548
+ const issues = [];
549
+ // Check for sensitive files
550
+ const sensitivePatterns = [
551
+ '.env',
552
+ 'secrets',
553
+ 'private',
554
+ 'credentials',
555
+ 'password',
556
+ 'key',
557
+ 'token',
558
+ ];
559
+ try {
560
+ const files = this.getAllFiles(entityPath);
561
+ for (const file of files) {
562
+ const fileName = path.basename(file).toLowerCase();
563
+ for (const pattern of sensitivePatterns) {
564
+ if (fileName.includes(pattern)) {
565
+ issues.push({
566
+ file,
567
+ message: `Potentially sensitive file found: ${file}`,
568
+ severity: 'high',
569
+ type: 'sensitive_file',
570
+ });
571
+ }
572
+ }
573
+ }
574
+ // Check for .gitignore
575
+ const gitignorePath = path.join(entityPath, '.gitignore');
576
+ if (!fs.existsSync(gitignorePath)) {
577
+ issues.push({
578
+ message: 'No .gitignore file found',
579
+ severity: 'medium',
580
+ type: 'missing_gitignore',
581
+ });
582
+ }
583
+ }
584
+ catch (error) {
585
+ const message = error instanceof Error ? error.message : String(error);
586
+ issues.push({
587
+ message: `Security audit failed: ${message}`,
588
+ severity: 'high',
589
+ type: 'security_audit_error',
590
+ });
591
+ }
592
+ return issues;
593
+ }
594
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Back extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,29 @@
1
+ import chalk from 'chalk';
2
+ import { BaseCommand } from '../base-command.js';
3
+ /** Minimum history length required to have a previous focus entry */
4
+ const MIN_HISTORY_FOR_PREVIOUS = 2;
5
+ /** Index of the previous entry in the history array (0 is current, 1 is previous) */
6
+ const PREVIOUS_ENTRY_INDEX = 1;
7
+ export default class Back extends BaseCommand {
8
+ static description = 'Navigate back to the previous focus';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %>',
11
+ ];
12
+ async run() {
13
+ await this.parse(Back);
14
+ const focusedEntities = await this.focusService.getFocusedEntities();
15
+ const history = this.configService.getHistory();
16
+ if (history.length < MIN_HISTORY_FOR_PREVIOUS) {
17
+ this.log(chalk.yellow('No previous focus available'));
18
+ this.log(chalk.dim('Focus history is empty or has only the current focus'));
19
+ return;
20
+ }
21
+ const previousEntry = history[PREVIOUS_ENTRY_INDEX];
22
+ const currentEntities = focusedEntities.map(e => e.name).join(', ') || 'none';
23
+ const previousEntities = previousEntry.entities.join(', ');
24
+ await this.focusService.switchToPrevious();
25
+ this.log(chalk.green('✓ Switched to previous focus'));
26
+ this.log(chalk.dim(` From: ${currentEntities}`));
27
+ this.log(chalk.dim(` To: ${previousEntities}`));
28
+ }
29
+ }
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Checkout extends BaseCommand {
3
+ static args: {
4
+ branch: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ 'create-branch': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ from: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }