@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.
- package/README.md +1048 -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 +18 -0
- package/dist/commands/entity/clone-all.js +166 -0
- package/dist/commands/entity/clone.d.ts +17 -0
- package/dist/commands/entity/clone.js +132 -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 +2006 -0
- package/package.json +11 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
export const DisplayUtils = {
|
|
5
|
+
createSpinner(text) {
|
|
6
|
+
return ora(text);
|
|
7
|
+
},
|
|
8
|
+
createTable(options) {
|
|
9
|
+
return new Table({
|
|
10
|
+
style: { border: [], head: [] },
|
|
11
|
+
...options,
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
error(message) {
|
|
15
|
+
console.log(chalk.red('✗'), message);
|
|
16
|
+
},
|
|
17
|
+
formatCommand(command) {
|
|
18
|
+
return chalk.cyan(`\`${command}\``);
|
|
19
|
+
},
|
|
20
|
+
formatDuration(ms) {
|
|
21
|
+
if (ms < 1000)
|
|
22
|
+
return `${ms}ms`;
|
|
23
|
+
if (ms < 60_000)
|
|
24
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
25
|
+
if (ms < 3_600_000)
|
|
26
|
+
return `${(ms / 60_000).toFixed(1)}m`;
|
|
27
|
+
return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
28
|
+
},
|
|
29
|
+
formatEntityType(type) {
|
|
30
|
+
const typeColors = {
|
|
31
|
+
delivery: chalk.blue,
|
|
32
|
+
module: chalk.green,
|
|
33
|
+
service: chalk.yellow,
|
|
34
|
+
tool: chalk.magenta,
|
|
35
|
+
};
|
|
36
|
+
const color = typeColors[type] || chalk.white;
|
|
37
|
+
return color(type);
|
|
38
|
+
},
|
|
39
|
+
formatGitStatus(status) {
|
|
40
|
+
const statusMap = {
|
|
41
|
+
'!': chalk.gray('!'), // Ignored
|
|
42
|
+
'?': chalk.gray('?'), // Untracked
|
|
43
|
+
A: chalk.green('A'), // Added
|
|
44
|
+
C: chalk.cyan('C'), // Copied
|
|
45
|
+
D: chalk.red('D'), // Deleted
|
|
46
|
+
M: chalk.yellow('M'), // Modified
|
|
47
|
+
R: chalk.blue('R'), // Renamed
|
|
48
|
+
U: chalk.magenta('U'), // Unmerged
|
|
49
|
+
};
|
|
50
|
+
return statusMap[status] || status;
|
|
51
|
+
},
|
|
52
|
+
formatList(items, maxItems = 5) {
|
|
53
|
+
if (items.length === 0)
|
|
54
|
+
return 'none';
|
|
55
|
+
if (items.length <= maxItems)
|
|
56
|
+
return items.join(', ');
|
|
57
|
+
const shown = items.slice(0, maxItems).join(', ');
|
|
58
|
+
const remaining = items.length - maxItems;
|
|
59
|
+
return `${shown}, and ${remaining} more`;
|
|
60
|
+
},
|
|
61
|
+
formatPath(path) {
|
|
62
|
+
return chalk.dim(path);
|
|
63
|
+
},
|
|
64
|
+
formatRelativeTime(date) {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const then = date.getTime();
|
|
67
|
+
const diff = now - then;
|
|
68
|
+
const seconds = Math.floor(diff / 1000);
|
|
69
|
+
const minutes = Math.floor(seconds / 60);
|
|
70
|
+
const hours = Math.floor(minutes / 60);
|
|
71
|
+
const days = Math.floor(hours / 24);
|
|
72
|
+
const weeks = Math.floor(days / 7);
|
|
73
|
+
const months = Math.floor(days / 30);
|
|
74
|
+
const years = Math.floor(days / 365);
|
|
75
|
+
if (years > 0)
|
|
76
|
+
return `${years} ${this.pluralize(years, 'year')} ago`;
|
|
77
|
+
if (months > 0)
|
|
78
|
+
return `${months} ${this.pluralize(months, 'month')} ago`;
|
|
79
|
+
if (weeks > 0)
|
|
80
|
+
return `${weeks} ${this.pluralize(weeks, 'week')} ago`;
|
|
81
|
+
if (days > 0)
|
|
82
|
+
return `${days} ${this.pluralize(days, 'day')} ago`;
|
|
83
|
+
if (hours > 0)
|
|
84
|
+
return `${hours} ${this.pluralize(hours, 'hour')} ago`;
|
|
85
|
+
if (minutes > 0)
|
|
86
|
+
return `${minutes} ${this.pluralize(minutes, 'minute')} ago`;
|
|
87
|
+
return 'just now';
|
|
88
|
+
},
|
|
89
|
+
formatSize(bytes) {
|
|
90
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
91
|
+
let size = bytes;
|
|
92
|
+
let unitIndex = 0;
|
|
93
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
94
|
+
size /= 1024;
|
|
95
|
+
unitIndex++;
|
|
96
|
+
}
|
|
97
|
+
return `${size.toFixed(1)}${units[unitIndex]}`;
|
|
98
|
+
},
|
|
99
|
+
formatTimestamp(date) {
|
|
100
|
+
return date.toLocaleString();
|
|
101
|
+
},
|
|
102
|
+
highlight(text, pattern) {
|
|
103
|
+
const regex = typeof pattern === 'string'
|
|
104
|
+
? new RegExp(pattern.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`), 'gi')
|
|
105
|
+
: pattern;
|
|
106
|
+
return text.replace(regex, match => chalk.yellow(match));
|
|
107
|
+
},
|
|
108
|
+
info(message) {
|
|
109
|
+
console.log(chalk.blue('ℹ'), message);
|
|
110
|
+
},
|
|
111
|
+
pluralize(count, singular, plural) {
|
|
112
|
+
return count === 1 ? singular : (plural || `${singular}s`);
|
|
113
|
+
},
|
|
114
|
+
printFooter(message, width = 50) {
|
|
115
|
+
console.log(chalk.dim('─'.repeat(width)));
|
|
116
|
+
if (message) {
|
|
117
|
+
console.log(chalk.dim(message));
|
|
118
|
+
}
|
|
119
|
+
console.log('');
|
|
120
|
+
},
|
|
121
|
+
printHeader(title, width = 50) {
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(chalk.bold(title));
|
|
124
|
+
console.log(chalk.dim('─'.repeat(width)));
|
|
125
|
+
},
|
|
126
|
+
progressBar(current, total, width = 20) {
|
|
127
|
+
const percentage = Math.min(100, Math.floor((current / total) * 100));
|
|
128
|
+
const filled = Math.floor((percentage / 100) * width);
|
|
129
|
+
const empty = width - filled;
|
|
130
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
131
|
+
return `${bar} ${percentage}%`;
|
|
132
|
+
},
|
|
133
|
+
success(message) {
|
|
134
|
+
console.log(chalk.green('✓'), message);
|
|
135
|
+
},
|
|
136
|
+
truncate(str, maxLength) {
|
|
137
|
+
if (str.length <= maxLength)
|
|
138
|
+
return str;
|
|
139
|
+
return str.slice(0, Math.max(0, maxLength - 3)) + '...';
|
|
140
|
+
},
|
|
141
|
+
warning(message) {
|
|
142
|
+
console.log(chalk.yellow('⚠'), message);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
export default DisplayUtils;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const FileSystemUtils: {
|
|
2
|
+
copyDir(src: string, dest: string): Promise<void>;
|
|
3
|
+
copyFile(src: string, dest: string): Promise<void>;
|
|
4
|
+
createSymlink(target: string, linkPath: string): Promise<void>;
|
|
5
|
+
ensureDir(dirPath: string): Promise<void>;
|
|
6
|
+
exists(filePath: string): Promise<boolean>;
|
|
7
|
+
findFiles(dirPath: string, pattern: RegExp | string, options?: {
|
|
8
|
+
excludeDirs?: string[];
|
|
9
|
+
maxDepth?: number;
|
|
10
|
+
recursive?: boolean;
|
|
11
|
+
}): Promise<string[]>;
|
|
12
|
+
getBasename(filePath: string, ext?: string): string;
|
|
13
|
+
getCreatedTime(filePath: string): Promise<Date>;
|
|
14
|
+
getDirname(filePath: string): string;
|
|
15
|
+
getExtension(filePath: string): string;
|
|
16
|
+
getModifiedTime(filePath: string): Promise<Date>;
|
|
17
|
+
getSize(filePath: string): Promise<number>;
|
|
18
|
+
globToRegex(glob: string): RegExp;
|
|
19
|
+
isDirectory(filePath: string): Promise<boolean>;
|
|
20
|
+
isFile(filePath: string): Promise<boolean>;
|
|
21
|
+
isSymlink(filePath: string): Promise<boolean>;
|
|
22
|
+
joinPath(...paths: string[]): string;
|
|
23
|
+
makeExecutable(filePath: string): Promise<void>;
|
|
24
|
+
readJSON<T = unknown>(filePath: string): Promise<T>;
|
|
25
|
+
readSymlink(filePath: string): Promise<string>;
|
|
26
|
+
relativePath(from: string, to: string): string;
|
|
27
|
+
removeDir(dirPath: string): Promise<void>;
|
|
28
|
+
resolvePath(...paths: string[]): string;
|
|
29
|
+
touch(filePath: string): Promise<void>;
|
|
30
|
+
writeJSON(filePath: string, data: unknown, pretty?: boolean): Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
export default FileSystemUtils;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const { mkdir, readdir, readFile, rmdir, stat, unlink, writeFile } = fs.promises;
|
|
4
|
+
export const FileSystemUtils = {
|
|
5
|
+
async copyDir(src, dest) {
|
|
6
|
+
await this.ensureDir(dest);
|
|
7
|
+
const files = await readdir(src);
|
|
8
|
+
for (const file of files) {
|
|
9
|
+
const srcPath = path.join(src, file);
|
|
10
|
+
const destPath = path.join(dest, file);
|
|
11
|
+
const fileStat = await stat(srcPath);
|
|
12
|
+
await (fileStat.isDirectory() ? this.copyDir(srcPath, destPath) : this.copyFile(srcPath, destPath));
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
async copyFile(src, dest) {
|
|
16
|
+
const destDir = path.dirname(dest);
|
|
17
|
+
await this.ensureDir(destDir);
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const readStream = fs.createReadStream(src);
|
|
20
|
+
const writeStream = fs.createWriteStream(dest);
|
|
21
|
+
readStream.on('error', reject);
|
|
22
|
+
writeStream.on('error', reject);
|
|
23
|
+
writeStream.on('finish', resolve);
|
|
24
|
+
readStream.pipe(writeStream);
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
async createSymlink(target, linkPath) {
|
|
28
|
+
await this.ensureDir(path.dirname(linkPath));
|
|
29
|
+
await fs.promises.symlink(target, linkPath);
|
|
30
|
+
},
|
|
31
|
+
async ensureDir(dirPath) {
|
|
32
|
+
try {
|
|
33
|
+
await mkdir(dirPath, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof Error && 'code' in error && error.code !== 'EEXIST') {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async exists(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
await stat(filePath);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async findFiles(dirPath, pattern, options = {}) {
|
|
51
|
+
const { excludeDirs = ['node_modules', '.git', 'dist', 'build'], maxDepth = 10, recursive = true, } = options;
|
|
52
|
+
const results = [];
|
|
53
|
+
const regex = typeof pattern === 'string'
|
|
54
|
+
? new RegExp(pattern.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`))
|
|
55
|
+
: pattern;
|
|
56
|
+
const search = async (currentPath, depth) => {
|
|
57
|
+
if (depth > maxDepth)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
const files = await readdir(currentPath);
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const filePath = path.join(currentPath, file);
|
|
63
|
+
const fileStat = await stat(filePath);
|
|
64
|
+
if (fileStat.isDirectory()) {
|
|
65
|
+
if (!excludeDirs.includes(file) && recursive) {
|
|
66
|
+
await search(filePath, depth + 1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (regex.test(file)) {
|
|
70
|
+
results.push(filePath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Ignore permission errors
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
await search(dirPath, 0);
|
|
79
|
+
return results;
|
|
80
|
+
},
|
|
81
|
+
getBasename(filePath, ext) {
|
|
82
|
+
return path.basename(filePath, ext);
|
|
83
|
+
},
|
|
84
|
+
async getCreatedTime(filePath) {
|
|
85
|
+
const fileStat = await stat(filePath);
|
|
86
|
+
return fileStat.birthtime;
|
|
87
|
+
},
|
|
88
|
+
getDirname(filePath) {
|
|
89
|
+
return path.dirname(filePath);
|
|
90
|
+
},
|
|
91
|
+
getExtension(filePath) {
|
|
92
|
+
return path.extname(filePath);
|
|
93
|
+
},
|
|
94
|
+
async getModifiedTime(filePath) {
|
|
95
|
+
const fileStat = await stat(filePath);
|
|
96
|
+
return fileStat.mtime;
|
|
97
|
+
},
|
|
98
|
+
async getSize(filePath) {
|
|
99
|
+
const fileStat = await stat(filePath);
|
|
100
|
+
if (fileStat.isFile()) {
|
|
101
|
+
return fileStat.size;
|
|
102
|
+
}
|
|
103
|
+
if (fileStat.isDirectory()) {
|
|
104
|
+
let totalSize = 0;
|
|
105
|
+
const files = await readdir(filePath);
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const childPath = path.join(filePath, file);
|
|
108
|
+
totalSize += await this.getSize(childPath);
|
|
109
|
+
}
|
|
110
|
+
return totalSize;
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
},
|
|
114
|
+
globToRegex(glob) {
|
|
115
|
+
const escaped = glob
|
|
116
|
+
.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`)
|
|
117
|
+
.replaceAll(String.raw `\*`, '.*')
|
|
118
|
+
.replaceAll(String.raw `\?`, '.');
|
|
119
|
+
return new RegExp(`^${escaped}$`);
|
|
120
|
+
},
|
|
121
|
+
async isDirectory(filePath) {
|
|
122
|
+
try {
|
|
123
|
+
const fileStat = await stat(filePath);
|
|
124
|
+
return fileStat.isDirectory();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
async isFile(filePath) {
|
|
131
|
+
try {
|
|
132
|
+
const fileStat = await stat(filePath);
|
|
133
|
+
return fileStat.isFile();
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async isSymlink(filePath) {
|
|
140
|
+
try {
|
|
141
|
+
const fileStat = await fs.promises.lstat(filePath);
|
|
142
|
+
return fileStat.isSymbolicLink();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
joinPath(...paths) {
|
|
149
|
+
return path.join(...paths);
|
|
150
|
+
},
|
|
151
|
+
async makeExecutable(filePath) {
|
|
152
|
+
const currentMode = (await stat(filePath)).mode;
|
|
153
|
+
const newMode = currentMode | 0o111; // Add execute permission
|
|
154
|
+
await fs.promises.chmod(filePath, newMode);
|
|
155
|
+
},
|
|
156
|
+
async readJSON(filePath) {
|
|
157
|
+
const content = await readFile(filePath, 'utf8');
|
|
158
|
+
return JSON.parse(content);
|
|
159
|
+
},
|
|
160
|
+
async readSymlink(filePath) {
|
|
161
|
+
return fs.promises.readlink(filePath);
|
|
162
|
+
},
|
|
163
|
+
relativePath(from, to) {
|
|
164
|
+
return path.relative(from, to);
|
|
165
|
+
},
|
|
166
|
+
async removeDir(dirPath) {
|
|
167
|
+
if (!fs.existsSync(dirPath))
|
|
168
|
+
return;
|
|
169
|
+
const files = await readdir(dirPath);
|
|
170
|
+
for (const file of files) {
|
|
171
|
+
const filePath = path.join(dirPath, file);
|
|
172
|
+
const fileStat = await stat(filePath);
|
|
173
|
+
await (fileStat.isDirectory() ? this.removeDir(filePath) : unlink(filePath));
|
|
174
|
+
}
|
|
175
|
+
await rmdir(dirPath);
|
|
176
|
+
},
|
|
177
|
+
resolvePath(...paths) {
|
|
178
|
+
return path.resolve(...paths);
|
|
179
|
+
},
|
|
180
|
+
async touch(filePath) {
|
|
181
|
+
const now = new Date();
|
|
182
|
+
try {
|
|
183
|
+
await fs.promises.utimes(filePath, now, now);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
const handle = await fs.promises.open(filePath, 'w');
|
|
187
|
+
await handle.close();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
async writeJSON(filePath, data, pretty = true) {
|
|
191
|
+
const content = pretty
|
|
192
|
+
? JSON.stringify(data, null, 2)
|
|
193
|
+
: JSON.stringify(data);
|
|
194
|
+
await this.ensureDir(path.dirname(filePath));
|
|
195
|
+
await writeFile(filePath, content, 'utf8');
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
export default FileSystemUtils;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './display.js';
|
|
2
|
+
export * from './filesystem.js';
|
|
3
|
+
export * from './validation.js';
|
|
4
|
+
import DisplayUtilsDefault from './display.js';
|
|
5
|
+
import FileSystemUtilsDefault from './filesystem.js';
|
|
6
|
+
import ValidationUtilsDefault from './validation.js';
|
|
7
|
+
export interface UtilsBundle {
|
|
8
|
+
display: typeof DisplayUtilsDefault;
|
|
9
|
+
filesystem: typeof FileSystemUtilsDefault;
|
|
10
|
+
validation: typeof ValidationUtilsDefault;
|
|
11
|
+
}
|
|
12
|
+
declare const utils: UtilsBundle;
|
|
13
|
+
export default utils;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Re-export all named exports from each module (includes DisplayUtils, FileSystemUtils, ValidationUtils)
|
|
2
|
+
export * from './display.js';
|
|
3
|
+
export * from './filesystem.js';
|
|
4
|
+
export * from './validation.js';
|
|
5
|
+
// Also provide a combined default export for convenience
|
|
6
|
+
import DisplayUtilsDefault from './display.js';
|
|
7
|
+
import FileSystemUtilsDefault from './filesystem.js';
|
|
8
|
+
import ValidationUtilsDefault from './validation.js';
|
|
9
|
+
const utils = {
|
|
10
|
+
display: DisplayUtilsDefault,
|
|
11
|
+
filesystem: FileSystemUtilsDefault,
|
|
12
|
+
validation: ValidationUtilsDefault,
|
|
13
|
+
};
|
|
14
|
+
export default utils;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const ValidationUtils: {
|
|
2
|
+
detectProjectType(dirPath: string): null | string;
|
|
3
|
+
hasUncommittedChanges(dirPath: string): boolean;
|
|
4
|
+
isEmptyDirectory(dirPath: string): boolean;
|
|
5
|
+
isGitRepository(dirPath: string): boolean;
|
|
6
|
+
isGoProject(dirPath: string): boolean;
|
|
7
|
+
isJavaProject(dirPath: string): boolean;
|
|
8
|
+
isNodeProject(dirPath: string): boolean;
|
|
9
|
+
isPythonProject(dirPath: string): boolean;
|
|
10
|
+
isRustProject(dirPath: string): boolean;
|
|
11
|
+
isValidBranch(branch: string): boolean;
|
|
12
|
+
isValidEntityName(name: string): boolean;
|
|
13
|
+
isValidEntityType(type: string): boolean;
|
|
14
|
+
isValidGitUrl(url: string): boolean;
|
|
15
|
+
isValidPath(inputPath: string): boolean;
|
|
16
|
+
sanitizeGitUrl(url: string): string;
|
|
17
|
+
sanitizePath(inputPath: string): string;
|
|
18
|
+
validateConfig(config: Record<string, unknown>): string[];
|
|
19
|
+
validateEntityData(data: Record<string, unknown>): string[];
|
|
20
|
+
validateFocusEntities(entities: string[], availableEntities: string[]): string[];
|
|
21
|
+
};
|
|
22
|
+
export default ValidationUtils;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const ValidationUtils = {
|
|
5
|
+
detectProjectType(dirPath) {
|
|
6
|
+
if (this.isNodeProject(dirPath))
|
|
7
|
+
return 'node';
|
|
8
|
+
if (this.isPythonProject(dirPath))
|
|
9
|
+
return 'python';
|
|
10
|
+
if (this.isGoProject(dirPath))
|
|
11
|
+
return 'go';
|
|
12
|
+
if (this.isRustProject(dirPath))
|
|
13
|
+
return 'rust';
|
|
14
|
+
if (this.isJavaProject(dirPath))
|
|
15
|
+
return 'java';
|
|
16
|
+
return null;
|
|
17
|
+
},
|
|
18
|
+
hasUncommittedChanges(dirPath) {
|
|
19
|
+
if (!this.isGitRepository(dirPath)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const status = execSync('git status --porcelain', {
|
|
24
|
+
cwd: dirPath,
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: 'pipe',
|
|
27
|
+
});
|
|
28
|
+
return status.trim().length > 0;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
isEmptyDirectory(dirPath) {
|
|
35
|
+
try {
|
|
36
|
+
const files = fs.readdirSync(dirPath);
|
|
37
|
+
return files.length === 0
|
|
38
|
+
|| (files.length === 1 && files[0] === '.git');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
isGitRepository(dirPath) {
|
|
45
|
+
return fs.existsSync(path.join(dirPath, '.git'));
|
|
46
|
+
},
|
|
47
|
+
isGoProject(dirPath) {
|
|
48
|
+
return fs.existsSync(path.join(dirPath, 'go.mod'));
|
|
49
|
+
},
|
|
50
|
+
isJavaProject(dirPath) {
|
|
51
|
+
return fs.existsSync(path.join(dirPath, 'pom.xml'))
|
|
52
|
+
|| fs.existsSync(path.join(dirPath, 'build.gradle'))
|
|
53
|
+
|| fs.existsSync(path.join(dirPath, 'build.gradle.kts'));
|
|
54
|
+
},
|
|
55
|
+
isNodeProject(dirPath) {
|
|
56
|
+
return fs.existsSync(path.join(dirPath, 'package.json'));
|
|
57
|
+
},
|
|
58
|
+
isPythonProject(dirPath) {
|
|
59
|
+
return fs.existsSync(path.join(dirPath, 'requirements.txt'))
|
|
60
|
+
|| fs.existsSync(path.join(dirPath, 'setup.py'))
|
|
61
|
+
|| fs.existsSync(path.join(dirPath, 'pyproject.toml'));
|
|
62
|
+
},
|
|
63
|
+
isRustProject(dirPath) {
|
|
64
|
+
return fs.existsSync(path.join(dirPath, 'Cargo.toml'));
|
|
65
|
+
},
|
|
66
|
+
isValidBranch(branch) {
|
|
67
|
+
// Git branch naming rules
|
|
68
|
+
const invalidPatterns = [
|
|
69
|
+
/^\./, // Cannot start with .
|
|
70
|
+
/\.$/, // Cannot end with .
|
|
71
|
+
/\.\./, // Cannot contain ..
|
|
72
|
+
/\/\//, // Cannot contain //
|
|
73
|
+
/\s/, // Cannot contain spaces
|
|
74
|
+
/[\u0000-\u001F\u007F]/, // Cannot contain control characters
|
|
75
|
+
/[~^:?*[\\]/, // Cannot contain special characters
|
|
76
|
+
];
|
|
77
|
+
return !invalidPatterns.some(pattern => pattern.test(branch))
|
|
78
|
+
&& branch.length > 0
|
|
79
|
+
&& branch.length <= 255;
|
|
80
|
+
},
|
|
81
|
+
isValidEntityName(name) {
|
|
82
|
+
// Entity names should be alphanumeric with hyphens/underscores
|
|
83
|
+
const pattern = /^[a-zA-Z0-9]([a-zA-Z0-9-_]*[a-zA-Z0-9])?$/;
|
|
84
|
+
return pattern.test(name) && name.length <= 50;
|
|
85
|
+
},
|
|
86
|
+
isValidEntityType(type) {
|
|
87
|
+
const validTypes = ['delivery', 'module', 'service', 'tool'];
|
|
88
|
+
return validTypes.includes(type);
|
|
89
|
+
},
|
|
90
|
+
isValidGitUrl(url) {
|
|
91
|
+
const patterns = [
|
|
92
|
+
/^https?:\/\/.+\.git$/,
|
|
93
|
+
/^git@.+:.+\.git$/,
|
|
94
|
+
/^ssh:\/\/.+\.git$/,
|
|
95
|
+
/^https:\/\/github\.com\/.+\/.+$/,
|
|
96
|
+
/^https:\/\/gitlab\.com\/.+\/.+$/,
|
|
97
|
+
/^https:\/\/bitbucket\.org\/.+\/.+$/,
|
|
98
|
+
];
|
|
99
|
+
return patterns.some(pattern => pattern.test(url));
|
|
100
|
+
},
|
|
101
|
+
isValidPath(inputPath) {
|
|
102
|
+
try {
|
|
103
|
+
// Check if path is valid
|
|
104
|
+
path.parse(inputPath);
|
|
105
|
+
// Check for dangerous patterns
|
|
106
|
+
const dangerous = ['..', '~', '$', '`', '|', '&', ';', '>', '<'];
|
|
107
|
+
return !dangerous.some(pattern => inputPath.includes(pattern));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
sanitizeGitUrl(url) {
|
|
114
|
+
// Ensure .git extension for common providers
|
|
115
|
+
if (!url.endsWith('.git')) {
|
|
116
|
+
const providers = ['github.com', 'gitlab.com', 'bitbucket.org'];
|
|
117
|
+
if (providers.some(p => url.includes(p))) {
|
|
118
|
+
url += '.git';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return url;
|
|
122
|
+
},
|
|
123
|
+
sanitizePath(inputPath) {
|
|
124
|
+
// Remove trailing slashes
|
|
125
|
+
let sanitized = inputPath.replace(/\/+$/, '');
|
|
126
|
+
// Normalize path separators
|
|
127
|
+
sanitized = sanitized.replaceAll('\\', '/');
|
|
128
|
+
// Remove redundant slashes
|
|
129
|
+
sanitized = sanitized.replaceAll(/\/+/g, '/');
|
|
130
|
+
return sanitized;
|
|
131
|
+
},
|
|
132
|
+
validateConfig(config) {
|
|
133
|
+
const errors = [];
|
|
134
|
+
if (!config.workspace) {
|
|
135
|
+
errors.push('Workspace configuration is missing');
|
|
136
|
+
}
|
|
137
|
+
if (!config.entities || typeof config.entities !== 'object') {
|
|
138
|
+
errors.push('Entities configuration is invalid');
|
|
139
|
+
}
|
|
140
|
+
if (config.focus) {
|
|
141
|
+
const focus = config.focus;
|
|
142
|
+
if (!Array.isArray(focus.entities)) {
|
|
143
|
+
errors.push('Focus entities must be an array');
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(focus.history)) {
|
|
146
|
+
errors.push('Focus history must be an array');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return errors;
|
|
150
|
+
},
|
|
151
|
+
validateEntityData(data) {
|
|
152
|
+
const errors = [];
|
|
153
|
+
if (!data.name) {
|
|
154
|
+
errors.push('Entity name is required');
|
|
155
|
+
}
|
|
156
|
+
else if (typeof data.name !== 'string' || !this.isValidEntityName(data.name)) {
|
|
157
|
+
errors.push('Invalid entity name format');
|
|
158
|
+
}
|
|
159
|
+
if (!data.type) {
|
|
160
|
+
errors.push('Entity type is required');
|
|
161
|
+
}
|
|
162
|
+
else if (typeof data.type !== 'string' || !this.isValidEntityType(data.type)) {
|
|
163
|
+
errors.push('Invalid entity type');
|
|
164
|
+
}
|
|
165
|
+
if (!data.path) {
|
|
166
|
+
errors.push('Entity path is required');
|
|
167
|
+
}
|
|
168
|
+
else if (typeof data.path !== 'string' || !this.isValidPath(data.path)) {
|
|
169
|
+
errors.push('Invalid entity path');
|
|
170
|
+
}
|
|
171
|
+
if (data.repository && (typeof data.repository !== 'string' || !this.isValidGitUrl(data.repository))) {
|
|
172
|
+
errors.push('Invalid repository URL');
|
|
173
|
+
}
|
|
174
|
+
return errors;
|
|
175
|
+
},
|
|
176
|
+
validateFocusEntities(entities, availableEntities) {
|
|
177
|
+
const errors = [];
|
|
178
|
+
if (entities.length === 0) {
|
|
179
|
+
errors.push('At least one entity must be specified');
|
|
180
|
+
}
|
|
181
|
+
const unavailable = entities.filter(e => !availableEntities.includes(e));
|
|
182
|
+
if (unavailable.length > 0) {
|
|
183
|
+
errors.push(`Unknown entities: ${unavailable.join(', ')}`);
|
|
184
|
+
}
|
|
185
|
+
const duplicates = entities.filter((e, i) => entities.indexOf(e) !== i);
|
|
186
|
+
if (duplicates.length > 0) {
|
|
187
|
+
errors.push(`Duplicate entities: ${[...new Set(duplicates)].join(', ')}`);
|
|
188
|
+
}
|
|
189
|
+
return errors;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
export default ValidationUtils;
|