@vibeversion/cli 0.1.1 → 0.2.1
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/index.js +764 -32
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,8 +14,8 @@ var ApiClient = class {
|
|
|
14
14
|
this.baseUrl = baseUrl;
|
|
15
15
|
this.token = token;
|
|
16
16
|
}
|
|
17
|
-
async request(method,
|
|
18
|
-
const url = `${this.baseUrl}${
|
|
17
|
+
async request(method, path5, body) {
|
|
18
|
+
const url = `${this.baseUrl}${path5}`;
|
|
19
19
|
const headers = {
|
|
20
20
|
"Content-Type": "application/json",
|
|
21
21
|
Accept: "application/json"
|
|
@@ -51,9 +51,15 @@ var ApiClient = class {
|
|
|
51
51
|
async listProjects() {
|
|
52
52
|
return this.request("GET", "/api/projects");
|
|
53
53
|
}
|
|
54
|
+
async listVersions(projectUlid) {
|
|
55
|
+
return this.request("GET", `/api/projects/${projectUlid}/versions`);
|
|
56
|
+
}
|
|
54
57
|
async createVersion(projectUlid, data) {
|
|
55
58
|
return this.request("POST", `/api/projects/${projectUlid}/versions`, data);
|
|
56
59
|
}
|
|
60
|
+
async triggerDeploy(projectUlid, data) {
|
|
61
|
+
return this.request("POST", `/api/projects/${projectUlid}/deploy`, data);
|
|
62
|
+
}
|
|
57
63
|
};
|
|
58
64
|
var ApiError = class extends Error {
|
|
59
65
|
constructor(status, message) {
|
|
@@ -67,21 +73,60 @@ var ApiError = class extends Error {
|
|
|
67
73
|
import fs from "fs";
|
|
68
74
|
import path from "path";
|
|
69
75
|
import os from "os";
|
|
70
|
-
var CONFIG_DIR = path.join(os.homedir(), ".
|
|
76
|
+
var CONFIG_DIR = path.join(os.homedir(), ".vv");
|
|
71
77
|
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
72
|
-
|
|
78
|
+
var LEGACY_CONFIG_DIR = path.join(os.homedir(), ".vibeversion");
|
|
79
|
+
var LEGACY_CONFIG_FILE = path.join(LEGACY_CONFIG_DIR, "config.json");
|
|
80
|
+
function readAuthConfig(filePath) {
|
|
73
81
|
try {
|
|
74
|
-
const raw = fs.readFileSync(
|
|
82
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
75
83
|
return JSON.parse(raw);
|
|
76
84
|
} catch {
|
|
77
85
|
return null;
|
|
78
86
|
}
|
|
79
87
|
}
|
|
88
|
+
function migrateLegacyAuthConfig() {
|
|
89
|
+
if (!fs.existsSync(LEGACY_CONFIG_FILE)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const legacyConfig = readAuthConfig(LEGACY_CONFIG_FILE);
|
|
93
|
+
if (!legacyConfig) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
97
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(legacyConfig, null, 2) + "\n", {
|
|
98
|
+
mode: 384
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
fs.unlinkSync(LEGACY_CONFIG_FILE);
|
|
102
|
+
fs.rmdirSync(LEGACY_CONFIG_DIR);
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
return legacyConfig;
|
|
106
|
+
}
|
|
107
|
+
function cleanupLegacyAuthConfig() {
|
|
108
|
+
try {
|
|
109
|
+
fs.unlinkSync(LEGACY_CONFIG_FILE);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
fs.rmdirSync(LEGACY_CONFIG_DIR);
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function getAuthConfig() {
|
|
118
|
+
const currentConfig = readAuthConfig(CONFIG_FILE);
|
|
119
|
+
if (currentConfig) {
|
|
120
|
+
return currentConfig;
|
|
121
|
+
}
|
|
122
|
+
return migrateLegacyAuthConfig();
|
|
123
|
+
}
|
|
80
124
|
function saveAuthConfig(config) {
|
|
81
125
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
82
126
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
83
127
|
mode: 384
|
|
84
128
|
});
|
|
129
|
+
cleanupLegacyAuthConfig();
|
|
85
130
|
}
|
|
86
131
|
|
|
87
132
|
// src/commands/login.ts
|
|
@@ -119,38 +164,102 @@ async function loginCommand() {
|
|
|
119
164
|
}
|
|
120
165
|
|
|
121
166
|
// src/commands/init.ts
|
|
122
|
-
import { select } from "@inquirer/prompts";
|
|
123
|
-
import
|
|
167
|
+
import { confirm, select } from "@inquirer/prompts";
|
|
168
|
+
import chalk3 from "chalk";
|
|
124
169
|
import ora2 from "ora";
|
|
125
170
|
|
|
126
171
|
// src/lib/project.ts
|
|
127
172
|
import fs2 from "fs";
|
|
128
173
|
import path2 from "path";
|
|
129
|
-
var PROJECT_DIR = ".
|
|
174
|
+
var PROJECT_DIR = ".vv";
|
|
175
|
+
var LEGACY_PROJECT_DIR = ".vibeversion";
|
|
130
176
|
var PROJECT_FILE = "config.json";
|
|
131
177
|
function getProjectConfigPath(dir) {
|
|
132
178
|
return path2.join(dir, PROJECT_DIR, PROJECT_FILE);
|
|
133
179
|
}
|
|
134
|
-
function
|
|
180
|
+
function getLegacyProjectConfigPath(dir) {
|
|
181
|
+
return path2.join(dir, LEGACY_PROJECT_DIR, PROJECT_FILE);
|
|
182
|
+
}
|
|
183
|
+
function readProjectConfig(filePath) {
|
|
135
184
|
try {
|
|
136
|
-
const raw = fs2.readFileSync(
|
|
185
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
137
186
|
return JSON.parse(raw);
|
|
138
187
|
} catch {
|
|
139
188
|
return null;
|
|
140
189
|
}
|
|
141
190
|
}
|
|
191
|
+
function migrateProjectConfig(dir) {
|
|
192
|
+
const currentPath = getProjectConfigPath(dir);
|
|
193
|
+
if (fs2.existsSync(currentPath)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const legacyDir = path2.join(dir, LEGACY_PROJECT_DIR);
|
|
197
|
+
const legacyPath = getLegacyProjectConfigPath(dir);
|
|
198
|
+
if (!fs2.existsSync(legacyPath)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const projectDir = path2.join(dir, PROJECT_DIR);
|
|
202
|
+
try {
|
|
203
|
+
fs2.renameSync(legacyDir, projectDir);
|
|
204
|
+
return;
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
const legacyConfig = readProjectConfig(legacyPath);
|
|
208
|
+
if (!legacyConfig) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
fs2.mkdirSync(projectDir, { recursive: true });
|
|
212
|
+
fs2.writeFileSync(currentPath, JSON.stringify(legacyConfig, null, 2) + "\n");
|
|
213
|
+
try {
|
|
214
|
+
fs2.rmSync(legacyDir, { recursive: true, force: true });
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function cleanupLegacyProjectDir(dir) {
|
|
219
|
+
const legacyDir = path2.join(dir, LEGACY_PROJECT_DIR);
|
|
220
|
+
if (!fs2.existsSync(legacyDir)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
fs2.rmSync(legacyDir, { recursive: true, force: true });
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function getProjectConfig(dir = process.cwd()) {
|
|
229
|
+
migrateProjectConfig(dir);
|
|
230
|
+
const currentConfig = readProjectConfig(getProjectConfigPath(dir));
|
|
231
|
+
if (currentConfig) {
|
|
232
|
+
return currentConfig;
|
|
233
|
+
}
|
|
234
|
+
const legacyConfig = readProjectConfig(getLegacyProjectConfigPath(dir));
|
|
235
|
+
if (!legacyConfig) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
saveProjectConfig(legacyConfig, dir);
|
|
239
|
+
return legacyConfig;
|
|
240
|
+
}
|
|
142
241
|
function saveProjectConfig(config, dir = process.cwd()) {
|
|
242
|
+
migrateProjectConfig(dir);
|
|
143
243
|
const projectDir = path2.join(dir, PROJECT_DIR);
|
|
144
244
|
fs2.mkdirSync(projectDir, { recursive: true });
|
|
145
245
|
fs2.writeFileSync(path2.join(projectDir, PROJECT_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
246
|
+
cleanupLegacyProjectDir(dir);
|
|
146
247
|
}
|
|
147
248
|
function ensureGitignore(dir = process.cwd()) {
|
|
148
249
|
const gitignorePath = path2.join(dir, ".gitignore");
|
|
149
|
-
const entry = ".
|
|
250
|
+
const entry = ".vv/";
|
|
251
|
+
const legacyEntry = ".vibeversion/";
|
|
150
252
|
try {
|
|
151
253
|
const content = fs2.readFileSync(gitignorePath, "utf-8");
|
|
152
|
-
if (content.includes(entry))
|
|
153
|
-
|
|
254
|
+
if (content.includes(entry)) {
|
|
255
|
+
if (content.includes(legacyEntry)) {
|
|
256
|
+
const updated2 = content.replaceAll(legacyEntry, entry);
|
|
257
|
+
fs2.writeFileSync(gitignorePath, updated2.trimEnd() + "\n");
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const updated = content.includes(legacyEntry) ? content.replaceAll(legacyEntry, entry) : content;
|
|
262
|
+
fs2.writeFileSync(gitignorePath, updated.trimEnd() + "\n" + entry + "\n");
|
|
154
263
|
} catch {
|
|
155
264
|
fs2.writeFileSync(gitignorePath, entry + "\n");
|
|
156
265
|
}
|
|
@@ -158,6 +267,7 @@ function ensureGitignore(dir = process.cwd()) {
|
|
|
158
267
|
|
|
159
268
|
// src/lib/git.ts
|
|
160
269
|
import fs3 from "fs";
|
|
270
|
+
import path3 from "path";
|
|
161
271
|
import git from "isomorphic-git";
|
|
162
272
|
var AUTHOR = { name: "Vibeversion", email: "cli@vibeversion.com" };
|
|
163
273
|
async function ensureRepo(dir) {
|
|
@@ -185,6 +295,10 @@ async function stageAll(dir) {
|
|
|
185
295
|
}
|
|
186
296
|
}
|
|
187
297
|
}
|
|
298
|
+
async function isWorktreeClean(dir) {
|
|
299
|
+
const statusMatrix = await git.statusMatrix({ fs: fs3, dir });
|
|
300
|
+
return statusMatrix.every(([, head, workdir, stage]) => head === workdir && workdir === stage);
|
|
301
|
+
}
|
|
188
302
|
async function commit(dir, message) {
|
|
189
303
|
return git.commit({
|
|
190
304
|
fs: fs3,
|
|
@@ -243,6 +357,168 @@ async function diffStats(dir) {
|
|
|
243
357
|
}
|
|
244
358
|
return { filesChanged, insertions, deletions };
|
|
245
359
|
}
|
|
360
|
+
var MAX_DIFF_BYTES = 50 * 1024;
|
|
361
|
+
async function perFileStats(dir) {
|
|
362
|
+
const first = await isFirstCommit(dir);
|
|
363
|
+
if (first) return [];
|
|
364
|
+
const headOid = await git.resolveRef({ fs: fs3, dir, ref: "HEAD" });
|
|
365
|
+
const headCommit = await git.readCommit({ fs: fs3, dir, oid: headOid });
|
|
366
|
+
const currentTree = headCommit.commit.tree;
|
|
367
|
+
let parentTree = null;
|
|
368
|
+
if (headCommit.commit.parent.length > 0) {
|
|
369
|
+
const parentCommit = await git.readCommit({ fs: fs3, dir, oid: headCommit.commit.parent[0] });
|
|
370
|
+
parentTree = parentCommit.commit.tree;
|
|
371
|
+
}
|
|
372
|
+
const currentFiles = await walkTree(dir, currentTree);
|
|
373
|
+
const parentFiles = parentTree ? await walkTree(dir, parentTree) : /* @__PURE__ */ new Map();
|
|
374
|
+
const stats = [];
|
|
375
|
+
for (const [filepath, currentOid] of currentFiles) {
|
|
376
|
+
const parentOid = parentFiles.get(filepath);
|
|
377
|
+
if (!parentOid) {
|
|
378
|
+
const content = await readBlob(dir, currentOid);
|
|
379
|
+
stats.push({ path: filepath, insertions: countLines(content), deletions: 0 });
|
|
380
|
+
} else if (parentOid !== currentOid) {
|
|
381
|
+
const oldContent = await readBlob(dir, parentOid);
|
|
382
|
+
const newContent = await readBlob(dir, currentOid);
|
|
383
|
+
const oldLines = countLines(oldContent);
|
|
384
|
+
const newLines = countLines(newContent);
|
|
385
|
+
stats.push({
|
|
386
|
+
path: filepath,
|
|
387
|
+
insertions: Math.max(0, newLines - oldLines),
|
|
388
|
+
deletions: Math.max(0, oldLines - newLines)
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const [filepath] of parentFiles) {
|
|
393
|
+
if (!currentFiles.has(filepath)) {
|
|
394
|
+
const content = await readBlob(dir, parentFiles.get(filepath));
|
|
395
|
+
stats.push({ path: filepath, insertions: 0, deletions: countLines(content) });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return stats;
|
|
399
|
+
}
|
|
400
|
+
async function generateDiffText(dir) {
|
|
401
|
+
const first = await isFirstCommit(dir);
|
|
402
|
+
if (first) return null;
|
|
403
|
+
const headOid = await git.resolveRef({ fs: fs3, dir, ref: "HEAD" });
|
|
404
|
+
const headCommit = await git.readCommit({ fs: fs3, dir, oid: headOid });
|
|
405
|
+
const currentTree = headCommit.commit.tree;
|
|
406
|
+
let parentTree = null;
|
|
407
|
+
if (headCommit.commit.parent.length > 0) {
|
|
408
|
+
const parentCommit = await git.readCommit({ fs: fs3, dir, oid: headCommit.commit.parent[0] });
|
|
409
|
+
parentTree = parentCommit.commit.tree;
|
|
410
|
+
}
|
|
411
|
+
const currentFiles = await walkTree(dir, currentTree);
|
|
412
|
+
const parentFiles = parentTree ? await walkTree(dir, parentTree) : /* @__PURE__ */ new Map();
|
|
413
|
+
const chunks = [];
|
|
414
|
+
let totalBytes = 0;
|
|
415
|
+
for (const [filepath, currentOid] of currentFiles) {
|
|
416
|
+
const parentOid = parentFiles.get(filepath);
|
|
417
|
+
if (!parentOid) {
|
|
418
|
+
const content = await readBlob(dir, currentOid);
|
|
419
|
+
const lines = content.split("\n").map((l) => `+${l}`);
|
|
420
|
+
const chunk = `--- /dev/null
|
|
421
|
+
+++ b/${filepath}
|
|
422
|
+
${lines.join("\n")}
|
|
423
|
+
`;
|
|
424
|
+
totalBytes += Buffer.byteLength(chunk);
|
|
425
|
+
if (totalBytes > MAX_DIFF_BYTES) break;
|
|
426
|
+
chunks.push(chunk);
|
|
427
|
+
} else if (parentOid !== currentOid) {
|
|
428
|
+
const oldContent = await readBlob(dir, parentOid);
|
|
429
|
+
const newContent = await readBlob(dir, currentOid);
|
|
430
|
+
const oldLines = oldContent.split("\n");
|
|
431
|
+
const newLines = newContent.split("\n");
|
|
432
|
+
const removed = oldLines.filter((l) => !newLines.includes(l)).map((l) => `-${l}`);
|
|
433
|
+
const added = newLines.filter((l) => !oldLines.includes(l)).map((l) => `+${l}`);
|
|
434
|
+
const chunk = `--- a/${filepath}
|
|
435
|
+
+++ b/${filepath}
|
|
436
|
+
${[...removed, ...added].join("\n")}
|
|
437
|
+
`;
|
|
438
|
+
totalBytes += Buffer.byteLength(chunk);
|
|
439
|
+
if (totalBytes > MAX_DIFF_BYTES) break;
|
|
440
|
+
chunks.push(chunk);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const [filepath] of parentFiles) {
|
|
444
|
+
if (!currentFiles.has(filepath)) {
|
|
445
|
+
const content = await readBlob(dir, parentFiles.get(filepath));
|
|
446
|
+
const lines = content.split("\n").map((l) => `-${l}`);
|
|
447
|
+
const chunk = `--- a/${filepath}
|
|
448
|
+
+++ /dev/null
|
|
449
|
+
${lines.join("\n")}
|
|
450
|
+
`;
|
|
451
|
+
totalBytes += Buffer.byteLength(chunk);
|
|
452
|
+
if (totalBytes > MAX_DIFF_BYTES) break;
|
|
453
|
+
chunks.push(chunk);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return chunks.length > 0 ? chunks.join("\n") : null;
|
|
457
|
+
}
|
|
458
|
+
async function restoreCommit(dir, commitHash) {
|
|
459
|
+
const commitObj = await git.readCommit({ fs: fs3, dir, oid: commitHash });
|
|
460
|
+
const targetTree = commitObj.commit.tree;
|
|
461
|
+
const targetFiles = await walkTree(dir, targetTree);
|
|
462
|
+
const statusMatrix = await git.statusMatrix({ fs: fs3, dir });
|
|
463
|
+
for (const [filepath] of statusMatrix) {
|
|
464
|
+
const fullPath = path3.join(dir, filepath);
|
|
465
|
+
try {
|
|
466
|
+
fs3.unlinkSync(fullPath);
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (const [filepath, oid] of targetFiles) {
|
|
471
|
+
const fullPath = path3.join(dir, filepath);
|
|
472
|
+
const dirPath = path3.dirname(fullPath);
|
|
473
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
474
|
+
const { blob } = await git.readBlob({ fs: fs3, dir, oid });
|
|
475
|
+
fs3.writeFileSync(fullPath, Buffer.from(blob));
|
|
476
|
+
}
|
|
477
|
+
cleanEmptyDirs(dir, dir);
|
|
478
|
+
}
|
|
479
|
+
async function listFiles(dir, commitHash) {
|
|
480
|
+
let oid;
|
|
481
|
+
if (commitHash) {
|
|
482
|
+
oid = commitHash;
|
|
483
|
+
} else {
|
|
484
|
+
oid = await git.resolveRef({ fs: fs3, dir, ref: "HEAD" });
|
|
485
|
+
}
|
|
486
|
+
const commitObj = await git.readCommit({ fs: fs3, dir, oid });
|
|
487
|
+
const treeOid = commitObj.commit.tree;
|
|
488
|
+
const files = await walkTreeWithSize(dir, treeOid);
|
|
489
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
490
|
+
}
|
|
491
|
+
async function walkTreeWithSize(dir, treeOid, prefix = "") {
|
|
492
|
+
const files = [];
|
|
493
|
+
const { tree } = await git.readTree({ fs: fs3, dir, oid: treeOid });
|
|
494
|
+
for (const entry of tree) {
|
|
495
|
+
const filepath = prefix ? `${prefix}/${entry.path}` : entry.path;
|
|
496
|
+
if (entry.type === "blob") {
|
|
497
|
+
const { blob } = await git.readBlob({ fs: fs3, dir, oid: entry.oid });
|
|
498
|
+
files.push({ path: filepath, size: blob.length });
|
|
499
|
+
} else if (entry.type === "tree") {
|
|
500
|
+
const subtree = await walkTreeWithSize(dir, entry.oid, filepath);
|
|
501
|
+
files.push(...subtree);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return files;
|
|
505
|
+
}
|
|
506
|
+
function cleanEmptyDirs(dir, rootDir) {
|
|
507
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
508
|
+
for (const entry of entries) {
|
|
509
|
+
if (entry.isDirectory() && entry.name !== ".git" && entry.name !== ".vv") {
|
|
510
|
+
const fullPath = path3.join(dir, entry.name);
|
|
511
|
+
cleanEmptyDirs(fullPath, rootDir);
|
|
512
|
+
try {
|
|
513
|
+
const remaining = fs3.readdirSync(fullPath);
|
|
514
|
+
if (remaining.length === 0) {
|
|
515
|
+
fs3.rmdirSync(fullPath);
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
246
522
|
async function walkTree(dir, treeOid, prefix = "") {
|
|
247
523
|
const files = /* @__PURE__ */ new Map();
|
|
248
524
|
const { tree } = await git.readTree({ fs: fs3, dir, oid: treeOid });
|
|
@@ -268,11 +544,170 @@ function countLines(content) {
|
|
|
268
544
|
return content.split("\n").length;
|
|
269
545
|
}
|
|
270
546
|
|
|
547
|
+
// src/lib/hooks.ts
|
|
548
|
+
import fs4 from "fs";
|
|
549
|
+
import path4 from "path";
|
|
550
|
+
import chalk2 from "chalk";
|
|
551
|
+
function setupClaudeCodeHook(dir) {
|
|
552
|
+
const claudeDir = path4.join(dir, ".claude");
|
|
553
|
+
const settingsPath = path4.join(claudeDir, "settings.json");
|
|
554
|
+
if (!fs4.existsSync(claudeDir)) {
|
|
555
|
+
fs4.mkdirSync(claudeDir, { recursive: true });
|
|
556
|
+
}
|
|
557
|
+
let settings = {};
|
|
558
|
+
if (fs4.existsSync(settingsPath)) {
|
|
559
|
+
try {
|
|
560
|
+
settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!settings.hooks) {
|
|
565
|
+
settings.hooks = {};
|
|
566
|
+
}
|
|
567
|
+
const existing = settings.hooks.PostToolUse ?? [];
|
|
568
|
+
const alreadyHasHook = existing.some((h) => h.hooks?.some((hook) => hook.command?.includes("vv save")));
|
|
569
|
+
if (alreadyHasHook) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
settings.hooks.PostToolUse = [
|
|
573
|
+
...existing,
|
|
574
|
+
{
|
|
575
|
+
matcher: "Write|Edit",
|
|
576
|
+
hooks: [
|
|
577
|
+
{
|
|
578
|
+
type: "command",
|
|
579
|
+
command: "vv save --auto",
|
|
580
|
+
timeout: 30
|
|
581
|
+
}
|
|
582
|
+
]
|
|
583
|
+
}
|
|
584
|
+
];
|
|
585
|
+
fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
function setupCursorRule(dir) {
|
|
589
|
+
const cursorDir = path4.join(dir, ".cursor", "rules");
|
|
590
|
+
const rulePath = path4.join(cursorDir, "vibeversion.mdc");
|
|
591
|
+
if (fs4.existsSync(rulePath)) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
fs4.mkdirSync(cursorDir, { recursive: true });
|
|
595
|
+
const content = `---
|
|
596
|
+
alwaysApply: true
|
|
597
|
+
---
|
|
598
|
+
This project uses Vibeversion for version tracking.
|
|
599
|
+
After completing a task or making significant changes, run \`vv save\` to snapshot the project.
|
|
600
|
+
Available commands: \`vv save\`, \`vv deploy\`, \`vv restore\`, \`vv files\`.
|
|
601
|
+
`;
|
|
602
|
+
fs4.writeFileSync(rulePath, content);
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
function setupOpenCodePlugin(dir) {
|
|
606
|
+
const pluginDir = path4.join(dir, ".opencode", "plugins");
|
|
607
|
+
const pluginPath = path4.join(pluginDir, "vibeversion.js");
|
|
608
|
+
if (fs4.existsSync(pluginPath)) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
fs4.mkdirSync(pluginDir, { recursive: true });
|
|
612
|
+
const content = `export const VibeversionPlugin = async ({ $ }) => {
|
|
613
|
+
const debounceMs = 12000
|
|
614
|
+
let timer = null
|
|
615
|
+
let inFlight = false
|
|
616
|
+
|
|
617
|
+
const shouldIgnore = (filePath) => {
|
|
618
|
+
if (!filePath) return false
|
|
619
|
+
const normalized = String(filePath).replaceAll('\\\\', '/')
|
|
620
|
+
|
|
621
|
+
if (normalized.includes('/.git/')) return true
|
|
622
|
+
if (normalized.includes('/.vv/')) return true
|
|
623
|
+
if (normalized.includes('/.opencode/')) return true
|
|
624
|
+
if (normalized.endsWith('/.opencode/plugins/vibeversion.js')) return true
|
|
625
|
+
|
|
626
|
+
return false
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const runSave = async () => {
|
|
630
|
+
if (inFlight) {
|
|
631
|
+
timer = setTimeout(runSave, debounceMs)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
inFlight = true
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
await $\`vv save --auto\`
|
|
639
|
+
} catch {
|
|
640
|
+
// Best-effort auto-save
|
|
641
|
+
} finally {
|
|
642
|
+
inFlight = false
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
'file.edited': async (input) => {
|
|
648
|
+
const filePath =
|
|
649
|
+
input?.path ??
|
|
650
|
+
input?.file?.path ??
|
|
651
|
+
input?.filePath ??
|
|
652
|
+
input?.file?.name ??
|
|
653
|
+
input?.file
|
|
654
|
+
|
|
655
|
+
if (shouldIgnore(filePath)) return
|
|
656
|
+
|
|
657
|
+
if (timer) clearTimeout(timer)
|
|
658
|
+
timer = setTimeout(runSave, debounceMs)
|
|
659
|
+
},
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
`;
|
|
663
|
+
fs4.writeFileSync(pluginPath, content);
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
function setupClaudeMd(dir) {
|
|
667
|
+
const claudeMdPath = path4.join(dir, "CLAUDE.md");
|
|
668
|
+
const section = `
|
|
669
|
+
## Vibeversion
|
|
670
|
+
|
|
671
|
+
This project uses Vibeversion for version tracking. After completing a task, run \`vv save\` to snapshot the project.
|
|
672
|
+
|
|
673
|
+
Available commands:
|
|
674
|
+
- \`vv save\` - Save the current state
|
|
675
|
+
- \`vv deploy\` - Save and deploy
|
|
676
|
+
- \`vv restore\` - Restore to a previous version
|
|
677
|
+
- \`vv files\` - List files in a version
|
|
678
|
+
`;
|
|
679
|
+
if (fs4.existsSync(claudeMdPath)) {
|
|
680
|
+
const existing = fs4.readFileSync(claudeMdPath, "utf-8");
|
|
681
|
+
if (existing.includes("Vibeversion")) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
fs4.appendFileSync(claudeMdPath, section);
|
|
685
|
+
} else {
|
|
686
|
+
fs4.writeFileSync(claudeMdPath, `# CLAUDE.md
|
|
687
|
+
${section}`);
|
|
688
|
+
}
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
function setupAllHooks(dir) {
|
|
692
|
+
const claudeResult = setupClaudeCodeHook(dir);
|
|
693
|
+
console.log(claudeResult ? chalk2.green(" \u2713 Claude Code hook added (.claude/settings.json)") : chalk2.dim(" - Claude Code hook already exists"));
|
|
694
|
+
const cursorResult = setupCursorRule(dir);
|
|
695
|
+
console.log(cursorResult ? chalk2.green(" \u2713 Cursor rule added (.cursor/rules/vibeversion.mdc)") : chalk2.dim(" - Cursor rule already exists"));
|
|
696
|
+
const openCodeResult = setupOpenCodePlugin(dir);
|
|
697
|
+
console.log(
|
|
698
|
+
openCodeResult ? chalk2.green(" \u2713 OpenCode plugin added (.opencode/plugins/vibeversion.js)") : chalk2.dim(" - OpenCode plugin already exists")
|
|
699
|
+
);
|
|
700
|
+
const claudeMdResult = setupClaudeMd(dir);
|
|
701
|
+
console.log(
|
|
702
|
+
claudeMdResult ? chalk2.green(" \u2713 CLAUDE.md updated with Vibeversion commands") : chalk2.dim(" - CLAUDE.md already has Vibeversion section")
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
271
706
|
// src/commands/init.ts
|
|
272
707
|
async function initCommand() {
|
|
273
708
|
const auth = getAuthConfig();
|
|
274
709
|
if (!auth) {
|
|
275
|
-
console.log(
|
|
710
|
+
console.log(chalk3.red("Not logged in. Run `vv login` first."));
|
|
276
711
|
process.exit(1);
|
|
277
712
|
}
|
|
278
713
|
const spinner = ora2("Fetching your projects...").start();
|
|
@@ -287,7 +722,7 @@ async function initCommand() {
|
|
|
287
722
|
}
|
|
288
723
|
spinner.stop();
|
|
289
724
|
if (projects.length === 0) {
|
|
290
|
-
console.log(
|
|
725
|
+
console.log(chalk3.yellow("No projects found. Create one on your Vibeversion dashboard first."));
|
|
291
726
|
process.exit(1);
|
|
292
727
|
}
|
|
293
728
|
const projectUlid = await select({
|
|
@@ -303,50 +738,78 @@ async function initCommand() {
|
|
|
303
738
|
saveProjectConfig({ projectUlid, apiUrl: auth.apiUrl, projectName: project.name }, dir);
|
|
304
739
|
ensureGitignore(dir);
|
|
305
740
|
await ensureRepo(dir);
|
|
306
|
-
setupSpinner.succeed(`Linked to ${
|
|
741
|
+
setupSpinner.succeed(`Linked to ${chalk3.cyan(project.name)}.`);
|
|
742
|
+
const setupHooks = await confirm({
|
|
743
|
+
message: "Set up auto-save hooks for AI tools (Claude Code, Cursor, OpenCode)?",
|
|
744
|
+
default: true
|
|
745
|
+
});
|
|
746
|
+
if (setupHooks) {
|
|
747
|
+
console.log("");
|
|
748
|
+
setupAllHooks(dir);
|
|
749
|
+
}
|
|
750
|
+
console.log(chalk3.green(`
|
|
751
|
+
Run ${chalk3.bold("vv save")} to save your first version.
|
|
752
|
+
`));
|
|
307
753
|
}
|
|
308
754
|
|
|
309
755
|
// src/commands/save.ts
|
|
310
756
|
import { input as input2 } from "@inquirer/prompts";
|
|
311
|
-
import
|
|
757
|
+
import chalk4 from "chalk";
|
|
312
758
|
import ora3 from "ora";
|
|
313
|
-
async function saveCommand() {
|
|
759
|
+
async function saveCommand(options = {}) {
|
|
314
760
|
const auth = getAuthConfig();
|
|
315
761
|
if (!auth) {
|
|
316
|
-
console.log(
|
|
762
|
+
console.log(chalk4.red("Not logged in. Run `vv login` first."));
|
|
317
763
|
process.exit(1);
|
|
318
764
|
}
|
|
319
765
|
const dir = process.cwd();
|
|
320
766
|
const project = getProjectConfig(dir);
|
|
321
767
|
if (!project) {
|
|
322
|
-
console.log(
|
|
768
|
+
console.log(chalk4.red("Project not initialized. Run `vv init` first."));
|
|
323
769
|
process.exit(1);
|
|
324
770
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
771
|
+
const worktreeClean = await isWorktreeClean(dir);
|
|
772
|
+
if (worktreeClean) {
|
|
773
|
+
console.log(chalk4.yellow("No changes to save."));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
let title;
|
|
777
|
+
let synopsis;
|
|
778
|
+
if (options.auto) {
|
|
779
|
+
const stats = await diffStats(dir);
|
|
780
|
+
title = `Auto-save: ${stats.filesChanged} ${stats.filesChanged === 1 ? "file" : "files"} changed`;
|
|
781
|
+
synopsis = void 0;
|
|
782
|
+
} else {
|
|
783
|
+
title = await input2({
|
|
784
|
+
message: "What did you change?",
|
|
785
|
+
validate: (v) => v.trim().length > 0 ? true : "A title is required"
|
|
786
|
+
});
|
|
787
|
+
synopsis = await input2({
|
|
788
|
+
message: "Any extra details? (optional)"
|
|
789
|
+
});
|
|
790
|
+
}
|
|
332
791
|
const spinner = ora3("Saving...").start();
|
|
333
792
|
try {
|
|
334
793
|
await stageAll(dir);
|
|
335
794
|
const commitHash = await commit(dir, title);
|
|
336
795
|
const stats = await diffStats(dir);
|
|
796
|
+
const fileStats = await perFileStats(dir);
|
|
797
|
+
const diffText = await generateDiffText(dir);
|
|
337
798
|
const client = new ApiClient(project.apiUrl, auth.token);
|
|
338
799
|
await client.createVersion(project.projectUlid, {
|
|
339
800
|
commit_hash: commitHash,
|
|
340
801
|
title,
|
|
341
802
|
synopsis: synopsis || void 0,
|
|
342
803
|
risk_level: "low",
|
|
343
|
-
type: "named_save",
|
|
804
|
+
type: options.auto ? "auto_save" : "named_save",
|
|
344
805
|
files_changed: stats.filesChanged,
|
|
345
806
|
insertions: stats.insertions,
|
|
346
|
-
deletions: stats.deletions
|
|
807
|
+
deletions: stats.deletions,
|
|
808
|
+
diff_text: diffText,
|
|
809
|
+
per_file_stats: fileStats.length > 0 ? fileStats : void 0
|
|
347
810
|
});
|
|
348
811
|
spinner.succeed(
|
|
349
|
-
`Saved! ${
|
|
812
|
+
`Saved! ${chalk4.bold(`"${title}"`)} ${chalk4.dim(`- ${stats.filesChanged} files changed, +${stats.insertions} -${stats.deletions}`)}`
|
|
350
813
|
);
|
|
351
814
|
} catch (error) {
|
|
352
815
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -355,9 +818,278 @@ async function saveCommand() {
|
|
|
355
818
|
}
|
|
356
819
|
}
|
|
357
820
|
|
|
821
|
+
// src/commands/deploy.ts
|
|
822
|
+
import { input as input3 } from "@inquirer/prompts";
|
|
823
|
+
import chalk5 from "chalk";
|
|
824
|
+
import ora4 from "ora";
|
|
825
|
+
async function deployCommand(options = {}) {
|
|
826
|
+
const auth = getAuthConfig();
|
|
827
|
+
if (!auth) {
|
|
828
|
+
console.log(chalk5.red("Not logged in. Run `vv login` first."));
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
const dir = process.cwd();
|
|
832
|
+
const project = getProjectConfig(dir);
|
|
833
|
+
if (!project) {
|
|
834
|
+
console.log(chalk5.red("Project not initialized. Run `vv init` first."));
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
const worktreeClean = await isWorktreeClean(dir);
|
|
838
|
+
if (worktreeClean) {
|
|
839
|
+
console.log(chalk5.yellow("No changes to deploy."));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
let title;
|
|
843
|
+
let synopsis;
|
|
844
|
+
if (options.auto) {
|
|
845
|
+
const stats = await diffStats(dir);
|
|
846
|
+
title = `Auto-deploy: ${stats.filesChanged} ${stats.filesChanged === 1 ? "file" : "files"} changed`;
|
|
847
|
+
synopsis = void 0;
|
|
848
|
+
} else {
|
|
849
|
+
title = await input3({
|
|
850
|
+
message: "What are you deploying?",
|
|
851
|
+
validate: (v) => v.trim().length > 0 ? true : "A title is required"
|
|
852
|
+
});
|
|
853
|
+
synopsis = await input3({
|
|
854
|
+
message: "Any extra details? (optional)"
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
const spinner = ora4("Saving and deploying...").start();
|
|
858
|
+
try {
|
|
859
|
+
await stageAll(dir);
|
|
860
|
+
const commitHash = await commit(dir, title);
|
|
861
|
+
const stats = await diffStats(dir);
|
|
862
|
+
const fileStats = await perFileStats(dir);
|
|
863
|
+
const diffText = await generateDiffText(dir);
|
|
864
|
+
const client = new ApiClient(project.apiUrl, auth.token);
|
|
865
|
+
await client.createVersion(project.projectUlid, {
|
|
866
|
+
commit_hash: commitHash,
|
|
867
|
+
title,
|
|
868
|
+
synopsis: synopsis || void 0,
|
|
869
|
+
risk_level: "low",
|
|
870
|
+
type: "deploy_save",
|
|
871
|
+
files_changed: stats.filesChanged,
|
|
872
|
+
insertions: stats.insertions,
|
|
873
|
+
deletions: stats.deletions,
|
|
874
|
+
diff_text: diffText,
|
|
875
|
+
per_file_stats: fileStats.length > 0 ? fileStats : void 0
|
|
876
|
+
});
|
|
877
|
+
await client.triggerDeploy(project.projectUlid, {
|
|
878
|
+
commit_hash: commitHash
|
|
879
|
+
});
|
|
880
|
+
spinner.succeed(
|
|
881
|
+
`Deployed! ${chalk5.bold(`"${title}"`)} ${chalk5.dim(`- ${stats.filesChanged} files changed, +${stats.insertions} -${stats.deletions}`)}`
|
|
882
|
+
);
|
|
883
|
+
} catch (error) {
|
|
884
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
885
|
+
spinner.fail(`Deploy failed: ${message}`);
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/commands/restore.ts
|
|
891
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
892
|
+
import chalk6 from "chalk";
|
|
893
|
+
import ora5 from "ora";
|
|
894
|
+
async function restoreCommand() {
|
|
895
|
+
const auth = getAuthConfig();
|
|
896
|
+
if (!auth) {
|
|
897
|
+
console.log(chalk6.red("Not logged in. Run `vv login` first."));
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
const dir = process.cwd();
|
|
901
|
+
const project = getProjectConfig(dir);
|
|
902
|
+
if (!project) {
|
|
903
|
+
console.log(chalk6.red("Project not initialized. Run `vv init` first."));
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
const client = new ApiClient(project.apiUrl, auth.token);
|
|
907
|
+
const fetchSpinner = ora5("Fetching versions...").start();
|
|
908
|
+
let versions;
|
|
909
|
+
try {
|
|
910
|
+
const response = await client.listVersions(project.projectUlid);
|
|
911
|
+
versions = response.data;
|
|
912
|
+
} catch {
|
|
913
|
+
fetchSpinner.fail("Could not fetch versions. Check your connection and try again.");
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
fetchSpinner.stop();
|
|
917
|
+
if (versions.length === 0) {
|
|
918
|
+
console.log(chalk6.yellow("No versions found. Save your project first with `vv save`."));
|
|
919
|
+
process.exit(1);
|
|
920
|
+
}
|
|
921
|
+
const selectedHash = await select2({
|
|
922
|
+
message: "Which version do you want to restore?",
|
|
923
|
+
choices: versions.map((v) => ({
|
|
924
|
+
name: `${v.title} ${chalk6.dim(`(${new Date(v.created_at).toLocaleDateString()})`)}`,
|
|
925
|
+
value: v.commit_hash,
|
|
926
|
+
description: v.synopsis || void 0
|
|
927
|
+
}))
|
|
928
|
+
});
|
|
929
|
+
const selectedVersion = versions.find((v) => v.commit_hash === selectedHash);
|
|
930
|
+
const restoreSpinner = ora5("Restoring...").start();
|
|
931
|
+
try {
|
|
932
|
+
await stageAll(dir);
|
|
933
|
+
const autoHash = await commit(dir, `Auto-save before restore to: ${selectedVersion.title}`);
|
|
934
|
+
const autoStats = await diffStats(dir);
|
|
935
|
+
await client.createVersion(project.projectUlid, {
|
|
936
|
+
commit_hash: autoHash,
|
|
937
|
+
title: `Auto-save before restore`,
|
|
938
|
+
synopsis: `Automatic backup before restoring to: ${selectedVersion.title}`,
|
|
939
|
+
risk_level: "low",
|
|
940
|
+
type: "auto_save",
|
|
941
|
+
files_changed: autoStats.filesChanged,
|
|
942
|
+
insertions: autoStats.insertions,
|
|
943
|
+
deletions: autoStats.deletions
|
|
944
|
+
});
|
|
945
|
+
await restoreCommit(dir, selectedHash);
|
|
946
|
+
await stageAll(dir);
|
|
947
|
+
const restoreHash = await commit(dir, `Restored to: ${selectedVersion.title}`);
|
|
948
|
+
const restoreStats = await diffStats(dir);
|
|
949
|
+
await client.createVersion(project.projectUlid, {
|
|
950
|
+
commit_hash: restoreHash,
|
|
951
|
+
title: `Restored to: ${selectedVersion.title}`,
|
|
952
|
+
risk_level: "low",
|
|
953
|
+
type: "named_save",
|
|
954
|
+
files_changed: restoreStats.filesChanged,
|
|
955
|
+
insertions: restoreStats.insertions,
|
|
956
|
+
deletions: restoreStats.deletions
|
|
957
|
+
});
|
|
958
|
+
restoreSpinner.succeed(
|
|
959
|
+
`Restored to ${chalk6.bold(`"${selectedVersion.title}"`)} ${chalk6.dim(`- ${restoreStats.filesChanged} files changed`)}`
|
|
960
|
+
);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
963
|
+
restoreSpinner.fail(`Restore failed: ${message}`);
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/commands/files.ts
|
|
969
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
970
|
+
import chalk7 from "chalk";
|
|
971
|
+
import ora6 from "ora";
|
|
972
|
+
function formatSize(bytes) {
|
|
973
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
974
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
975
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
976
|
+
}
|
|
977
|
+
function printTree(files) {
|
|
978
|
+
const tree = /* @__PURE__ */ new Map();
|
|
979
|
+
for (const file of files) {
|
|
980
|
+
const parts = file.path.split("/");
|
|
981
|
+
const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
|
|
982
|
+
if (!tree.has(dir)) tree.set(dir, []);
|
|
983
|
+
tree.get(dir).push(file);
|
|
984
|
+
}
|
|
985
|
+
for (const [dir, dirFiles] of [...tree.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
986
|
+
if (dir !== ".") {
|
|
987
|
+
console.log(chalk7.blue(` ${dir}/`));
|
|
988
|
+
}
|
|
989
|
+
for (const file of dirFiles) {
|
|
990
|
+
const name = file.path.split("/").pop();
|
|
991
|
+
const size = chalk7.dim(formatSize(file.size));
|
|
992
|
+
const indent = dir === "." ? " " : " ";
|
|
993
|
+
console.log(`${indent}${name} ${size}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async function filesCommand(options) {
|
|
998
|
+
const dir = process.cwd();
|
|
999
|
+
const project = getProjectConfig(dir);
|
|
1000
|
+
if (!project) {
|
|
1001
|
+
console.log(chalk7.red("Project not initialized. Run `vv init` first."));
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
let commitHash;
|
|
1005
|
+
if (options.version) {
|
|
1006
|
+
const auth = getAuthConfig();
|
|
1007
|
+
if (!auth) {
|
|
1008
|
+
console.log(chalk7.red("Not logged in. Run `vv login` first."));
|
|
1009
|
+
process.exit(1);
|
|
1010
|
+
}
|
|
1011
|
+
const client = new ApiClient(project.apiUrl, auth.token);
|
|
1012
|
+
const fetchSpinner = ora6("Fetching versions...").start();
|
|
1013
|
+
try {
|
|
1014
|
+
const response = await client.listVersions(project.projectUlid);
|
|
1015
|
+
const versions = response.data;
|
|
1016
|
+
fetchSpinner.stop();
|
|
1017
|
+
if (versions.length === 0) {
|
|
1018
|
+
console.log(chalk7.yellow("No versions found."));
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
commitHash = await select3({
|
|
1022
|
+
message: "Which version?",
|
|
1023
|
+
choices: versions.map((v) => ({
|
|
1024
|
+
name: `${v.title} ${chalk7.dim(`(${new Date(v.created_at).toLocaleDateString()})`)}`,
|
|
1025
|
+
value: v.commit_hash
|
|
1026
|
+
}))
|
|
1027
|
+
});
|
|
1028
|
+
} catch {
|
|
1029
|
+
fetchSpinner.fail("Could not fetch versions.");
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const spinner = ora6("Reading files...").start();
|
|
1034
|
+
try {
|
|
1035
|
+
const files = await listFiles(dir, commitHash);
|
|
1036
|
+
spinner.stop();
|
|
1037
|
+
console.log(chalk7.bold(`
|
|
1038
|
+
${files.length} files
|
|
1039
|
+
`));
|
|
1040
|
+
printTree(files);
|
|
1041
|
+
console.log();
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1044
|
+
spinner.fail(`Could not read files: ${message}`);
|
|
1045
|
+
process.exit(1);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/commands/hooks.ts
|
|
1050
|
+
import chalk8 from "chalk";
|
|
1051
|
+
async function hooksCommand(options) {
|
|
1052
|
+
const dir = process.cwd();
|
|
1053
|
+
const project = getProjectConfig(dir);
|
|
1054
|
+
if (!project) {
|
|
1055
|
+
console.log(chalk8.red("Project not initialized. Run `vv init` first."));
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
console.log(chalk8.bold("\nSetting up AI tool hooks...\n"));
|
|
1059
|
+
if (options.all || !options.claude && !options.cursor && !options.opencode) {
|
|
1060
|
+
setupAllHooks(dir);
|
|
1061
|
+
} else {
|
|
1062
|
+
if (options.claude) {
|
|
1063
|
+
const result = setupClaudeCodeHook(dir);
|
|
1064
|
+
console.log(
|
|
1065
|
+
result ? chalk8.green(" \u2713 Claude Code hook added (.claude/settings.json)") : chalk8.dim(" - Claude Code hook already exists")
|
|
1066
|
+
);
|
|
1067
|
+
const mdResult = setupClaudeMd(dir);
|
|
1068
|
+
console.log(
|
|
1069
|
+
mdResult ? chalk8.green(" \u2713 CLAUDE.md updated with Vibeversion commands") : chalk8.dim(" - CLAUDE.md already has Vibeversion section")
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
if (options.cursor) {
|
|
1073
|
+
const result = setupCursorRule(dir);
|
|
1074
|
+
console.log(result ? chalk8.green(" \u2713 Cursor rule added (.cursor/rules/vibeversion.mdc)") : chalk8.dim(" - Cursor rule already exists"));
|
|
1075
|
+
}
|
|
1076
|
+
if (options.opencode) {
|
|
1077
|
+
const result = setupOpenCodePlugin(dir);
|
|
1078
|
+
console.log(
|
|
1079
|
+
result ? chalk8.green(" \u2713 OpenCode plugin added (.opencode/plugins/vibeversion.js)") : chalk8.dim(" - OpenCode plugin already exists")
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
console.log(chalk8.green("\nDone! Auto-save hooks are ready.\n"));
|
|
1084
|
+
}
|
|
1085
|
+
|
|
358
1086
|
// src/index.ts
|
|
359
|
-
program.name("vv").description("Save and version your projects with Vibeversion").version("0.
|
|
1087
|
+
program.name("vv").description("Save and version your projects with Vibeversion").version("0.2.0");
|
|
360
1088
|
program.command("login").description("Log in to your Vibeversion account").action(loginCommand);
|
|
361
1089
|
program.command("init").description("Link this folder to a Vibeversion project").action(initCommand);
|
|
362
|
-
program.command("save").description("Save the current state of your project").action(saveCommand);
|
|
1090
|
+
program.command("save").description("Save the current state of your project").option("--auto", "Non-interactive mode with auto-generated title").action(saveCommand);
|
|
1091
|
+
program.command("deploy").description("Save and deploy your project").option("--auto", "Non-interactive mode with auto-generated title").action(deployCommand);
|
|
1092
|
+
program.command("restore").description("Restore your project to a previous version").action(restoreCommand);
|
|
1093
|
+
program.command("files").description("List files in the current or a specific version").option("--version", "Choose a specific version to view").action(filesCommand);
|
|
1094
|
+
program.command("hooks").description("Set up auto-save hooks for AI tools (Claude Code, Cursor, OpenCode)").option("--claude", "Only set up Claude Code hook").option("--cursor", "Only set up Cursor rule").option("--opencode", "Only set up OpenCode plugin").option("--all", "Set up all hooks").action(hooksCommand);
|
|
363
1095
|
program.parse();
|