@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.
Files changed (2) hide show
  1. package/dist/index.js +764 -32
  2. 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, path3, body) {
18
- const url = `${this.baseUrl}${path3}`;
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(), ".vibeversion");
76
+ var CONFIG_DIR = path.join(os.homedir(), ".vv");
71
77
  var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
72
- function getAuthConfig() {
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(CONFIG_FILE, "utf-8");
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 chalk2 from "chalk";
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 = ".vibeversion";
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 getProjectConfig(dir = process.cwd()) {
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(getProjectConfigPath(dir), "utf-8");
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 = ".vibeversion/";
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)) return;
153
- fs2.writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
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(chalk2.red("Not logged in. Run `vv login` first."));
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(chalk2.yellow("No projects found. Create one on your Vibeversion dashboard first."));
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 ${chalk2.cyan(project.name)}. Run ${chalk2.bold("vv save")} to save your first version.`);
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 chalk3 from "chalk";
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(chalk3.red("Not logged in. Run `vv login` first."));
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(chalk3.red("Project not initialized. Run `vv init` first."));
768
+ console.log(chalk4.red("Project not initialized. Run `vv init` first."));
323
769
  process.exit(1);
324
770
  }
325
- const title = await input2({
326
- message: "What did you change?",
327
- validate: (v) => v.trim().length > 0 ? true : "A title is required"
328
- });
329
- const synopsis = await input2({
330
- message: "Any extra details? (optional)"
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! ${chalk3.bold(`"${title}"`)} ${chalk3.dim(`- ${stats.filesChanged} files changed, +${stats.insertions} -${stats.deletions}`)}`
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.1.1");
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeversion/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Save and version your projects with Vibeversion",
5
5
  "type": "module",
6
6
  "bin": {