@vibeversion/cli 0.1.1 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +232 -3
  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, path4, body) {
18
+ const url = `${this.baseUrl}${path4}`;
19
19
  const headers = {
20
20
  "Content-Type": "application/json",
21
21
  Accept: "application/json"
@@ -51,6 +51,9 @@ 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
  }
@@ -158,6 +161,7 @@ function ensureGitignore(dir = process.cwd()) {
158
161
 
159
162
  // src/lib/git.ts
160
163
  import fs3 from "fs";
164
+ import path3 from "path";
161
165
  import git from "isomorphic-git";
162
166
  var AUTHOR = { name: "Vibeversion", email: "cli@vibeversion.com" };
163
167
  async function ensureRepo(dir) {
@@ -243,6 +247,70 @@ async function diffStats(dir) {
243
247
  }
244
248
  return { filesChanged, insertions, deletions };
245
249
  }
250
+ async function restoreCommit(dir, commitHash) {
251
+ const commitObj = await git.readCommit({ fs: fs3, dir, oid: commitHash });
252
+ const targetTree = commitObj.commit.tree;
253
+ const targetFiles = await walkTree(dir, targetTree);
254
+ const statusMatrix = await git.statusMatrix({ fs: fs3, dir });
255
+ for (const [filepath] of statusMatrix) {
256
+ const fullPath = path3.join(dir, filepath);
257
+ try {
258
+ fs3.unlinkSync(fullPath);
259
+ } catch {
260
+ }
261
+ }
262
+ for (const [filepath, oid] of targetFiles) {
263
+ const fullPath = path3.join(dir, filepath);
264
+ const dirPath = path3.dirname(fullPath);
265
+ fs3.mkdirSync(dirPath, { recursive: true });
266
+ const { blob } = await git.readBlob({ fs: fs3, dir, oid });
267
+ fs3.writeFileSync(fullPath, Buffer.from(blob));
268
+ }
269
+ cleanEmptyDirs(dir, dir);
270
+ }
271
+ async function listFiles(dir, commitHash) {
272
+ let oid;
273
+ if (commitHash) {
274
+ oid = commitHash;
275
+ } else {
276
+ oid = await git.resolveRef({ fs: fs3, dir, ref: "HEAD" });
277
+ }
278
+ const commitObj = await git.readCommit({ fs: fs3, dir, oid });
279
+ const treeOid = commitObj.commit.tree;
280
+ const files = await walkTreeWithSize(dir, treeOid);
281
+ return files.sort((a, b) => a.path.localeCompare(b.path));
282
+ }
283
+ async function walkTreeWithSize(dir, treeOid, prefix = "") {
284
+ const files = [];
285
+ const { tree } = await git.readTree({ fs: fs3, dir, oid: treeOid });
286
+ for (const entry of tree) {
287
+ const filepath = prefix ? `${prefix}/${entry.path}` : entry.path;
288
+ if (entry.type === "blob") {
289
+ const { blob } = await git.readBlob({ fs: fs3, dir, oid: entry.oid });
290
+ files.push({ path: filepath, size: blob.length });
291
+ } else if (entry.type === "tree") {
292
+ const subtree = await walkTreeWithSize(dir, entry.oid, filepath);
293
+ files.push(...subtree);
294
+ }
295
+ }
296
+ return files;
297
+ }
298
+ function cleanEmptyDirs(dir, rootDir) {
299
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
300
+ for (const entry of entries) {
301
+ if (entry.isDirectory() && entry.name !== ".git" && entry.name !== ".vibeversion") {
302
+ const fullPath = path3.join(dir, entry.name);
303
+ cleanEmptyDirs(fullPath, rootDir);
304
+ try {
305
+ const remaining = fs3.readdirSync(fullPath);
306
+ if (remaining.length === 0) {
307
+ fs3.rmdirSync(fullPath);
308
+ }
309
+ } catch {
310
+ }
311
+ }
312
+ }
313
+ }
246
314
  async function walkTree(dir, treeOid, prefix = "") {
247
315
  const files = /* @__PURE__ */ new Map();
248
316
  const { tree } = await git.readTree({ fs: fs3, dir, oid: treeOid });
@@ -355,9 +423,170 @@ async function saveCommand() {
355
423
  }
356
424
  }
357
425
 
426
+ // src/commands/restore.ts
427
+ import { select as select2 } from "@inquirer/prompts";
428
+ import chalk4 from "chalk";
429
+ import ora4 from "ora";
430
+ async function restoreCommand() {
431
+ const auth = getAuthConfig();
432
+ if (!auth) {
433
+ console.log(chalk4.red("Not logged in. Run `vv login` first."));
434
+ process.exit(1);
435
+ }
436
+ const dir = process.cwd();
437
+ const project = getProjectConfig(dir);
438
+ if (!project) {
439
+ console.log(chalk4.red("Project not initialized. Run `vv init` first."));
440
+ process.exit(1);
441
+ }
442
+ const client = new ApiClient(project.apiUrl, auth.token);
443
+ const fetchSpinner = ora4("Fetching versions...").start();
444
+ let versions;
445
+ try {
446
+ const response = await client.listVersions(project.projectUlid);
447
+ versions = response.data;
448
+ } catch {
449
+ fetchSpinner.fail("Could not fetch versions. Check your connection and try again.");
450
+ process.exit(1);
451
+ }
452
+ fetchSpinner.stop();
453
+ if (versions.length === 0) {
454
+ console.log(chalk4.yellow("No versions found. Save your project first with `vv save`."));
455
+ process.exit(1);
456
+ }
457
+ const selectedHash = await select2({
458
+ message: "Which version do you want to restore?",
459
+ choices: versions.map((v) => ({
460
+ name: `${v.title} ${chalk4.dim(`(${new Date(v.created_at).toLocaleDateString()})`)}`,
461
+ value: v.commit_hash,
462
+ description: v.synopsis || void 0
463
+ }))
464
+ });
465
+ const selectedVersion = versions.find((v) => v.commit_hash === selectedHash);
466
+ const restoreSpinner = ora4("Restoring...").start();
467
+ try {
468
+ await stageAll(dir);
469
+ const autoHash = await commit(dir, `Auto-save before restore to: ${selectedVersion.title}`);
470
+ const autoStats = await diffStats(dir);
471
+ await client.createVersion(project.projectUlid, {
472
+ commit_hash: autoHash,
473
+ title: `Auto-save before restore`,
474
+ synopsis: `Automatic backup before restoring to: ${selectedVersion.title}`,
475
+ risk_level: "low",
476
+ type: "auto_save",
477
+ files_changed: autoStats.filesChanged,
478
+ insertions: autoStats.insertions,
479
+ deletions: autoStats.deletions
480
+ });
481
+ await restoreCommit(dir, selectedHash);
482
+ await stageAll(dir);
483
+ const restoreHash = await commit(dir, `Restored to: ${selectedVersion.title}`);
484
+ const restoreStats = await diffStats(dir);
485
+ await client.createVersion(project.projectUlid, {
486
+ commit_hash: restoreHash,
487
+ title: `Restored to: ${selectedVersion.title}`,
488
+ risk_level: "low",
489
+ type: "named_save",
490
+ files_changed: restoreStats.filesChanged,
491
+ insertions: restoreStats.insertions,
492
+ deletions: restoreStats.deletions
493
+ });
494
+ restoreSpinner.succeed(
495
+ `Restored to ${chalk4.bold(`"${selectedVersion.title}"`)} ${chalk4.dim(`- ${restoreStats.filesChanged} files changed`)}`
496
+ );
497
+ } catch (error) {
498
+ const message = error instanceof Error ? error.message : "Unknown error";
499
+ restoreSpinner.fail(`Restore failed: ${message}`);
500
+ process.exit(1);
501
+ }
502
+ }
503
+
504
+ // src/commands/files.ts
505
+ import { select as select3 } from "@inquirer/prompts";
506
+ import chalk5 from "chalk";
507
+ import ora5 from "ora";
508
+ function formatSize(bytes) {
509
+ if (bytes < 1024) return `${bytes} B`;
510
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
511
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
512
+ }
513
+ function printTree(files) {
514
+ const tree = /* @__PURE__ */ new Map();
515
+ for (const file of files) {
516
+ const parts = file.path.split("/");
517
+ const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
518
+ if (!tree.has(dir)) tree.set(dir, []);
519
+ tree.get(dir).push(file);
520
+ }
521
+ for (const [dir, dirFiles] of [...tree.entries()].sort(([a], [b]) => a.localeCompare(b))) {
522
+ if (dir !== ".") {
523
+ console.log(chalk5.blue(` ${dir}/`));
524
+ }
525
+ for (const file of dirFiles) {
526
+ const name = file.path.split("/").pop();
527
+ const size = chalk5.dim(formatSize(file.size));
528
+ const indent = dir === "." ? " " : " ";
529
+ console.log(`${indent}${name} ${size}`);
530
+ }
531
+ }
532
+ }
533
+ async function filesCommand(options) {
534
+ const dir = process.cwd();
535
+ const project = getProjectConfig(dir);
536
+ if (!project) {
537
+ console.log(chalk5.red("Project not initialized. Run `vv init` first."));
538
+ process.exit(1);
539
+ }
540
+ let commitHash;
541
+ if (options.version) {
542
+ const auth = getAuthConfig();
543
+ if (!auth) {
544
+ console.log(chalk5.red("Not logged in. Run `vv login` first."));
545
+ process.exit(1);
546
+ }
547
+ const client = new ApiClient(project.apiUrl, auth.token);
548
+ const fetchSpinner = ora5("Fetching versions...").start();
549
+ try {
550
+ const response = await client.listVersions(project.projectUlid);
551
+ const versions = response.data;
552
+ fetchSpinner.stop();
553
+ if (versions.length === 0) {
554
+ console.log(chalk5.yellow("No versions found."));
555
+ process.exit(1);
556
+ }
557
+ commitHash = await select3({
558
+ message: "Which version?",
559
+ choices: versions.map((v) => ({
560
+ name: `${v.title} ${chalk5.dim(`(${new Date(v.created_at).toLocaleDateString()})`)}`,
561
+ value: v.commit_hash
562
+ }))
563
+ });
564
+ } catch {
565
+ fetchSpinner.fail("Could not fetch versions.");
566
+ process.exit(1);
567
+ }
568
+ }
569
+ const spinner = ora5("Reading files...").start();
570
+ try {
571
+ const files = await listFiles(dir, commitHash);
572
+ spinner.stop();
573
+ console.log(chalk5.bold(`
574
+ ${files.length} files
575
+ `));
576
+ printTree(files);
577
+ console.log();
578
+ } catch (error) {
579
+ const message = error instanceof Error ? error.message : "Unknown error";
580
+ spinner.fail(`Could not read files: ${message}`);
581
+ process.exit(1);
582
+ }
583
+ }
584
+
358
585
  // src/index.ts
359
- program.name("vv").description("Save and version your projects with Vibeversion").version("0.1.1");
586
+ program.name("vv").description("Save and version your projects with Vibeversion").version("0.2.0");
360
587
  program.command("login").description("Log in to your Vibeversion account").action(loginCommand);
361
588
  program.command("init").description("Link this folder to a Vibeversion project").action(initCommand);
362
589
  program.command("save").description("Save the current state of your project").action(saveCommand);
590
+ program.command("restore").description("Restore your project to a previous version").action(restoreCommand);
591
+ program.command("files").description("List files in the current or a specific version").option("--version", "Choose a specific version to view").action(filesCommand);
363
592
  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.0",
4
4
  "description": "Save and version your projects with Vibeversion",
5
5
  "type": "module",
6
6
  "bin": {