gent-cli 2.0.0 → 5.0.0

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.
@@ -0,0 +1,379 @@
1
+ /**
2
+ * ============================================================================
3
+ * Merge Engine - Three-Way Smart Merge with Auto-Resolution
4
+ * ============================================================================
5
+ *
6
+ * PURPOSE:
7
+ * Merge two diverged branches using their common ancestor as reference.
8
+ * Minimizes manual conflict resolution through aggressive auto-resolution.
9
+ *
10
+ * THREE-WAY MERGE ALGORITHM:
11
+ * Given: BASE (common ancestor), OURS (current branch), THEIRS (incoming)
12
+ *
13
+ * 1. Compute diff: BASE → OURS (what we changed)
14
+ * 2. Compute diff: BASE → THEIRS (what they changed)
15
+ * 3. Build "change regions" from each diff (contiguous modified areas)
16
+ * 4. Walk base line-by-line, apply resolution rules:
17
+ *
18
+ * AUTO-RESOLUTION RULES (in priority order):
19
+ * ┌─────────────────────────────────────────────────────────────────┐
20
+ * │ Scenario │ Result │
21
+ * ├─────────────────────────────────────────────────────────────────┤
22
+ * │ Only OURS modified region │ Take OURS │
23
+ * │ Only THEIRS modified region │ Take THEIRS │
24
+ * │ Both modified identically │ Take either (same) │
25
+ * │ Both modified, same-length, │ │
26
+ * │ non-overlapping line changes │ Line-by-line merge │
27
+ * │ Whitespace-only difference │ Take OURS │
28
+ * │ Modify/delete conflict │ Keep modified (smart) │
29
+ * │ True overlapping conflict │ Insert conflict markers │
30
+ * └─────────────────────────────────────────────────────────────────┘
31
+ *
32
+ * MERGE BASE FINDER:
33
+ * Walks parent chain from both branch tips.
34
+ * Collects all ancestors of branch A, then walks B until first hit.
35
+ * Returns the most recent common ancestor. O(n) where n = total commits.
36
+ *
37
+ * TREE-LEVEL MERGE:
38
+ * Compares file presence/absence + blob hashes across base/ours/theirs trees.
39
+ * For each file: decide add/delete/modify/conflict independently.
40
+ * Only files with different blob hashes on both sides trigger content merge.
41
+ *
42
+ * CONFLICT MARKERS FORMAT:
43
+ * <<<<<<< ours
44
+ * [our version of conflicting lines]
45
+ * =======
46
+ * [their version of conflicting lines]
47
+ * >>>>>>> theirs
48
+ *
49
+ * BACKEND EXPECTATIONS:
50
+ * POST /api/repos/:id/merge/
51
+ * { sourceBranch, targetBranch, strategy: "three-way" }
52
+ * Backend can use same algorithm or delegate to client.
53
+ * Backend should store merge commit with two parents (parent + mergeParent).
54
+ *
55
+ * ============================================================================
56
+ */
57
+
58
+ const { splitLines } = require('./hash-engine');
59
+ const { buildLineOperations } = require('./diff-engine');
60
+ const { readBlobAsString, treeToMap, storeBlob } = require('./hash-engine');
61
+
62
+ // ─── Line-Level 3-Way Merge ─────────────────────────────
63
+
64
+ /**
65
+ * Three-way merge of line arrays.
66
+ * @param {String[]} baseLines
67
+ * @param {String[]} oursLines
68
+ * @param {String[]} theirsLines
69
+ * @returns {{merged: String[], conflicts: Array, hasConflicts: Boolean}}
70
+ */
71
+ function threeWayMerge(baseLines, oursLines, theirsLines) {
72
+ const ourOps = buildLineOperations(baseLines, oursLines);
73
+ const theirOps = buildLineOperations(baseLines, theirsLines);
74
+
75
+ const ourRegions = buildChangeRegions(ourOps);
76
+ const theirRegions = buildChangeRegions(theirOps);
77
+
78
+ const ourMap = mapRegionsByBase(ourRegions);
79
+ const theirMap = mapRegionsByBase(theirRegions);
80
+
81
+ const allStarts = new Set([...ourMap.keys(), ...theirMap.keys()]);
82
+ const merged = [];
83
+ const conflicts = [];
84
+ let baseIdx = 0;
85
+
86
+ while (baseIdx <= baseLines.length) {
87
+ if (allStarts.has(baseIdx)) {
88
+ const ourR = ourMap.get(baseIdx);
89
+ const theirR = theirMap.get(baseIdx);
90
+
91
+ if (ourR && theirR) {
92
+ const ourText = ourR.newLines.join('\n');
93
+ const theirText = theirR.newLines.join('\n');
94
+
95
+ if (ourText === theirText) {
96
+ merged.push(...ourR.newLines);
97
+ } else {
98
+ const sub = subMergeRegion(ourR.oldLines, ourR.newLines, theirR.newLines);
99
+ if (sub.hasConflicts) {
100
+ conflicts.push({
101
+ baseLine: baseIdx,
102
+ baseContent: ourR.oldLines,
103
+ oursContent: ourR.newLines,
104
+ theirsContent: theirR.newLines
105
+ });
106
+ merged.push('<<<<<<< ours');
107
+ merged.push(...ourR.newLines);
108
+ merged.push('=======');
109
+ merged.push(...theirR.newLines);
110
+ merged.push('>>>>>>> theirs');
111
+ } else {
112
+ merged.push(...sub.lines);
113
+ }
114
+ }
115
+ const skip = Math.max(ourR.oldLines.length, theirR.oldLines.length);
116
+ baseIdx += skip;
117
+ continue;
118
+ } else if (ourR) {
119
+ merged.push(...ourR.newLines);
120
+ baseIdx += ourR.oldLines.length;
121
+ continue;
122
+ } else if (theirR) {
123
+ merged.push(...theirR.newLines);
124
+ baseIdx += theirR.oldLines.length;
125
+ continue;
126
+ }
127
+ }
128
+
129
+ if (baseIdx < baseLines.length) {
130
+ merged.push(baseLines[baseIdx]);
131
+ }
132
+ baseIdx++;
133
+ }
134
+
135
+ return { merged, conflicts, hasConflicts: conflicts.length > 0 };
136
+ }
137
+
138
+ // ─── Region Building ────────────────────────────────────
139
+
140
+ /**
141
+ * Extract contiguous change regions from diff ops, anchored to base line indices.
142
+ * @param {Array} ops
143
+ * @returns {Array<{baseStart, oldLines, newLines}>}
144
+ */
145
+ function buildChangeRegions(ops) {
146
+ const regions = [];
147
+ let current = null;
148
+ let baseIdx = 0;
149
+
150
+ for (const op of ops) {
151
+ if (op.type === 'equal') {
152
+ if (current) { regions.push(current); current = null; }
153
+ baseIdx++;
154
+ } else if (op.type === 'delete') {
155
+ if (!current) current = { baseStart: baseIdx, oldLines: [], newLines: [] };
156
+ current.oldLines.push(op.content);
157
+ baseIdx++;
158
+ } else if (op.type === 'insert') {
159
+ if (!current) current = { baseStart: baseIdx, oldLines: [], newLines: [] };
160
+ current.newLines.push(op.content);
161
+ }
162
+ }
163
+ if (current) regions.push(current);
164
+ return regions;
165
+ }
166
+
167
+ /**
168
+ * Map regions by baseStart index.
169
+ * @param {Array} regions
170
+ * @returns {Map}
171
+ */
172
+ function mapRegionsByBase(regions) {
173
+ const map = new Map();
174
+ for (const r of regions) map.set(r.baseStart, r);
175
+ return map;
176
+ }
177
+
178
+ // ─── Sub-Merge (fine-grained) ───────────────────────────
179
+
180
+ /**
181
+ * Attempt line-by-line merge within a region.
182
+ * Catches non-overlapping edits inside same region.
183
+ * @param {String[]} baseLines
184
+ * @param {String[]} oursLines
185
+ * @param {String[]} theirsLines
186
+ * @returns {{lines: String[], hasConflicts: Boolean}}
187
+ */
188
+ function subMergeRegion(baseLines, oursLines, theirsLines) {
189
+ if (oursLines.length === 0 && theirsLines.length === 0) {
190
+ return { lines: [], hasConflicts: false };
191
+ }
192
+
193
+ if (oursLines.length === theirsLines.length) {
194
+ const result = [];
195
+ for (let i = 0; i < oursLines.length; i++) {
196
+ const baseLine = i < baseLines.length ? baseLines[i] : null;
197
+ const ourLine = oursLines[i];
198
+ const theirLine = theirsLines[i];
199
+
200
+ if (ourLine === theirLine) { result.push(ourLine); continue; }
201
+ if (ourLine === baseLine) { result.push(theirLine); continue; }
202
+ if (theirLine === baseLine) { result.push(ourLine); continue; }
203
+ // Whitespace-only diff → take ours
204
+ if (ourLine.trim() === theirLine.trim()) { result.push(ourLine); continue; }
205
+ return { lines: [], hasConflicts: true };
206
+ }
207
+ return { lines: result, hasConflicts: false };
208
+ }
209
+
210
+ return { lines: [], hasConflicts: true };
211
+ }
212
+
213
+ // ─── File-Level Merge ───────────────────────────────────
214
+
215
+ /**
216
+ * Merge single file content strings.
217
+ * @param {String} baseContent
218
+ * @param {String} oursContent
219
+ * @param {String} theirsContent
220
+ * @returns {{content: String, hasConflicts: Boolean, conflicts: Array}}
221
+ */
222
+ function mergeFileContent(baseContent, oursContent, theirsContent) {
223
+ const base = splitLines(baseContent || '');
224
+ const ours = splitLines(oursContent || '');
225
+ const theirs = splitLines(theirsContent || '');
226
+ const result = threeWayMerge(base, ours, theirs);
227
+ return { content: result.merged.join('\n'), hasConflicts: result.hasConflicts, conflicts: result.conflicts };
228
+ }
229
+
230
+ /**
231
+ * Quick auto-merge with fast-path shortcuts.
232
+ * @param {String} baseText
233
+ * @param {String} oursText
234
+ * @param {String} theirsText
235
+ * @returns {{mergedText: String, hasConflicts: Boolean, conflicts: Array, algorithm: String, confidence: Number}}
236
+ */
237
+ function autoMerge(baseText, oursText, theirsText) {
238
+ // Fast paths
239
+ if (oursText === theirsText) return { mergedText: oursText, hasConflicts: false, conflicts: [], algorithm: 'three-way-line-v2', confidence: 1 };
240
+ if (oursText === baseText) return { mergedText: theirsText, hasConflicts: false, conflicts: [], algorithm: 'three-way-line-v2', confidence: 1 };
241
+ if (theirsText === baseText) return { mergedText: oursText, hasConflicts: false, conflicts: [], algorithm: 'three-way-line-v2', confidence: 1 };
242
+
243
+ const result = mergeFileContent(baseText, oursText, theirsText);
244
+ const maxLen = Math.max(splitLines(baseText).length, 1);
245
+ const confidence = result.hasConflicts ? Math.max(0, 1 - result.conflicts.length / maxLen) : 1;
246
+
247
+ return {
248
+ mergedText: result.content,
249
+ hasConflicts: result.hasConflicts,
250
+ conflicts: result.conflicts,
251
+ algorithm: 'three-way-line-v2',
252
+ confidence
253
+ };
254
+ }
255
+
256
+ // ─── Tree-Level Merge ───────────────────────────────────
257
+
258
+ /**
259
+ * Merge two tree snapshots against common-base tree.
260
+ * Handles file add/delete/modify across branches.
261
+ * @param {String} gentPath
262
+ * @param {Array} baseEntries
263
+ * @param {Array} oursEntries
264
+ * @param {Array} theirsEntries
265
+ * @returns {Promise<{mergedEntries: Array, conflicts: Array, hasConflicts: Boolean}>}
266
+ */
267
+ async function mergeTreeEntries(gentPath, baseEntries, oursEntries, theirsEntries) {
268
+ const baseMap = treeToMap(baseEntries);
269
+ const oursMap = treeToMap(oursEntries);
270
+ const theirsMap = treeToMap(theirsEntries);
271
+
272
+ const allFiles = new Set([...baseMap.keys(), ...oursMap.keys(), ...theirsMap.keys()]);
273
+ const mergedEntries = [];
274
+ const conflicts = [];
275
+
276
+ for (const filePath of allFiles) {
277
+ const bH = baseMap.get(filePath) || null;
278
+ const oH = oursMap.get(filePath) || null;
279
+ const tH = theirsMap.get(filePath) || null;
280
+
281
+ // No change or both identical
282
+ if (oH === tH) {
283
+ if (oH) mergedEntries.push({ mode: '100644', name: filePath, hash: oH, type: 'blob' });
284
+ continue;
285
+ }
286
+
287
+ // Only one side changed
288
+ if (oH === bH && tH !== bH) {
289
+ if (tH) mergedEntries.push({ mode: '100644', name: filePath, hash: tH, type: 'blob' });
290
+ continue;
291
+ }
292
+ if (tH === bH && oH !== bH) {
293
+ if (oH) mergedEntries.push({ mode: '100644', name: filePath, hash: oH, type: 'blob' });
294
+ continue;
295
+ }
296
+
297
+ // Both changed differently — content merge
298
+ if (oH && tH && bH) {
299
+ const [baseC, oursC, theirsC] = await Promise.all([
300
+ readBlobAsString(gentPath, bH),
301
+ readBlobAsString(gentPath, oH),
302
+ readBlobAsString(gentPath, tH)
303
+ ]);
304
+ const result = mergeFileContent(baseC, oursC, theirsC);
305
+ const mergedHash = await storeBlob(gentPath, result.content);
306
+ mergedEntries.push({ mode: '100644', name: filePath, hash: mergedHash, type: 'blob' });
307
+ if (result.hasConflicts) conflicts.push({ file: filePath, type: 'content', details: result.conflicts });
308
+ continue;
309
+ }
310
+
311
+ // Modify/delete conflict — keep modified version (smart default)
312
+ if (!oH && tH && tH !== bH) {
313
+ conflicts.push({ file: filePath, type: 'modify-delete', deletedBy: 'ours', modifiedBy: 'theirs' });
314
+ mergedEntries.push({ mode: '100644', name: filePath, hash: tH, type: 'blob' });
315
+ continue;
316
+ }
317
+ if (!tH && oH && oH !== bH) {
318
+ conflicts.push({ file: filePath, type: 'modify-delete', deletedBy: 'theirs', modifiedBy: 'ours' });
319
+ mergedEntries.push({ mode: '100644', name: filePath, hash: oH, type: 'blob' });
320
+ continue;
321
+ }
322
+
323
+ // Both added (no base) with different content
324
+ if (!bH && oH && tH) {
325
+ const [oursC, theirsC] = await Promise.all([
326
+ readBlobAsString(gentPath, oH),
327
+ readBlobAsString(gentPath, tH)
328
+ ]);
329
+ const result = mergeFileContent('', oursC, theirsC);
330
+ const mergedHash = await storeBlob(gentPath, result.content);
331
+ mergedEntries.push({ mode: '100644', name: filePath, hash: mergedHash, type: 'blob' });
332
+ if (result.hasConflicts) conflicts.push({ file: filePath, type: 'add-add', details: result.conflicts });
333
+ }
334
+ }
335
+
336
+ return { mergedEntries, conflicts, hasConflicts: conflicts.length > 0 };
337
+ }
338
+
339
+ // ─── Merge Base Finder ──────────────────────────────────
340
+
341
+ /**
342
+ * Find common ancestor of two branch tips by walking parents.
343
+ * @param {Array} commits
344
+ * @param {String} hashA
345
+ * @param {String} hashB
346
+ * @returns {String|null}
347
+ */
348
+ function findMergeBase(commits, hashA, hashB) {
349
+ const commitMap = new Map();
350
+ for (const c of commits) commitMap.set(c.hash, c);
351
+
352
+ // Collect all ancestors of A
353
+ const ancestorsA = new Set();
354
+ let cur = hashA;
355
+ while (cur) {
356
+ ancestorsA.add(cur);
357
+ const c = commitMap.get(cur);
358
+ cur = c ? c.parent : null;
359
+ }
360
+
361
+ // Walk B ancestors → first hit in A's set = merge base
362
+ cur = hashB;
363
+ while (cur) {
364
+ if (ancestorsA.has(cur)) return cur;
365
+ const c = commitMap.get(cur);
366
+ cur = c ? c.parent : null;
367
+ }
368
+ return null;
369
+ }
370
+
371
+ module.exports = {
372
+ threeWayMerge,
373
+ mergeFileContent,
374
+ autoMerge,
375
+ mergeTreeEntries,
376
+ findMergeBase,
377
+ buildChangeRegions,
378
+ subMergeRegion
379
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Object Store
3
+ * Persist content-addressed blobs inside .gent/objects.
4
+ */
5
+
6
+ const fs = require('fs').promises;
7
+ const path = require('path');
8
+ const { pathExists } = require('./fileSystem');
9
+
10
+ /**
11
+ * Compute object file path for hash.
12
+ * @param {String} gentPath
13
+ * @param {String} hash
14
+ * @returns {String}
15
+ */
16
+ function getObjectPath(gentPath, hash) {
17
+ return path.join(gentPath, 'objects', `${hash}.blob`);
18
+ }
19
+
20
+ /**
21
+ * Save object if missing.
22
+ * @param {String} gentPath
23
+ * @param {String} hash
24
+ * @param {Buffer} content
25
+ */
26
+ async function writeObject(gentPath, hash, content) {
27
+ const objectPath = getObjectPath(gentPath, hash);
28
+
29
+ if (!await pathExists(objectPath)) {
30
+ await fs.writeFile(objectPath, content);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Read object as buffer.
36
+ * @param {String} gentPath
37
+ * @param {String} hash
38
+ * @returns {Promise<Buffer|null>}
39
+ */
40
+ async function readObject(gentPath, hash) {
41
+ const objectPath = getObjectPath(gentPath, hash);
42
+
43
+ if (!await pathExists(objectPath)) {
44
+ return null;
45
+ }
46
+
47
+ return fs.readFile(objectPath);
48
+ }
49
+
50
+ module.exports = {
51
+ getObjectPath,
52
+ writeObject,
53
+ readObject
54
+ };
@@ -1,121 +0,0 @@
1
- /**
2
- * Create Command - Create a new cloud repository
3
- * Creates a repository on the cloud and optionally initializes locally
4
- */
5
-
6
- const chalk = require('chalk');
7
- const inquirer = require('inquirer');
8
- const ora = require('ora');
9
- const path = require('path');
10
- const repoService = require('../services/repo-service');
11
- const authStorage = require('../utils/auth-storage');
12
- const { ensureDir, writeJSON } = require('../utils/fileSystem');
13
- const { GENT_DIR } = require('../utils/constants');
14
- const { addRemote } = require('../utils/cloud-sync');
15
-
16
- /**
17
- * Create a new repository on the cloud
18
- * @param {string} repoName - Repository name
19
- * @param {Object} options - Command options
20
- */
21
- async function create(repoName, options) {
22
- try {
23
- // Check authentication
24
- const user = await authStorage.getUser();
25
- if (!user) {
26
- console.error(chalk.red('Error: You must be logged in to create a repository'));
27
- console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
28
- process.exit(1);
29
- }
30
-
31
- // Validate repository name
32
- if (!repoName) {
33
- console.error(chalk.red('Error: Repository name is required'));
34
- console.log(chalk.yellow('Usage:'), chalk.cyan('gent create <repo-name>'));
35
- process.exit(1);
36
- }
37
-
38
- // Prompt for additional details if not provided
39
- let description = options.description || '';
40
- let isPrivate = options.private || false;
41
-
42
- if (!options.yes) {
43
- const answers = await inquirer.prompt([
44
- {
45
- type: 'input',
46
- name: 'description',
47
- message: 'Repository description (optional):',
48
- default: description
49
- },
50
- {
51
- type: 'confirm',
52
- name: 'isPrivate',
53
- message: 'Make repository private?',
54
- default: isPrivate
55
- },
56
- {
57
- type: 'confirm',
58
- name: 'initLocal',
59
- message: 'Initialize local repository and link remote?',
60
- default: true
61
- }
62
- ]);
63
-
64
- description = answers.description;
65
- isPrivate = answers.isPrivate;
66
- options.initLocal = answers.initLocal;
67
- }
68
-
69
- // Create repository
70
- const spinner = ora('Creating repository...').start();
71
-
72
- const repository = await repoService.createRepository(
73
- repoName,
74
- description,
75
- isPrivate,
76
- 'main'
77
- );
78
-
79
- const ownerId = repository.owner_id || repository.owner?.id || repository.owner;
80
- const name = repository.name || repository.project_name || repoName;
81
-
82
- spinner.succeed(chalk.green(`Repository created: ${name}`));
83
-
84
- console.log(chalk.cyan('\nRepository Details:'));
85
- console.log(chalk.gray(' Name:'), name);
86
- console.log(chalk.gray(' Owner:'), ownerId);
87
- console.log(chalk.gray(' Private:'), repository.is_private ? 'Yes' : 'No');
88
- console.log(chalk.gray(' Description:'), repository.description || '(none)');
89
- console.log(chalk.gray(' Created:'), new Date(repository.created_at || repository.created || new Date()).toLocaleString());
90
-
91
- // Initialize local repository if requested
92
- if (options.initLocal) {
93
- const cwd = process.cwd();
94
- const gentPath = path.join(cwd, GENT_DIR);
95
-
96
- // Create .gent directory
97
- await ensureDir(gentPath);
98
- await ensureDir(path.join(gentPath, 'objects'));
99
- await ensureDir(path.join(gentPath, 'refs', 'heads'));
100
-
101
- // Add remote
102
- await addRemote('origin', ownerId, name, cwd);
103
-
104
- console.log(chalk.green('\n✓ Local repository initialized and remote added'));
105
- console.log(chalk.yellow('\nNext steps:'));
106
- console.log(chalk.cyan(' gent add <files>'), chalk.gray('- Add files to staging'));
107
- console.log(chalk.cyan(' gent commit -m "message"'), chalk.gray('- Commit changes'));
108
- console.log(chalk.cyan(' gent push'), chalk.gray('- Push to cloud'));
109
- } else {
110
- console.log(chalk.yellow('\nTo clone this repository:'));
111
- console.log(chalk.cyan(` gent clone ${repository.owner_id}/${repository.name}`));
112
- }
113
-
114
- } catch (error) {
115
- console.error(chalk.red('Failed to create repository'));
116
- console.error(chalk.red('Error:'), error.message);
117
- process.exit(1);
118
- }
119
- }
120
-
121
- module.exports = create;
@@ -1,67 +0,0 @@
1
- /**
2
- * List Command - List all cloud repositories
3
- * Displays all repositories owned by the authenticated user
4
- */
5
-
6
- const chalk = require('chalk');
7
- const ora = require('ora');
8
- const repoService = require('../services/repo-service');
9
- const authStorage = require('../utils/auth-storage');
10
-
11
- /**
12
- * Format date for display
13
- * @param {string} dateString - ISO date string
14
- * @returns {string} Formatted date
15
- */
16
- function formatDate(dateString) {
17
- const date = new Date(dateString);
18
- return date.toLocaleDateString();
19
- }
20
-
21
- /**
22
- * List all repositories owned by the authenticated user
23
- * @param {Object} options - Command options
24
- */
25
- async function list(options) {
26
- try {
27
- // Check authentication
28
- const user = await authStorage.getUser();
29
- if (!user) {
30
- console.error(chalk.red('Error: You must be logged in to list repositories'));
31
- console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
32
- process.exit(1);
33
- }
34
-
35
- // Fetch repositories
36
- const spinner = ora('Fetching repositories...').start();
37
- const repositories = await repoService.listRepositories();
38
- spinner.stop();
39
-
40
- if (repositories.length === 0) {
41
- console.log(chalk.yellow('No repositories found'));
42
- console.log(chalk.gray('Create a new repository with:'), chalk.cyan('gent create <repo-name>'));
43
- return;
44
- }
45
-
46
- console.log(chalk.cyan(`\nYour Repositories (${repositories.length}):\n`));
47
-
48
- repositories.forEach(repo => {
49
- const privacy = repo.is_private ? chalk.red('🔒 Private') : chalk.green('🌐 Public');
50
- console.log(chalk.bold(repo.name), privacy);
51
- if (repo.description) {
52
- console.log(chalk.gray(` ${repo.description}`));
53
- }
54
- console.log(chalk.gray(` Owner: ${repo.owner_email}`));
55
- console.log(chalk.gray(` Created: ${formatDate(repo.created_at)}`));
56
- console.log(chalk.gray(` Default branch: ${repo.default_branch}`));
57
- console.log(chalk.gray(` Clone: gent clone ${repo.owner_id}/${repo.name}\n`));
58
- });
59
-
60
- } catch (error) {
61
- console.error(chalk.red('Failed to list repositories'));
62
- console.error(chalk.red('Error:'), error.message);
63
- process.exit(1);
64
- }
65
- }
66
-
67
- module.exports = list;