@agents-at-scale/ark 0.1.31
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 +95 -0
- package/dist/commands/cluster/get-ip.d.ts +2 -0
- package/dist/commands/cluster/get-ip.js +32 -0
- package/dist/commands/cluster/get-type.d.ts +2 -0
- package/dist/commands/cluster/get-type.js +26 -0
- package/dist/commands/cluster/index.d.ts +2 -0
- package/dist/commands/cluster/index.js +10 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +108 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +327 -0
- package/dist/commands/generate/config.d.ts +145 -0
- package/dist/commands/generate/config.js +253 -0
- package/dist/commands/generate/generators/agent.d.ts +2 -0
- package/dist/commands/generate/generators/agent.js +156 -0
- package/dist/commands/generate/generators/index.d.ts +6 -0
- package/dist/commands/generate/generators/index.js +6 -0
- package/dist/commands/generate/generators/marketplace.d.ts +2 -0
- package/dist/commands/generate/generators/marketplace.js +304 -0
- package/dist/commands/generate/generators/mcpserver.d.ts +25 -0
- package/dist/commands/generate/generators/mcpserver.js +350 -0
- package/dist/commands/generate/generators/project.d.ts +2 -0
- package/dist/commands/generate/generators/project.js +784 -0
- package/dist/commands/generate/generators/query.d.ts +2 -0
- package/dist/commands/generate/generators/query.js +213 -0
- package/dist/commands/generate/generators/team.d.ts +2 -0
- package/dist/commands/generate/generators/team.js +407 -0
- package/dist/commands/generate/index.d.ts +24 -0
- package/dist/commands/generate/index.js +357 -0
- package/dist/commands/generate/templateDiscovery.d.ts +30 -0
- package/dist/commands/generate/templateDiscovery.js +94 -0
- package/dist/commands/generate/templateEngine.d.ts +78 -0
- package/dist/commands/generate/templateEngine.js +368 -0
- package/dist/commands/generate/utils/nameUtils.d.ts +35 -0
- package/dist/commands/generate/utils/nameUtils.js +110 -0
- package/dist/commands/generate/utils/projectUtils.d.ts +28 -0
- package/dist/commands/generate/utils/projectUtils.js +133 -0
- package/dist/components/DashboardCLI.d.ts +3 -0
- package/dist/components/DashboardCLI.js +149 -0
- package/dist/components/GeneratorUI.d.ts +3 -0
- package/dist/components/GeneratorUI.js +167 -0
- package/dist/components/statusChecker.d.ts +48 -0
- package/dist/components/statusChecker.js +251 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +243 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -0
- package/dist/lib/arkClient.d.ts +32 -0
- package/dist/lib/arkClient.js +43 -0
- package/dist/lib/cluster.d.ts +8 -0
- package/dist/lib/cluster.js +134 -0
- package/dist/lib/config.d.ts +82 -0
- package/dist/lib/config.js +223 -0
- package/dist/lib/consts.d.ts +10 -0
- package/dist/lib/consts.js +15 -0
- package/dist/lib/errors.d.ts +56 -0
- package/dist/lib/errors.js +208 -0
- package/dist/lib/exec.d.ts +5 -0
- package/dist/lib/exec.js +20 -0
- package/dist/lib/gatewayManager.d.ts +24 -0
- package/dist/lib/gatewayManager.js +85 -0
- package/dist/lib/kubernetes.d.ts +28 -0
- package/dist/lib/kubernetes.js +122 -0
- package/dist/lib/progress.d.ts +128 -0
- package/dist/lib/progress.js +273 -0
- package/dist/lib/security.d.ts +37 -0
- package/dist/lib/security.js +295 -0
- package/dist/lib/types.d.ts +37 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/wrappers/git.d.ts +2 -0
- package/dist/lib/wrappers/git.js +43 -0
- package/dist/ui/MainMenu.d.ts +3 -0
- package/dist/ui/MainMenu.js +116 -0
- package/dist/ui/statusFormatter.d.ts +9 -0
- package/dist/ui/statusFormatter.js +47 -0
- package/package.json +62 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { SecurityUtils } from '../../lib/security.js';
|
|
5
|
+
import { TemplateError } from '../../lib/errors.js';
|
|
6
|
+
export class TemplateEngine {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.variables = {};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Set template variables for substitution
|
|
12
|
+
*/
|
|
13
|
+
setVariables(variables) {
|
|
14
|
+
this.variables = { ...this.variables, ...variables };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get current template variables
|
|
18
|
+
*/
|
|
19
|
+
getVariables() {
|
|
20
|
+
return { ...this.variables };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Process a template directory and copy it to destination
|
|
24
|
+
*/
|
|
25
|
+
async processTemplate(templatePath, destinationPath, options = {}) {
|
|
26
|
+
const { skipIfExists = false, createDirectories = true, exclude = ['.git', 'node_modules', '.DS_Store'], include = [], } = options;
|
|
27
|
+
// Validate paths for security
|
|
28
|
+
SecurityUtils.validatePath(templatePath, 'template path');
|
|
29
|
+
SecurityUtils.validatePath(destinationPath, 'destination path');
|
|
30
|
+
if (!fs.existsSync(templatePath)) {
|
|
31
|
+
throw new TemplateError(`Template path does not exist: ${templatePath}`, templatePath, [
|
|
32
|
+
'Check that the template directory exists',
|
|
33
|
+
'Verify the template path is correct',
|
|
34
|
+
'Ensure templates are properly installed',
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
if (createDirectories) {
|
|
38
|
+
await this.ensureDirectorySafe(destinationPath, destinationPath);
|
|
39
|
+
}
|
|
40
|
+
await this.copyDirectory(templatePath, destinationPath, {
|
|
41
|
+
skipIfExists,
|
|
42
|
+
exclude,
|
|
43
|
+
include,
|
|
44
|
+
baseDir: destinationPath,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Process a single template file
|
|
49
|
+
*/
|
|
50
|
+
async processFile(templateFilePath, destinationFilePath, options = {}) {
|
|
51
|
+
const { skipIfExists = false, baseDir = process.cwd() } = options;
|
|
52
|
+
// Validate paths for security
|
|
53
|
+
SecurityUtils.validatePath(templateFilePath, 'template file path');
|
|
54
|
+
SecurityUtils.validatePath(destinationFilePath, 'destination file path');
|
|
55
|
+
SecurityUtils.validateOutputPath(destinationFilePath, baseDir);
|
|
56
|
+
if (skipIfExists && fs.existsSync(destinationFilePath)) {
|
|
57
|
+
console.log(chalk.yellow(`⏭️ Skipping existing file: ${destinationFilePath}`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let templateContent;
|
|
61
|
+
try {
|
|
62
|
+
templateContent = fs.readFileSync(templateFilePath, 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new TemplateError(`Failed to read template file: ${templateFilePath}. ${error instanceof Error ? error.message : String(error)}`, templateFilePath, [
|
|
66
|
+
'Check that the template file exists',
|
|
67
|
+
'Verify file permissions',
|
|
68
|
+
'Ensure the template path is correct',
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
// Validate template content for security
|
|
72
|
+
SecurityUtils.validateTemplateContent(templateContent, templateFilePath);
|
|
73
|
+
const processedContent = this.substituteVariables(templateContent);
|
|
74
|
+
// Ensure destination directory exists
|
|
75
|
+
const destinationDir = path.dirname(destinationFilePath);
|
|
76
|
+
await this.ensureDirectorySafe(destinationDir, baseDir);
|
|
77
|
+
// Write file securely
|
|
78
|
+
await SecurityUtils.writeFileSafe(destinationFilePath, processedContent, baseDir);
|
|
79
|
+
// Show important generated content with relative paths
|
|
80
|
+
if (this.isImportantContent(destinationFilePath)) {
|
|
81
|
+
const relativePath = this.getRelativePath(destinationFilePath);
|
|
82
|
+
console.log(chalk.green(`📝 ${relativePath}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Substitute template variables in content
|
|
87
|
+
*/
|
|
88
|
+
substituteVariables(content) {
|
|
89
|
+
let result = content;
|
|
90
|
+
// Replace template variables in Golang template format {{ .Values.variableName }}
|
|
91
|
+
for (const [key, value] of Object.entries(this.variables)) {
|
|
92
|
+
// Escape regex metacharacters in key to prevent ReDoS attacks
|
|
93
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
const regex = new RegExp(`{{\\s*\\.Values\\.${escapedKey}\\s*}}`, 'g');
|
|
95
|
+
result = result.replace(regex, String(value));
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Substitute variables in file/directory names
|
|
101
|
+
*/
|
|
102
|
+
substituteVariablesInPath(pathStr) {
|
|
103
|
+
let result = pathStr;
|
|
104
|
+
// Handle new .template.yaml naming convention
|
|
105
|
+
if (pathStr.endsWith('.template.yaml')) {
|
|
106
|
+
const templateName = path.basename(pathStr, '.template.yaml');
|
|
107
|
+
result = this.deriveDestinationFilename(templateName);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Use Golang template variable substitution for other files
|
|
111
|
+
for (const [key, value] of Object.entries(this.variables)) {
|
|
112
|
+
const regex = new RegExp(`{{\\s*\\.Values\\.${key}\\s*}}`, 'g');
|
|
113
|
+
result = result.replace(regex, String(value));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Derive destination filename from template name
|
|
120
|
+
*/
|
|
121
|
+
deriveDestinationFilename(templateName) {
|
|
122
|
+
switch (templateName) {
|
|
123
|
+
case 'agent':
|
|
124
|
+
return `${this.variables.agentName || 'unnamed'}-agent.yaml`;
|
|
125
|
+
case 'team':
|
|
126
|
+
return `${this.variables.teamName || 'unnamed'}-team.yaml`;
|
|
127
|
+
case 'query':
|
|
128
|
+
return `${this.variables.queryName || 'unnamed'}-query.yaml`;
|
|
129
|
+
default: {
|
|
130
|
+
// For unknown template types, use the template name as-is with variables
|
|
131
|
+
let result = `${templateName}.yaml`;
|
|
132
|
+
for (const [key, value] of Object.entries(this.variables)) {
|
|
133
|
+
const regex = new RegExp(`{{\\s*\\.Values\\.${key}\\s*}}`, 'g');
|
|
134
|
+
result = result.replace(regex, String(value));
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Copy directory recursively with template processing
|
|
142
|
+
*/
|
|
143
|
+
async copyDirectory(sourcePath, destinationPath, options) {
|
|
144
|
+
const { baseDir = destinationPath } = options;
|
|
145
|
+
const entries = fs.readdirSync(sourcePath, { withFileTypes: true });
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (!this.shouldProcessEntry(entry.name, options)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const sourceEntry = path.join(sourcePath, entry.name);
|
|
151
|
+
const destinationName = this.substituteVariablesInPath(entry.name);
|
|
152
|
+
const destinationEntry = path.join(destinationPath, destinationName);
|
|
153
|
+
if (entry.isDirectory()) {
|
|
154
|
+
await this.processDirectory(sourceEntry, destinationEntry, options, baseDir);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
await this.processFileEntry(sourceEntry, destinationEntry, destinationName, options, baseDir);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
shouldProcessEntry(entryName, options) {
|
|
162
|
+
if (this.shouldExclude(entryName, options.exclude || [])) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (options.include &&
|
|
166
|
+
options.include.length > 0 &&
|
|
167
|
+
!this.shouldInclude(entryName, options.include)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
async processDirectory(sourceEntry, destinationEntry, options, baseDir) {
|
|
173
|
+
await this.ensureDirectorySafe(destinationEntry, baseDir);
|
|
174
|
+
await this.copyDirectory(sourceEntry, destinationEntry, {
|
|
175
|
+
...options,
|
|
176
|
+
baseDir,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async processFileEntry(sourceEntry, destinationEntry, destinationName, options, baseDir) {
|
|
180
|
+
const sanitizedName = SecurityUtils.sanitizeFileName(destinationName);
|
|
181
|
+
if (sanitizedName !== destinationName) {
|
|
182
|
+
console.warn(chalk.yellow(`⚠️ File name sanitized: "${destinationName}" → "${sanitizedName}"`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (this.isTextFile(sourceEntry)) {
|
|
186
|
+
await this.processFile(sourceEntry, destinationEntry, {
|
|
187
|
+
skipIfExists: options.skipIfExists,
|
|
188
|
+
baseDir,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else if (options.skipIfExists && fs.existsSync(destinationEntry)) {
|
|
192
|
+
console.log(chalk.yellow(`⏭️ Skipping existing file: ${destinationEntry}`));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
SecurityUtils.validateOutputPath(destinationEntry, baseDir);
|
|
196
|
+
fs.copyFileSync(sourceEntry, destinationEntry);
|
|
197
|
+
if (this.isImportantContent(destinationEntry)) {
|
|
198
|
+
const relativePath = this.getRelativePath(destinationEntry);
|
|
199
|
+
console.log(chalk.green(`📋 ${relativePath}`));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Ensure a directory exists, creating it if necessary
|
|
205
|
+
*/
|
|
206
|
+
async ensureDirectory(dirPath) {
|
|
207
|
+
if (!fs.existsSync(dirPath)) {
|
|
208
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Safely ensure a directory exists with security validation
|
|
213
|
+
*/
|
|
214
|
+
async ensureDirectorySafe(dirPath, baseDir) {
|
|
215
|
+
SecurityUtils.validateOutputPath(dirPath, baseDir);
|
|
216
|
+
if (!fs.existsSync(dirPath)) {
|
|
217
|
+
await SecurityUtils.createDirectorySafe(dirPath, baseDir);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Check if a file should be excluded
|
|
222
|
+
*/
|
|
223
|
+
shouldExclude(fileName, excludePatterns) {
|
|
224
|
+
return excludePatterns.some((pattern) => {
|
|
225
|
+
if (pattern.includes('*')) {
|
|
226
|
+
// Safe glob pattern matching - escape regex metacharacters except *
|
|
227
|
+
const escapedPattern = pattern
|
|
228
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
229
|
+
.replace(/\\\*/g, '.*');
|
|
230
|
+
const regex = new RegExp(escapedPattern);
|
|
231
|
+
return regex.test(fileName);
|
|
232
|
+
}
|
|
233
|
+
return fileName === pattern;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Check if a file should be included
|
|
238
|
+
*/
|
|
239
|
+
shouldInclude(fileName, includePatterns) {
|
|
240
|
+
return includePatterns.some((pattern) => {
|
|
241
|
+
if (pattern.includes('*')) {
|
|
242
|
+
// Safe glob pattern matching - escape regex metacharacters except *
|
|
243
|
+
const escapedPattern = pattern
|
|
244
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
245
|
+
.replace(/\\\*/g, '.*');
|
|
246
|
+
const regex = new RegExp(escapedPattern);
|
|
247
|
+
return regex.test(fileName);
|
|
248
|
+
}
|
|
249
|
+
return fileName === pattern;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Check if a file is a text file that should have variable substitution
|
|
254
|
+
*/
|
|
255
|
+
isTextFile(filePath) {
|
|
256
|
+
const textExtensions = [
|
|
257
|
+
'.txt',
|
|
258
|
+
'.md',
|
|
259
|
+
'.yaml',
|
|
260
|
+
'.yml',
|
|
261
|
+
'.json',
|
|
262
|
+
'.js',
|
|
263
|
+
'.ts',
|
|
264
|
+
'.jsx',
|
|
265
|
+
'.tsx',
|
|
266
|
+
'.py',
|
|
267
|
+
'.go',
|
|
268
|
+
'.rs',
|
|
269
|
+
'.java',
|
|
270
|
+
'.cs',
|
|
271
|
+
'.php',
|
|
272
|
+
'.rb',
|
|
273
|
+
'.sh',
|
|
274
|
+
'.bash',
|
|
275
|
+
'.zsh',
|
|
276
|
+
'.fish',
|
|
277
|
+
'.ps1',
|
|
278
|
+
'.bat',
|
|
279
|
+
'.cmd',
|
|
280
|
+
'.dockerfile',
|
|
281
|
+
'.docker',
|
|
282
|
+
'.makefile',
|
|
283
|
+
'.mk',
|
|
284
|
+
'.toml',
|
|
285
|
+
'.ini',
|
|
286
|
+
'.cfg',
|
|
287
|
+
'.conf',
|
|
288
|
+
'.properties',
|
|
289
|
+
'.xml',
|
|
290
|
+
'.html',
|
|
291
|
+
'.htm',
|
|
292
|
+
'.css',
|
|
293
|
+
'.scss',
|
|
294
|
+
'.sass',
|
|
295
|
+
'.less',
|
|
296
|
+
'.vue',
|
|
297
|
+
'.svelte',
|
|
298
|
+
'.sql',
|
|
299
|
+
'.r',
|
|
300
|
+
'.R',
|
|
301
|
+
'.swift',
|
|
302
|
+
'.kt',
|
|
303
|
+
'.scala',
|
|
304
|
+
'.clj',
|
|
305
|
+
'.hs',
|
|
306
|
+
'.elm',
|
|
307
|
+
'.ex',
|
|
308
|
+
'.exs',
|
|
309
|
+
'.erl',
|
|
310
|
+
'.pl',
|
|
311
|
+
'.pm',
|
|
312
|
+
'.raku',
|
|
313
|
+
'.lua',
|
|
314
|
+
];
|
|
315
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
316
|
+
if (textExtensions.includes(extension)) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
// Check for files without extensions that are commonly text
|
|
320
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
321
|
+
const textBasenames = [
|
|
322
|
+
'readme',
|
|
323
|
+
'license',
|
|
324
|
+
'changelog',
|
|
325
|
+
'makefile',
|
|
326
|
+
'dockerfile',
|
|
327
|
+
'gitignore',
|
|
328
|
+
'gitattributes',
|
|
329
|
+
'editorconfig',
|
|
330
|
+
'eslintrc',
|
|
331
|
+
'prettierrc',
|
|
332
|
+
];
|
|
333
|
+
return textBasenames.some((name) => basename.includes(name));
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Check if a file path represents important generated content
|
|
337
|
+
*/
|
|
338
|
+
isImportantContent(filePath) {
|
|
339
|
+
const normalizedPath = path.normalize(filePath);
|
|
340
|
+
// Show files in important directories (agents, teams, queries, models)
|
|
341
|
+
const importantDirs = ['agents', 'teams', 'queries', 'models'];
|
|
342
|
+
const hasImportantDir = importantDirs.some((dir) => normalizedPath.includes(`${path.sep}${dir}${path.sep}`) ||
|
|
343
|
+
normalizedPath.includes(`/${dir}/`));
|
|
344
|
+
if (hasImportantDir) {
|
|
345
|
+
// Skip .keep files
|
|
346
|
+
return !path.basename(filePath).includes('.keep');
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Get relative path from a full file path for display
|
|
352
|
+
*/
|
|
353
|
+
getRelativePath(filePath) {
|
|
354
|
+
const normalizedPath = path.normalize(filePath);
|
|
355
|
+
// Find the project root by looking for common patterns
|
|
356
|
+
const pathParts = normalizedPath.split(path.sep);
|
|
357
|
+
// Look for agents, teams, queries, or models directory
|
|
358
|
+
const importantDirs = ['agents', 'teams', 'queries', 'models'];
|
|
359
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
360
|
+
if (importantDirs.includes(pathParts[i])) {
|
|
361
|
+
// Return from this directory onwards
|
|
362
|
+
return pathParts.slice(i).join('/');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Fallback to just filename
|
|
366
|
+
return path.basename(filePath);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for name validation and normalization
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Convert a string to lowercase kebab-case
|
|
6
|
+
* Examples:
|
|
7
|
+
* - "My Project" -> "my-project"
|
|
8
|
+
* - "MyProject" -> "my-project"
|
|
9
|
+
* - "my_project" -> "my-project"
|
|
10
|
+
* - "myProject123" -> "my-project123"
|
|
11
|
+
*/
|
|
12
|
+
export declare function toKebabCase(str: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Validate that a name follows kebab-case format
|
|
15
|
+
*/
|
|
16
|
+
export declare function isValidKebabCase(str: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Validate that a name is suitable for Kubernetes resources
|
|
19
|
+
*/
|
|
20
|
+
export declare function isValidKubernetesName(str: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Get validation error message for invalid names
|
|
23
|
+
*/
|
|
24
|
+
export declare function getNameValidationError(name: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Strict name validation that throws ValidationError with suggestions
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateNameStrict(name: string, type?: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Normalize and validate a name for use in generators
|
|
31
|
+
*/
|
|
32
|
+
export declare function normalizeAndValidateName(input: string, type?: string): {
|
|
33
|
+
name: string;
|
|
34
|
+
wasTransformed: boolean;
|
|
35
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for name validation and normalization
|
|
3
|
+
*/
|
|
4
|
+
import { ValidationError } from '../../../lib/errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Convert a string to lowercase kebab-case
|
|
7
|
+
* Examples:
|
|
8
|
+
* - "My Project" -> "my-project"
|
|
9
|
+
* - "MyProject" -> "my-project"
|
|
10
|
+
* - "my_project" -> "my-project"
|
|
11
|
+
* - "myProject123" -> "my-project123"
|
|
12
|
+
*/
|
|
13
|
+
export function toKebabCase(str) {
|
|
14
|
+
if (str.length > 1000) {
|
|
15
|
+
throw new Error('Input string too long for processing');
|
|
16
|
+
}
|
|
17
|
+
let result = str
|
|
18
|
+
.trim()
|
|
19
|
+
// Replace spaces and underscores with hyphens
|
|
20
|
+
.replace(/[\s_]+/g, '-')
|
|
21
|
+
// Insert hyphens before uppercase letters (camelCase -> kebab-case)
|
|
22
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
23
|
+
// Convert to lowercase
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
// Remove any double hyphens
|
|
26
|
+
.replace(/-+/g, '-');
|
|
27
|
+
// Remove leading/trailing hyphens using string methods to avoid regex backtracking
|
|
28
|
+
while (result.startsWith('-')) {
|
|
29
|
+
result = result.slice(1);
|
|
30
|
+
}
|
|
31
|
+
while (result.endsWith('-')) {
|
|
32
|
+
result = result.slice(0, -1);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate that a name follows kebab-case format
|
|
38
|
+
*/
|
|
39
|
+
export function isValidKebabCase(str) {
|
|
40
|
+
// Must be lowercase, can contain letters, numbers, and hyphens
|
|
41
|
+
// Cannot start or end with hyphen, cannot have consecutive hyphens
|
|
42
|
+
const kebabRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
43
|
+
return kebabRegex.test(str);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate that a name is suitable for Kubernetes resources
|
|
47
|
+
*/
|
|
48
|
+
export function isValidKubernetesName(str) {
|
|
49
|
+
// Kubernetes names must be lowercase alphanumeric or '-'
|
|
50
|
+
// Must start and end with alphanumeric character
|
|
51
|
+
// Max length 63 characters
|
|
52
|
+
return (isValidKebabCase(str) &&
|
|
53
|
+
str.length <= 63 &&
|
|
54
|
+
str.length >= 1 &&
|
|
55
|
+
/^[a-z0-9]/.test(str) &&
|
|
56
|
+
/[a-z0-9]$/.test(str));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get validation error message for invalid names
|
|
60
|
+
*/
|
|
61
|
+
export function getNameValidationError(name) {
|
|
62
|
+
try {
|
|
63
|
+
validateNameStrict(name);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof ValidationError) {
|
|
68
|
+
return error.message;
|
|
69
|
+
}
|
|
70
|
+
return error instanceof Error ? error.message : String(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Strict name validation that throws ValidationError with suggestions
|
|
75
|
+
*/
|
|
76
|
+
export function validateNameStrict(name, type = 'name') {
|
|
77
|
+
if (!name || name.trim().length === 0) {
|
|
78
|
+
throw new ValidationError(`${type} cannot be empty`, 'name', [
|
|
79
|
+
`Provide a valid ${type}`,
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
const trimmed = name.trim();
|
|
83
|
+
if (trimmed.length > 63) {
|
|
84
|
+
throw new ValidationError(`${type} must be 63 characters or less (got ${trimmed.length})`, 'name', [`Shorten the ${type} to 63 characters or less`]);
|
|
85
|
+
}
|
|
86
|
+
if (!isValidKubernetesName(trimmed)) {
|
|
87
|
+
const suggested = toKebabCase(trimmed);
|
|
88
|
+
const suggestions = [];
|
|
89
|
+
if (suggested !== trimmed && isValidKubernetesName(suggested)) {
|
|
90
|
+
suggestions.push(`Try: "${suggested}"`);
|
|
91
|
+
}
|
|
92
|
+
suggestions.push(`${type} must be lowercase letters, numbers, and hyphens only`);
|
|
93
|
+
suggestions.push(`${type} cannot start or end with a hyphen`);
|
|
94
|
+
suggestions.push(`${type} cannot contain consecutive hyphens`);
|
|
95
|
+
throw new ValidationError(`Invalid ${type}: "${trimmed}"`, 'name', suggestions);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Normalize and validate a name for use in generators
|
|
100
|
+
*/
|
|
101
|
+
export function normalizeAndValidateName(input, type = 'name') {
|
|
102
|
+
const original = input.trim();
|
|
103
|
+
const normalized = toKebabCase(original);
|
|
104
|
+
// Validate the normalized name
|
|
105
|
+
validateNameStrict(normalized, type);
|
|
106
|
+
return {
|
|
107
|
+
name: normalized,
|
|
108
|
+
wasTransformed: normalized !== original,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ProjectValidationResult {
|
|
2
|
+
isValid: boolean;
|
|
3
|
+
projectName?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Validate if the given directory is a valid ARK project
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateProjectStructure(projectDir: string): ProjectValidationResult;
|
|
10
|
+
/**
|
|
11
|
+
* Validate project structure and throw detailed error if invalid
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateProjectStructureStrict(projectDir: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Get the current project directory (current working directory)
|
|
16
|
+
*/
|
|
17
|
+
export declare function getCurrentProjectDirectory(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Check if the current directory is a valid ARK project
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateCurrentProject(): ProjectValidationResult;
|
|
22
|
+
/**
|
|
23
|
+
* Get current project info with strict validation
|
|
24
|
+
*/
|
|
25
|
+
export declare function getCurrentProjectInfo(): {
|
|
26
|
+
projectName: string;
|
|
27
|
+
projectDir: string;
|
|
28
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ProjectStructureError } from '../../../lib/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* Validate if the given directory is a valid ARK project
|
|
6
|
+
*/
|
|
7
|
+
export function validateProjectStructure(projectDir) {
|
|
8
|
+
// Check for required project files
|
|
9
|
+
const chartYamlPath = path.join(projectDir, 'Chart.yaml');
|
|
10
|
+
const agentsDir = path.join(projectDir, 'agents');
|
|
11
|
+
if (!fs.existsSync(chartYamlPath)) {
|
|
12
|
+
return {
|
|
13
|
+
isValid: false,
|
|
14
|
+
error: 'Chart.yaml not found. This does not appear to be a valid ARK project directory.',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (!fs.existsSync(agentsDir)) {
|
|
18
|
+
return {
|
|
19
|
+
isValid: false,
|
|
20
|
+
error: 'agents/ directory not found. This does not appear to be a valid ARK project directory.',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Try to extract project name from Chart.yaml
|
|
24
|
+
try {
|
|
25
|
+
const chartContent = fs.readFileSync(chartYamlPath, 'utf-8');
|
|
26
|
+
const nameMatch = /^name:\s*([a-zA-Z0-9\s._-]{1,200}?)$/m.exec(chartContent);
|
|
27
|
+
if (nameMatch) {
|
|
28
|
+
return {
|
|
29
|
+
isValid: true,
|
|
30
|
+
projectName: nameMatch[1].trim(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return {
|
|
36
|
+
isValid: false,
|
|
37
|
+
error: `Failed to read Chart.yaml: ${error}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
isValid: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check file/directory validation
|
|
46
|
+
*/
|
|
47
|
+
function validateRequiredPath(projectDir, required, missing, wrongType) {
|
|
48
|
+
const fullPath = path.join(projectDir, required.path);
|
|
49
|
+
if (!fs.existsSync(fullPath)) {
|
|
50
|
+
missing.push(required.path);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const stat = fs.statSync(fullPath);
|
|
54
|
+
if (required.type === 'file' && !stat.isFile()) {
|
|
55
|
+
wrongType.push(`${required.path} (expected file, found directory)`);
|
|
56
|
+
}
|
|
57
|
+
else if (required.type === 'directory' && !stat.isDirectory()) {
|
|
58
|
+
wrongType.push(`${required.path} (expected directory, found file)`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Extract project name from Chart.yaml
|
|
63
|
+
*/
|
|
64
|
+
function extractProjectName(projectDir) {
|
|
65
|
+
const chartYamlPath = path.join(projectDir, 'Chart.yaml');
|
|
66
|
+
const chartContent = fs.readFileSync(chartYamlPath, 'utf-8');
|
|
67
|
+
const nameMatch = /^name:\s*([a-zA-Z0-9\s._-]{1,200}?)$/m.exec(chartContent);
|
|
68
|
+
if (nameMatch) {
|
|
69
|
+
return nameMatch[1].trim();
|
|
70
|
+
}
|
|
71
|
+
throw new Error('No name field found in Chart.yaml');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Validate project structure and throw detailed error if invalid
|
|
75
|
+
*/
|
|
76
|
+
export function validateProjectStructureStrict(projectDir) {
|
|
77
|
+
const requiredFiles = [
|
|
78
|
+
{ path: 'Chart.yaml', type: 'file' },
|
|
79
|
+
{ path: 'agents', type: 'directory' },
|
|
80
|
+
{ path: 'teams', type: 'directory' },
|
|
81
|
+
{ path: 'queries', type: 'directory' },
|
|
82
|
+
{ path: 'models', type: 'directory' },
|
|
83
|
+
];
|
|
84
|
+
const missing = [];
|
|
85
|
+
const wrongType = [];
|
|
86
|
+
for (const required of requiredFiles) {
|
|
87
|
+
validateRequiredPath(projectDir, required, missing, wrongType);
|
|
88
|
+
}
|
|
89
|
+
if (missing.length > 0 || wrongType.length > 0) {
|
|
90
|
+
const issues = [];
|
|
91
|
+
if (missing.length > 0) {
|
|
92
|
+
issues.push(`Missing: ${missing.join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
if (wrongType.length > 0) {
|
|
95
|
+
issues.push(`Wrong type: ${wrongType.join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
throw new ProjectStructureError(`Invalid ARK project structure: ${issues.join('; ')}`, projectDir, [
|
|
98
|
+
'Ensure you are in a valid ARK project directory',
|
|
99
|
+
'Run "ark generate project" to create a new project',
|
|
100
|
+
'Check that all required files and directories exist',
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return extractProjectName(projectDir);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new ProjectStructureError(`Failed to read project name from Chart.yaml: ${error instanceof Error ? error.message : String(error)}`, projectDir, [
|
|
108
|
+
'Ensure Chart.yaml exists and is readable',
|
|
109
|
+
'Check that Chart.yaml contains a valid name field',
|
|
110
|
+
'Verify file permissions',
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get the current project directory (current working directory)
|
|
116
|
+
*/
|
|
117
|
+
export function getCurrentProjectDirectory() {
|
|
118
|
+
return process.cwd();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if the current directory is a valid ARK project
|
|
122
|
+
*/
|
|
123
|
+
export function validateCurrentProject() {
|
|
124
|
+
return validateProjectStructure(getCurrentProjectDirectory());
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get current project info with strict validation
|
|
128
|
+
*/
|
|
129
|
+
export function getCurrentProjectInfo() {
|
|
130
|
+
const projectDir = getCurrentProjectDirectory();
|
|
131
|
+
const projectName = validateProjectStructureStrict(projectDir);
|
|
132
|
+
return { projectName, projectDir };
|
|
133
|
+
}
|