@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,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,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
|
+
}
|