vigthoria-cli 1.10.47 ā 1.10.49
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/dist/commands/agent-session-menu.js +2 -8
- package/dist/commands/auth.js +51 -68
- package/dist/commands/bridge.js +42 -22
- package/dist/commands/cancel.js +15 -22
- package/dist/commands/chat.d.ts +3 -0
- package/dist/commands/chat.js +326 -295
- package/dist/commands/config.js +33 -73
- package/dist/commands/deploy.js +83 -123
- package/dist/commands/device.js +21 -61
- package/dist/commands/edit.js +32 -39
- package/dist/commands/explain.js +18 -25
- package/dist/commands/fork.d.ts +17 -0
- package/dist/commands/fork.js +164 -0
- package/dist/commands/generate.js +37 -44
- package/dist/commands/history.d.ts +17 -0
- package/dist/commands/history.js +113 -0
- package/dist/commands/hub.js +95 -102
- package/dist/commands/index.js +41 -46
- package/dist/commands/legion.js +146 -186
- package/dist/commands/preview.d.ts +55 -0
- package/dist/commands/preview.js +467 -0
- package/dist/commands/replay.d.ts +18 -0
- package/dist/commands/replay.js +156 -0
- package/dist/commands/repo.d.ts +97 -0
- package/dist/commands/repo.js +773 -0
- package/dist/commands/review.js +29 -36
- package/dist/commands/security.js +5 -12
- package/dist/commands/update.d.ts +9 -0
- package/dist/commands/update.js +201 -0
- package/dist/commands/wallet.js +28 -35
- package/dist/commands/workflow.js +13 -20
- package/dist/index.d.ts +21 -0
- package/dist/index.js +1652 -0
- package/dist/utils/api.d.ts +544 -0
- package/dist/utils/api.js +5486 -0
- package/dist/utils/brain-hub-client.js +1 -5
- package/dist/utils/bridge-client.js +11 -52
- package/dist/utils/cli-state.d.ts +54 -0
- package/dist/utils/cli-state.js +185 -0
- package/dist/utils/codebase-indexer.js +4 -41
- package/dist/utils/config.d.ts +82 -0
- package/dist/utils/config.js +269 -0
- package/dist/utils/context-ranker.js +15 -21
- package/dist/utils/desktop-bridge-client.d.ts +12 -0
- package/dist/utils/desktop-bridge-client.js +30 -0
- package/dist/utils/files.js +5 -42
- package/dist/utils/logger.js +42 -50
- package/dist/utils/persona.js +3 -8
- package/dist/utils/post-write-validator.js +26 -33
- package/dist/utils/project-memory.js +16 -23
- package/dist/utils/session.d.ts +118 -0
- package/dist/utils/session.js +423 -0
- package/dist/utils/task-display.js +13 -20
- package/dist/utils/tools.d.ts +269 -0
- package/dist/utils/tools.js +3450 -0
- package/dist/utils/workspace-brain-service.js +8 -45
- package/dist/utils/workspace-cache.js +18 -26
- package/dist/utils/workspace-stream.js +21 -63
- package/package.json +2 -1
- package/scripts/release/validate-no-go-gates.sh +7 -4
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vigthoria CLI - Repo Commands
|
|
3
|
+
*
|
|
4
|
+
* Push and pull projects to/from Vigthoria Repository
|
|
5
|
+
* Enables version control and project sharing through the Vigthoria platform
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* vigthoria repo push [path] - Push current/specified project to Vigthoria Repo
|
|
9
|
+
* vigthoria repo pull <name> - Pull a project from your Vigthoria Repo
|
|
10
|
+
* vigthoria repo list - List all your projects in Vigthoria Repo
|
|
11
|
+
* vigthoria repo status - Show sync status of current project
|
|
12
|
+
* vigthoria repo share <name> - Generate shareable link for a project
|
|
13
|
+
* vigthoria repo delete <name> - Remove a project from your Vigthoria Repo
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { createSpinner, CH } from '../utils/logger.js';
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
import inquirer from 'inquirer';
|
|
22
|
+
import archiver from 'archiver';
|
|
23
|
+
import { createWriteStream } from 'fs';
|
|
24
|
+
export class RepoCommand {
|
|
25
|
+
config;
|
|
26
|
+
logger;
|
|
27
|
+
apiBase;
|
|
28
|
+
communityBase;
|
|
29
|
+
communityToken;
|
|
30
|
+
constructor(config, logger) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
this.apiBase = this.config.get('apiUrl') || 'https://coder.vigthoria.io';
|
|
34
|
+
this.communityBase = process.env.VIGTHORIA_COMMUNITY_API_URL || 'https://community.vigthoria.io';
|
|
35
|
+
this.communityToken = process.env.VIGTHORIA_COMMUNITY_TOKEN || null;
|
|
36
|
+
}
|
|
37
|
+
getAuthHeaders() {
|
|
38
|
+
const token = this.communityToken || this.config.get('authToken');
|
|
39
|
+
return {
|
|
40
|
+
'Authorization': `Bearer ${token}`,
|
|
41
|
+
'Content-Type': 'application/json'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async ensureCommunityAuth() {
|
|
45
|
+
if (this.communityToken) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const email = process.env.VIGTHORIA_COMMUNITY_EMAIL;
|
|
49
|
+
const password = process.env.VIGTHORIA_COMMUNITY_PASSWORD;
|
|
50
|
+
if (!email || !password) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const response = await fetch(`${this.communityBase}/api/auth/login`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ email, password })
|
|
57
|
+
});
|
|
58
|
+
const data = await response.json();
|
|
59
|
+
if (!response.ok || !data.token) {
|
|
60
|
+
throw new Error(data.error || data.message || 'Community login failed');
|
|
61
|
+
}
|
|
62
|
+
this.communityToken = data.token;
|
|
63
|
+
}
|
|
64
|
+
async repoFetch(apiPath, init = {}) {
|
|
65
|
+
const normalizedApiPath = apiPath.startsWith('/api/repo') ? apiPath : `/api/repo${apiPath.startsWith('/') ? apiPath : `/${apiPath}`}`;
|
|
66
|
+
const proxyPath = `${this.apiBase}/api/community-repo${normalizedApiPath.replace('/api/repo', '')}`;
|
|
67
|
+
const directPath = `${this.communityBase}${normalizedApiPath}`;
|
|
68
|
+
const attempt = async (url, allowCommunityAuthRetry) => {
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
...init,
|
|
71
|
+
headers: {
|
|
72
|
+
...this.getAuthHeaders(),
|
|
73
|
+
...(init.headers || {})
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
if (response.status === 401 && allowCommunityAuthRetry && !this.communityToken) {
|
|
77
|
+
await this.ensureCommunityAuth();
|
|
78
|
+
if (this.communityToken) {
|
|
79
|
+
return attempt(url, false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return response;
|
|
83
|
+
};
|
|
84
|
+
try {
|
|
85
|
+
const proxyResponse = await attempt(proxyPath, true);
|
|
86
|
+
if (proxyResponse.ok || proxyResponse.status < 500) {
|
|
87
|
+
return proxyResponse;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// fall through to direct community endpoint
|
|
92
|
+
}
|
|
93
|
+
return attempt(directPath, true);
|
|
94
|
+
}
|
|
95
|
+
collectProjectFiles(projectPath) {
|
|
96
|
+
const files = [];
|
|
97
|
+
const ignoreNames = new Set(['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv']);
|
|
98
|
+
const ignoreSuffixes = ['.log'];
|
|
99
|
+
const walk = (dir) => {
|
|
100
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
101
|
+
if (ignoreNames.has(entry.name) || entry.name.startsWith('.')) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const fullPath = path.join(dir, entry.name);
|
|
105
|
+
const relativePath = path.relative(projectPath, fullPath);
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
walk(fullPath);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!entry.isFile() || ignoreSuffixes.some((suffix) => entry.name.endsWith(suffix))) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
files.push({
|
|
115
|
+
path: relativePath,
|
|
116
|
+
content: fs.readFileSync(fullPath, 'utf8')
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Skip binary or unreadable files.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
walk(projectPath);
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
async getMyRepos() {
|
|
128
|
+
const response = await this.repoFetch('/api/repo/my-repos', {
|
|
129
|
+
method: 'GET'
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const error = await response.json().catch(async () => ({ error: await response.text() }));
|
|
133
|
+
throw new Error(error.error || error.message || 'Failed to fetch repositories');
|
|
134
|
+
}
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
return data.repos || data.projects || [];
|
|
137
|
+
}
|
|
138
|
+
async resolveRepoByName(projectName) {
|
|
139
|
+
const repos = await this.getMyRepos();
|
|
140
|
+
const normalized = projectName.trim().toLowerCase();
|
|
141
|
+
const exactMatch = repos.find((repo) => {
|
|
142
|
+
const name = String(repo.name || repo.project_name || '').trim().toLowerCase();
|
|
143
|
+
return name === normalized || String(repo.id) === projectName.trim();
|
|
144
|
+
});
|
|
145
|
+
const match = exactMatch || repos.find((repo) => {
|
|
146
|
+
const name = String(repo.name || repo.project_name || '').trim().toLowerCase();
|
|
147
|
+
return name.includes(normalized);
|
|
148
|
+
});
|
|
149
|
+
if (!match) {
|
|
150
|
+
throw new Error(`Repository not found: ${projectName}`);
|
|
151
|
+
}
|
|
152
|
+
return match;
|
|
153
|
+
}
|
|
154
|
+
formatRepoError(error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
156
|
+
if (/invalid or expired session/i.test(message)) {
|
|
157
|
+
return 'Repo memory service is not deployed or not reachable. Check `vigthoria status` for details.';
|
|
158
|
+
}
|
|
159
|
+
if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|socket hang up/i.test(message)) {
|
|
160
|
+
return 'Repo memory service is not reachable. Check `vigthoria status` for details.';
|
|
161
|
+
}
|
|
162
|
+
return message;
|
|
163
|
+
}
|
|
164
|
+
isAuthenticated() {
|
|
165
|
+
const token = this.config.get('authToken');
|
|
166
|
+
return !!token;
|
|
167
|
+
}
|
|
168
|
+
requireAuth() {
|
|
169
|
+
if (!this.isAuthenticated()) {
|
|
170
|
+
console.log(chalk.red('\nā Authentication required. Run: vigthoria login\n'));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Detect project info from package.json, requirements.txt, etc.
|
|
176
|
+
*/
|
|
177
|
+
detectProjectInfo(projectPath) {
|
|
178
|
+
let name = path.basename(projectPath);
|
|
179
|
+
let description = '';
|
|
180
|
+
const techStack = [];
|
|
181
|
+
// Check package.json (Node.js)
|
|
182
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
183
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
184
|
+
try {
|
|
185
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
186
|
+
name = pkg.name || name;
|
|
187
|
+
description = pkg.description || '';
|
|
188
|
+
techStack.push('Node.js');
|
|
189
|
+
// Detect frameworks from dependencies
|
|
190
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
191
|
+
if (deps.express)
|
|
192
|
+
techStack.push('Express');
|
|
193
|
+
if (deps.react)
|
|
194
|
+
techStack.push('React');
|
|
195
|
+
if (deps.vue)
|
|
196
|
+
techStack.push('Vue');
|
|
197
|
+
if (deps.next)
|
|
198
|
+
techStack.push('Next.js');
|
|
199
|
+
if (deps.typescript)
|
|
200
|
+
techStack.push('TypeScript');
|
|
201
|
+
if (deps.sqlite3 || deps['better-sqlite3'])
|
|
202
|
+
techStack.push('SQLite');
|
|
203
|
+
if (deps.stripe)
|
|
204
|
+
techStack.push('Stripe');
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
// Ignore parse errors
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Check requirements.txt (Python)
|
|
211
|
+
const requirementsPath = path.join(projectPath, 'requirements.txt');
|
|
212
|
+
if (fs.existsSync(requirementsPath)) {
|
|
213
|
+
techStack.push('Python');
|
|
214
|
+
const content = fs.readFileSync(requirementsPath, 'utf8').toLowerCase();
|
|
215
|
+
if (content.includes('flask'))
|
|
216
|
+
techStack.push('Flask');
|
|
217
|
+
if (content.includes('django'))
|
|
218
|
+
techStack.push('Django');
|
|
219
|
+
if (content.includes('fastapi'))
|
|
220
|
+
techStack.push('FastAPI');
|
|
221
|
+
if (content.includes('torch') || content.includes('pytorch'))
|
|
222
|
+
techStack.push('PyTorch');
|
|
223
|
+
if (content.includes('tensorflow'))
|
|
224
|
+
techStack.push('TensorFlow');
|
|
225
|
+
}
|
|
226
|
+
// Check Cargo.toml (Rust)
|
|
227
|
+
if (fs.existsSync(path.join(projectPath, 'Cargo.toml'))) {
|
|
228
|
+
techStack.push('Rust');
|
|
229
|
+
}
|
|
230
|
+
// Check go.mod (Go)
|
|
231
|
+
if (fs.existsSync(path.join(projectPath, 'go.mod'))) {
|
|
232
|
+
techStack.push('Go');
|
|
233
|
+
}
|
|
234
|
+
return { name, description, techStack: [...new Set(techStack)] };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Create a zip archive of a project
|
|
238
|
+
*/
|
|
239
|
+
async createProjectArchive(projectPath) {
|
|
240
|
+
const tempDir = path.join(require('os').tmpdir(), 'vigthoria-cli');
|
|
241
|
+
if (!fs.existsSync(tempDir)) {
|
|
242
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
const archivePath = path.join(tempDir, `project-${Date.now()}.zip`);
|
|
245
|
+
const output = createWriteStream(archivePath);
|
|
246
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
output.on('close', () => resolve(archivePath));
|
|
249
|
+
archive.on('error', (err) => reject(err));
|
|
250
|
+
archive.pipe(output);
|
|
251
|
+
// Ignore patterns
|
|
252
|
+
const ignorePatterns = [
|
|
253
|
+
'node_modules/**',
|
|
254
|
+
'.git/**',
|
|
255
|
+
'dist/**',
|
|
256
|
+
'build/**',
|
|
257
|
+
'__pycache__/**',
|
|
258
|
+
'.venv/**',
|
|
259
|
+
'venv/**',
|
|
260
|
+
'*.log',
|
|
261
|
+
'.env',
|
|
262
|
+
'.DS_Store'
|
|
263
|
+
];
|
|
264
|
+
archive.glob('**/*', {
|
|
265
|
+
cwd: projectPath,
|
|
266
|
+
ignore: ignorePatterns,
|
|
267
|
+
dot: true
|
|
268
|
+
});
|
|
269
|
+
archive.finalize();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Push a project to Vigthoria Repository
|
|
274
|
+
*/
|
|
275
|
+
async push(options = {}) {
|
|
276
|
+
this.requireAuth();
|
|
277
|
+
const projectPath = options.path ? path.resolve(options.path) : process.cwd();
|
|
278
|
+
if (!fs.existsSync(projectPath)) {
|
|
279
|
+
console.log(chalk.red(`\nā Path does not exist: ${projectPath}\n`));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const spinner = createSpinner('Analyzing project...').start();
|
|
283
|
+
try {
|
|
284
|
+
const projectInfo = this.detectProjectInfo(projectPath);
|
|
285
|
+
spinner.succeed(`Project detected: ${chalk.cyan(projectInfo.name)}`);
|
|
286
|
+
console.log(chalk.gray(`\n š Path: ${projectPath}`));
|
|
287
|
+
console.log(chalk.gray(` š Description: ${projectInfo.description || '(none)'}`));
|
|
288
|
+
console.log(chalk.gray(` š§ Tech Stack: ${projectInfo.techStack.join(', ') || 'Unknown'}`));
|
|
289
|
+
console.log();
|
|
290
|
+
const defaults = {
|
|
291
|
+
name: options.name || projectInfo.name,
|
|
292
|
+
description: options.description || projectInfo.description,
|
|
293
|
+
visibility: options.visibility || 'private'
|
|
294
|
+
};
|
|
295
|
+
const answers = options.yes
|
|
296
|
+
? { ...defaults, confirm: true }
|
|
297
|
+
: await inquirer.prompt([
|
|
298
|
+
{
|
|
299
|
+
type: 'input',
|
|
300
|
+
name: 'name',
|
|
301
|
+
message: 'Project name:',
|
|
302
|
+
default: defaults.name
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
type: 'input',
|
|
306
|
+
name: 'description',
|
|
307
|
+
message: 'Description:',
|
|
308
|
+
default: defaults.description
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
type: 'list',
|
|
312
|
+
name: 'visibility',
|
|
313
|
+
message: 'Visibility:',
|
|
314
|
+
choices: [
|
|
315
|
+
{ name: 'Private - Only you can access', value: 'private' },
|
|
316
|
+
{ name: 'Restricted - Invite only', value: 'restricted' },
|
|
317
|
+
{ name: 'Public - Anyone can view', value: 'public' }
|
|
318
|
+
],
|
|
319
|
+
default: defaults.visibility
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
type: 'confirm',
|
|
323
|
+
name: 'confirm',
|
|
324
|
+
message: 'Push project to Vigthoria Repo?',
|
|
325
|
+
default: true
|
|
326
|
+
}
|
|
327
|
+
]);
|
|
328
|
+
if (!answers.confirm) {
|
|
329
|
+
console.log(chalk.yellow(`\n${CH.warnEmoji} Push cancelled.\n`));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const uploadSpinner = createSpinner('Preparing project files...').start();
|
|
333
|
+
const files = this.collectProjectFiles(projectPath);
|
|
334
|
+
if (files.length === 0) {
|
|
335
|
+
throw new Error('No readable text files found to push');
|
|
336
|
+
}
|
|
337
|
+
uploadSpinner.text = 'Uploading to Vigthoria Community...';
|
|
338
|
+
const registerResponse = await this.repoFetch('/api/repo/push', {
|
|
339
|
+
method: 'POST',
|
|
340
|
+
body: JSON.stringify({
|
|
341
|
+
projectName: answers.name,
|
|
342
|
+
description: answers.description,
|
|
343
|
+
techStack: projectInfo.techStack.join(', '),
|
|
344
|
+
visibility: answers.visibility,
|
|
345
|
+
force: options.force || false,
|
|
346
|
+
files,
|
|
347
|
+
commitMessage: `Push from Vigthoria CLI: ${files.length} file(s)`
|
|
348
|
+
})
|
|
349
|
+
});
|
|
350
|
+
if (!registerResponse.ok) {
|
|
351
|
+
const error = await registerResponse.json().catch(async () => ({ error: await registerResponse.text() }));
|
|
352
|
+
throw new Error(error.error || error.message || 'Failed to push project');
|
|
353
|
+
}
|
|
354
|
+
const registerData = await registerResponse.json();
|
|
355
|
+
uploadSpinner.succeed(chalk.green('Project pushed successfully!'));
|
|
356
|
+
console.log(chalk.cyan('\nš¦ Project Details:'));
|
|
357
|
+
console.log(chalk.gray(` Name: ${registerData.project?.project_name || registerData.project?.name || answers.name}`));
|
|
358
|
+
console.log(chalk.gray(` Visibility: ${registerData.project?.visibility || answers.visibility}`));
|
|
359
|
+
if (registerData.project?.id || registerData.projectId) {
|
|
360
|
+
console.log(chalk.gray(` ID: ${registerData.project?.id || registerData.projectId}`));
|
|
361
|
+
}
|
|
362
|
+
if ((registerData.project?.visibility || answers.visibility) === 'public') {
|
|
363
|
+
console.log(chalk.cyan(`\nš Public URL: ${registerData.url || `https://community.vigthoria.io/showcase/${registerData.project?.id || registerData.projectId}`}`));
|
|
364
|
+
}
|
|
365
|
+
console.log(chalk.gray('\nTip: Use `vigthoria repo pull <name>` to restore this project anywhere.\n'));
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
spinner.stop();
|
|
369
|
+
this.logger.error('Push failed');
|
|
370
|
+
const errMsg = this.formatRepoError(error);
|
|
371
|
+
console.log(chalk.red(`\nā Error: ${errMsg}\n`));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Pull a project from Vigthoria Repository
|
|
376
|
+
*/
|
|
377
|
+
async pull(projectName, options = {}) {
|
|
378
|
+
this.requireAuth();
|
|
379
|
+
const spinner = createSpinner(`Fetching project: ${projectName}...`).start();
|
|
380
|
+
try {
|
|
381
|
+
const repo = await this.resolveRepoByName(projectName);
|
|
382
|
+
const response = await this.repoFetch('/api/repo/pull', {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
body: JSON.stringify({ projectId: repo.id })
|
|
385
|
+
});
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
const error = await response.json().catch(async () => ({ error: await response.text() }));
|
|
388
|
+
throw new Error(error.error || error.message || 'Project not found');
|
|
389
|
+
}
|
|
390
|
+
const data = await response.json();
|
|
391
|
+
spinner.succeed(`Found project: ${chalk.cyan(data.project?.project_name || data.projectName || repo.name || repo.project_name || projectName)}`);
|
|
392
|
+
const outputPath = options.output
|
|
393
|
+
? path.resolve(options.output)
|
|
394
|
+
: path.join(process.cwd(), data.project?.project_name || data.projectName || repo.name || repo.project_name || projectName);
|
|
395
|
+
if (fs.existsSync(outputPath) && !options.force) {
|
|
396
|
+
const { overwrite } = await inquirer.prompt([{
|
|
397
|
+
type: 'confirm',
|
|
398
|
+
name: 'overwrite',
|
|
399
|
+
message: `Directory ${outputPath} exists. Overwrite?`,
|
|
400
|
+
default: false
|
|
401
|
+
}]);
|
|
402
|
+
if (!overwrite) {
|
|
403
|
+
console.log(chalk.yellow(`\n${CH.warnEmoji} Pull cancelled.\n`));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const downloadSpinner = createSpinner('Downloading project files...').start();
|
|
408
|
+
// Create output directory
|
|
409
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
410
|
+
// If we have a download URL, fetch and extract
|
|
411
|
+
if (data.downloadUrl) {
|
|
412
|
+
const archiveResponse = await fetch(data.downloadUrl, {
|
|
413
|
+
headers: this.getAuthHeaders()
|
|
414
|
+
});
|
|
415
|
+
if (!archiveResponse.ok) {
|
|
416
|
+
throw new Error('Failed to download project archive');
|
|
417
|
+
}
|
|
418
|
+
// Save and extract the archive
|
|
419
|
+
const tempArchive = path.join(require('os').tmpdir(), `vigthoria-pull-${Date.now()}.zip`);
|
|
420
|
+
const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
|
|
421
|
+
fs.writeFileSync(tempArchive, archiveBuffer);
|
|
422
|
+
// Extract archive - cross-platform
|
|
423
|
+
const os = require('os');
|
|
424
|
+
const platform = os.platform();
|
|
425
|
+
if (platform === 'win32') {
|
|
426
|
+
// Use PowerShell's Expand-Archive ā args as array to prevent injection
|
|
427
|
+
const { execFileSync } = await import('child_process');
|
|
428
|
+
execFileSync('powershell', [
|
|
429
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
430
|
+
`Expand-Archive -Path '${tempArchive.replace(/'/g, "''")}' -DestinationPath '${outputPath.replace(/'/g, "''")}' -Force`
|
|
431
|
+
], { stdio: 'ignore', windowsHide: true });
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
// Use unzip on Unix-like systems ā args as array to prevent injection
|
|
435
|
+
const { execFileSync } = await import('child_process');
|
|
436
|
+
execFileSync('unzip', ['-o', tempArchive, '-d', outputPath], { stdio: 'ignore' });
|
|
437
|
+
}
|
|
438
|
+
fs.unlinkSync(tempArchive);
|
|
439
|
+
}
|
|
440
|
+
else if (data.files) {
|
|
441
|
+
// If we have files inline (small projects)
|
|
442
|
+
for (const file of data.files) {
|
|
443
|
+
const filePath = path.join(outputPath, file.path);
|
|
444
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
445
|
+
fs.writeFileSync(filePath, file.content);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
downloadSpinner.succeed(chalk.green('Project pulled successfully!'));
|
|
449
|
+
console.log(chalk.cyan('\nš Project extracted to:'));
|
|
450
|
+
console.log(chalk.white(` ${outputPath}`));
|
|
451
|
+
console.log(chalk.gray(`\n Tech Stack: ${data.project?.tech_stack || repo.tech_stack || 'Unknown'}`));
|
|
452
|
+
console.log(chalk.gray(` Description: ${data.project?.description || data.description || repo.description || '(none)'}`));
|
|
453
|
+
console.log();
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
spinner.stop();
|
|
457
|
+
this.logger.error('Pull failed');
|
|
458
|
+
const errMsg = this.formatRepoError(error);
|
|
459
|
+
console.log(chalk.red(`\nā Error: ${errMsg}\n`));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* List all projects in user's Vigthoria Repository
|
|
464
|
+
*/
|
|
465
|
+
async list(options = {}) {
|
|
466
|
+
this.requireAuth();
|
|
467
|
+
const spinner = createSpinner('Fetching your projects...').start();
|
|
468
|
+
try {
|
|
469
|
+
const repos = await this.getMyRepos();
|
|
470
|
+
const filteredRepos = options.visibility
|
|
471
|
+
? repos.filter((repo) => (repo.visibility || repo.is_public === true ? 'public' : 'private') === options.visibility)
|
|
472
|
+
: repos;
|
|
473
|
+
const data = {
|
|
474
|
+
projects: filteredRepos,
|
|
475
|
+
owned: filteredRepos,
|
|
476
|
+
shared: [],
|
|
477
|
+
total: filteredRepos.length
|
|
478
|
+
};
|
|
479
|
+
spinner.stop();
|
|
480
|
+
if (data.projects.length === 0) {
|
|
481
|
+
console.log(chalk.yellow('\nš¦ No projects in your Vigthoria Repo yet.\n'));
|
|
482
|
+
console.log(chalk.gray('Use `vigthoria repo push` to upload your first project.\n'));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
// Separate owned and shared projects
|
|
486
|
+
const ownedProjects = data.owned || data.projects.filter((p) => p.access_level === 'owner');
|
|
487
|
+
const sharedProjects = data.shared || data.projects.filter((p) => p.access_level !== 'owner');
|
|
488
|
+
console.log(chalk.cyan(`\nš¦ Your Vigthoria Repository (${data.total} project${data.total !== 1 ? 's' : ''})\n`));
|
|
489
|
+
// Display owned projects grouped by visibility
|
|
490
|
+
if (ownedProjects.length > 0) {
|
|
491
|
+
console.log(chalk.bold.cyan(' š YOUR PROJECTS'));
|
|
492
|
+
console.log(chalk.gray(' ' + CH.hLine.repeat(50)));
|
|
493
|
+
// Group by visibility
|
|
494
|
+
const grouped = ownedProjects.reduce((acc, project) => {
|
|
495
|
+
const vis = project.visibility || 'private';
|
|
496
|
+
if (!acc[vis])
|
|
497
|
+
acc[vis] = [];
|
|
498
|
+
acc[vis].push(project);
|
|
499
|
+
return acc;
|
|
500
|
+
}, {});
|
|
501
|
+
const visibilityOrder = ['private', 'restricted', 'public'];
|
|
502
|
+
const visibilityIcons = {
|
|
503
|
+
private: 'š',
|
|
504
|
+
restricted: 'š„',
|
|
505
|
+
public: 'š'
|
|
506
|
+
};
|
|
507
|
+
visibilityOrder.forEach(vis => {
|
|
508
|
+
const projects = grouped[vis];
|
|
509
|
+
if (!projects || projects.length === 0)
|
|
510
|
+
return;
|
|
511
|
+
console.log(chalk.bold.white(` ${visibilityIcons[vis]} ${vis.toUpperCase()}`));
|
|
512
|
+
projects.forEach((project) => {
|
|
513
|
+
const projectName = project.project_name || project.name || 'Unnamed';
|
|
514
|
+
const techStack = project.tech_stack || project.framework || 'Unknown';
|
|
515
|
+
const updatedAt = project.updated_at || project.updatedAt || project.created_at || project.createdAt;
|
|
516
|
+
const syncStatus = (project.last_synced_at || project.lastSyncedAt)
|
|
517
|
+
? chalk.green(`${CH.success} synced`)
|
|
518
|
+
: chalk.yellow('- not synced');
|
|
519
|
+
console.log(chalk.white(` ${projectName.padEnd(30)} ${syncStatus}`));
|
|
520
|
+
if (project.description) {
|
|
521
|
+
console.log(chalk.gray(` ${project.description.substring(0, 50)}${project.description.length > 50 ? '...' : ''}`));
|
|
522
|
+
}
|
|
523
|
+
console.log(chalk.gray(` Tech: ${techStack}`));
|
|
524
|
+
console.log(chalk.gray(` Updated: ${updatedAt ? new Date(updatedAt).toLocaleDateString() : 'Unknown'}`));
|
|
525
|
+
console.log();
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// Display shared projects
|
|
530
|
+
if (sharedProjects.length > 0) {
|
|
531
|
+
console.log(chalk.bold.magenta('\n š¤ SHARED WITH YOU'));
|
|
532
|
+
console.log(chalk.gray(' ' + CH.hLine.repeat(50)));
|
|
533
|
+
sharedProjects.forEach((project) => {
|
|
534
|
+
const projectName = project.project_name || project.name || 'Unnamed';
|
|
535
|
+
const techStack = project.tech_stack || project.framework || 'Unknown';
|
|
536
|
+
const updatedAt = project.updated_at || project.updatedAt || project.created_at || project.createdAt;
|
|
537
|
+
const accessIcon = project.access_level === 'admin' ? 'š' : project.access_level === 'write' ? 'āļø' : 'šļø';
|
|
538
|
+
console.log(chalk.white(` ${accessIcon} ${projectName.padEnd(28)} [${project.access_level}]`));
|
|
539
|
+
if (project.description) {
|
|
540
|
+
console.log(chalk.gray(` ${project.description.substring(0, 50)}${project.description.length > 50 ? '...' : ''}`));
|
|
541
|
+
}
|
|
542
|
+
console.log(chalk.gray(` Tech: ${techStack}`));
|
|
543
|
+
console.log(chalk.gray(` Updated: ${updatedAt ? new Date(updatedAt).toLocaleDateString() : 'Unknown'}`));
|
|
544
|
+
console.log();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
console.log(chalk.gray(CH.hLine.repeat(60)));
|
|
548
|
+
console.log(chalk.cyan('\nCommands:'));
|
|
549
|
+
console.log(chalk.gray(' vigthoria repo pull <name> - Download a project'));
|
|
550
|
+
console.log(chalk.gray(' vigthoria repo push - Upload current directory'));
|
|
551
|
+
console.log(chalk.gray(' vigthoria repo status - Check sync status'));
|
|
552
|
+
console.log(chalk.gray(' vigthoria repo share <name> - Share a project\n'));
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
spinner.stop();
|
|
556
|
+
this.logger.error('List failed');
|
|
557
|
+
const errMsg = this.formatRepoError(error);
|
|
558
|
+
console.log(chalk.red(`\nā Error: ${errMsg}\n`));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Show sync status of current project
|
|
563
|
+
*/
|
|
564
|
+
async status() {
|
|
565
|
+
this.requireAuth();
|
|
566
|
+
const projectPath = process.cwd();
|
|
567
|
+
const projectInfo = this.detectProjectInfo(projectPath);
|
|
568
|
+
const spinner = createSpinner('Checking sync status...').start();
|
|
569
|
+
try {
|
|
570
|
+
const response = await fetch(`${this.apiBase}/api/repo/status/${encodeURIComponent(projectInfo.name)}`, {
|
|
571
|
+
method: 'GET',
|
|
572
|
+
headers: this.getAuthHeaders()
|
|
573
|
+
});
|
|
574
|
+
if (!response.ok) {
|
|
575
|
+
spinner.succeed('Project not tracked in Vigthoria Repo');
|
|
576
|
+
console.log(chalk.yellow('\nš¦ Current project is not synced with Vigthoria Repo.\n'));
|
|
577
|
+
console.log(chalk.gray('Use `vigthoria repo push` to upload it.\n'));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const data = await response.json();
|
|
581
|
+
spinner.stop();
|
|
582
|
+
const statusIcons = {
|
|
583
|
+
synced: 'ā
',
|
|
584
|
+
ahead: 'ā¬ļø',
|
|
585
|
+
behind: 'ā¬ļø',
|
|
586
|
+
diverged: CH.warnEmoji
|
|
587
|
+
};
|
|
588
|
+
console.log(chalk.cyan(`\nš¦ Project: ${chalk.white(data.project.project_name)}\n`));
|
|
589
|
+
console.log(chalk.gray(` Path: ${projectPath}`));
|
|
590
|
+
console.log(chalk.gray(` Visibility: ${data.project.visibility}`));
|
|
591
|
+
console.log(chalk.gray(` Tech Stack: ${data.project.tech_stack || 'Unknown'}`));
|
|
592
|
+
console.log();
|
|
593
|
+
console.log(` Status: ${statusIcons[data.syncStatus]} ${data.syncStatus.toUpperCase()}`);
|
|
594
|
+
if (data.localChanges && data.localChanges > 0) {
|
|
595
|
+
console.log(chalk.yellow(` Local changes: ${data.localChanges} file(s) modified`));
|
|
596
|
+
}
|
|
597
|
+
console.log(chalk.gray(`\n Last synced: ${data.project.last_synced_at
|
|
598
|
+
? new Date(data.project.last_synced_at).toLocaleString()
|
|
599
|
+
: 'Never'}`));
|
|
600
|
+
console.log();
|
|
601
|
+
if (data.syncStatus === 'ahead' || data.syncStatus === 'diverged') {
|
|
602
|
+
console.log(chalk.cyan('Tip: Use `vigthoria repo push` to sync changes.\n'));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
spinner.stop();
|
|
607
|
+
this.logger.error('Status check failed');
|
|
608
|
+
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
609
|
+
console.log(chalk.red(`\nā Error: ${errMsg}\n`));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Generate a shareable link for a project
|
|
614
|
+
*/
|
|
615
|
+
async share(projectName, options = {}) {
|
|
616
|
+
this.requireAuth();
|
|
617
|
+
const spinner = createSpinner('Generating share link...').start();
|
|
618
|
+
try {
|
|
619
|
+
const response = await fetch(`${this.apiBase}/api/repo/share`, {
|
|
620
|
+
method: 'POST',
|
|
621
|
+
headers: this.getAuthHeaders(),
|
|
622
|
+
body: JSON.stringify({
|
|
623
|
+
projectName,
|
|
624
|
+
expiresIn: options.expires || '7d'
|
|
625
|
+
})
|
|
626
|
+
});
|
|
627
|
+
if (!response.ok) {
|
|
628
|
+
const error = await response.json();
|
|
629
|
+
throw new Error(error.error || 'Failed to generate share link');
|
|
630
|
+
}
|
|
631
|
+
const data = await response.json();
|
|
632
|
+
spinner.succeed(chalk.green('Share link generated!'));
|
|
633
|
+
console.log(chalk.cyan('\nš Share Link:'));
|
|
634
|
+
console.log(chalk.white(` ${data.shareUrl}`));
|
|
635
|
+
console.log(chalk.gray(`\n Expires: ${new Date(data.expiresAt).toLocaleString()}`));
|
|
636
|
+
console.log(chalk.gray('\n Anyone with this link can view/download the project.\n'));
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
spinner.stop();
|
|
640
|
+
this.logger.error('Share failed');
|
|
641
|
+
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
642
|
+
console.log(chalk.red(`\nā Error: ${errMsg}\n`));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Delete a project from Vigthoria Repository
|
|
647
|
+
*/
|
|
648
|
+
async delete(projectName) {
|
|
649
|
+
this.requireAuth();
|
|
650
|
+
const { confirm } = await inquirer.prompt([{
|
|
651
|
+
type: 'confirm',
|
|
652
|
+
name: 'confirm',
|
|
653
|
+
message: chalk.red(`Are you sure you want to delete "${projectName}" from Vigthoria Repo?`),
|
|
654
|
+
default: false
|
|
655
|
+
}]);
|
|
656
|
+
if (!confirm) {
|
|
657
|
+
console.log(chalk.yellow(`\n${CH.warnEmoji} Delete cancelled.\n`));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const spinner = createSpinner('Deleting project...').start();
|
|
661
|
+
try {
|
|
662
|
+
const response = await fetch(`${this.apiBase}/api/repo/projects/${encodeURIComponent(projectName)}`, {
|
|
663
|
+
method: 'DELETE',
|
|
664
|
+
headers: this.getAuthHeaders()
|
|
665
|
+
});
|
|
666
|
+
if (!response.ok) {
|
|
667
|
+
const error = await response.json();
|
|
668
|
+
throw new Error(error.error || 'Failed to delete project');
|
|
669
|
+
}
|
|
670
|
+
spinner.succeed(chalk.green('Project deleted from Vigthoria Repo'));
|
|
671
|
+
console.log(chalk.gray('\nNote: Your local files are not affected.\n'));
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
spinner.stop();
|
|
675
|
+
this.logger.error('Delete failed');
|
|
676
|
+
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
677
|
+
console.log(chalk.red(`\nā Error: ${errMsg}\n`));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Open a project in a Vigthoria engine (shop, visual, game).
|
|
682
|
+
* Pushes to community if not yet there, then triggers engine import.
|
|
683
|
+
*/
|
|
684
|
+
async openIn(engine, projectName, options = {}) {
|
|
685
|
+
this.requireAuth();
|
|
686
|
+
const engineLabels = {
|
|
687
|
+
shop: 'Shop Engine',
|
|
688
|
+
visual: 'Visual Editor',
|
|
689
|
+
game: 'Gaming Engine'
|
|
690
|
+
};
|
|
691
|
+
const label = engineLabels[engine] || engine;
|
|
692
|
+
// Resolve project name from arg or current directory
|
|
693
|
+
const resolvedName = projectName || this.detectProjectInfo(process.cwd()).name;
|
|
694
|
+
const spinner = createSpinner(`Opening "${resolvedName}" in ${label}...`).start();
|
|
695
|
+
try {
|
|
696
|
+
const response = await fetch(`${this.apiBase}/api/engine-import`, {
|
|
697
|
+
method: 'POST',
|
|
698
|
+
headers: this.getAuthHeaders(),
|
|
699
|
+
body: JSON.stringify({
|
|
700
|
+
engine,
|
|
701
|
+
projectName: resolvedName,
|
|
702
|
+
shopId: options.shopId || 'default',
|
|
703
|
+
source: 'repo'
|
|
704
|
+
})
|
|
705
|
+
});
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
const err = await response.json().catch(async () => ({ error: await response.text() }));
|
|
708
|
+
throw new Error(err.error || `Engine import failed (${response.status})`);
|
|
709
|
+
}
|
|
710
|
+
const data = await response.json();
|
|
711
|
+
spinner.succeed(chalk.green(`Project imported into ${label}!`));
|
|
712
|
+
console.log(chalk.cyan(`\nš Open in browser:`));
|
|
713
|
+
console.log(chalk.bold(` ${data.url}\n`));
|
|
714
|
+
if (options.browser) {
|
|
715
|
+
try {
|
|
716
|
+
const { execFileSync } = await import('child_process');
|
|
717
|
+
const platform = process.platform;
|
|
718
|
+
// Validate URL scheme before passing to OS ā prevents shell injection
|
|
719
|
+
const parsedUrl = new URL(data.url);
|
|
720
|
+
if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
|
|
721
|
+
throw new Error('Unsafe URL scheme for browser open');
|
|
722
|
+
}
|
|
723
|
+
const safeUrl = parsedUrl.href;
|
|
724
|
+
if (platform === 'win32')
|
|
725
|
+
execFileSync('cmd', ['/c', 'start', '', safeUrl], { stdio: 'ignore', windowsHide: true });
|
|
726
|
+
else if (platform === 'darwin')
|
|
727
|
+
execFileSync('open', [safeUrl], { stdio: 'ignore' });
|
|
728
|
+
else
|
|
729
|
+
execFileSync('xdg-open', [safeUrl], { stdio: 'ignore' });
|
|
730
|
+
}
|
|
731
|
+
catch { /* browser open is optional */ }
|
|
732
|
+
}
|
|
733
|
+
if (data.message) {
|
|
734
|
+
console.log(chalk.gray(` ${data.message}\n`));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
spinner.stop();
|
|
739
|
+
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
740
|
+
console.log(chalk.red(`\nā ${label} import failed: ${errMsg}\n`));
|
|
741
|
+
console.log(chalk.gray('Tip: Make sure your project is pushed first with `vigthoria repo push`\n'));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Clone a public project from Vigthoria Repository
|
|
746
|
+
*/
|
|
747
|
+
async clone(projectUrl, options = {}) {
|
|
748
|
+
// Parse project URL or name
|
|
749
|
+
const projectIdMatch = projectUrl.match(/preview\/(\d+)/);
|
|
750
|
+
const projectId = projectIdMatch ? projectIdMatch[1] : projectUrl;
|
|
751
|
+
const spinner = createSpinner(`Cloning project...`).start();
|
|
752
|
+
try {
|
|
753
|
+
const response = await fetch(`${this.apiBase}/api/repo/clone/${projectId}`, {
|
|
754
|
+
method: 'GET',
|
|
755
|
+
headers: this.isAuthenticated() ? this.getAuthHeaders() : { 'Content-Type': 'application/json' }
|
|
756
|
+
});
|
|
757
|
+
if (!response.ok) {
|
|
758
|
+
const error = await response.json();
|
|
759
|
+
throw new Error(error.error || 'Project not found or not public');
|
|
760
|
+
}
|
|
761
|
+
const data = await response.json();
|
|
762
|
+
spinner.succeed(`Found project: ${chalk.cyan(data.project.project_name)}`);
|
|
763
|
+
// Use pull logic for the rest
|
|
764
|
+
await this.pull(data.project.project_name, options);
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
spinner.stop();
|
|
768
|
+
this.logger.error('Clone failed');
|
|
769
|
+
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
770
|
+
console.log(chalk.red(`\nā Error: ${errMsg}\n`));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|