@seedvault/cli 0.4.0 → 0.4.2

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/sv.js +141 -51
  2. package/package.json +6 -1
package/dist/sv.js CHANGED
@@ -2,8 +2,8 @@
2
2
  // @bun
3
3
 
4
4
  // src/index.ts
5
- import { readFileSync as readFileSync2 } from "fs";
6
- import { resolve as resolve6 } from "path";
5
+ import { readFileSync as readFileSync3 } from "fs";
6
+ import { resolve as resolve7 } from "path";
7
7
 
8
8
  // src/commands/init.ts
9
9
  import * as readline from "readline/promises";
@@ -200,22 +200,48 @@ function createClient(serverUrl, token) {
200
200
  return res.json();
201
201
  },
202
202
  async putFile(username, path, content) {
203
- const res = await request("PUT", `/v1/contributors/${username}/files/${encodePath(path)}`, {
203
+ const res = await request("PUT", `/v1/files/${username}/${encodePath(path)}`, {
204
204
  body: content,
205
205
  contentType: "text/markdown"
206
206
  });
207
207
  return res.json();
208
208
  },
209
209
  async deleteFile(username, path) {
210
- await request("DELETE", `/v1/contributors/${username}/files/${encodePath(path)}`);
210
+ await request("DELETE", `/v1/files/${username}/${encodePath(path)}`);
211
211
  },
212
212
  async listFiles(username, prefix) {
213
- const qs = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
214
- const res = await request("GET", `/v1/contributors/${username}/files${qs}`);
215
- return res.json();
213
+ const fullPrefix = prefix ? `${username}/${prefix}` : `${username}/`;
214
+ const qs = `?prefix=${encodeURIComponent(fullPrefix)}`;
215
+ const res = await request("GET", `/v1/files${qs}`);
216
+ const data = await res.json();
217
+ return {
218
+ files: data.files.map((f) => ({
219
+ ...f,
220
+ path: f.path.startsWith(`${username}/`) ? f.path.slice(username.length + 1) : f.path
221
+ }))
222
+ };
216
223
  },
217
224
  async getFile(username, path) {
218
- const res = await request("GET", `/v1/contributors/${username}/files/${encodePath(path)}`);
225
+ const fullPath = `${username}/${path}`;
226
+ const res = await request("POST", "/v1/sh", {
227
+ body: JSON.stringify({ cmd: `cat "${fullPath}"` }),
228
+ contentType: "application/json"
229
+ });
230
+ const exitCode = parseInt(res.headers.get("X-Exit-Code") || "0", 10);
231
+ if (exitCode !== 0) {
232
+ const stderr = decodeURIComponent(res.headers.get("X-Stderr") || "");
233
+ if (stderr.includes("No such file or directory")) {
234
+ throw new ApiError(404, "File not found");
235
+ }
236
+ throw new ApiError(500, stderr || `cat exited with code ${exitCode}`);
237
+ }
238
+ return res.text();
239
+ },
240
+ async sh(cmd) {
241
+ const res = await request("POST", "/v1/sh", {
242
+ body: JSON.stringify({ cmd }),
243
+ contentType: "application/json"
244
+ });
219
245
  return res.text();
220
246
  },
221
247
  async health() {
@@ -2012,12 +2038,26 @@ function watch(paths, options = {}) {
2012
2038
  import { relative as relative4 } from "path";
2013
2039
  function createWatcher(collections2, onEvent) {
2014
2040
  const paths = collections2.map((f) => f.path);
2041
+ const shouldIgnore = (filePath) => {
2042
+ for (const col of collections2) {
2043
+ if (filePath.startsWith(col.path + "/") || filePath === col.path) {
2044
+ const relPath = filePath.slice(col.path.length + 1);
2045
+ const segments = relPath.split("/");
2046
+ for (const seg of segments) {
2047
+ if (seg.startsWith("."))
2048
+ return true;
2049
+ if (seg === "node_modules")
2050
+ return true;
2051
+ if (seg.includes(".tmp."))
2052
+ return true;
2053
+ }
2054
+ return false;
2055
+ }
2056
+ }
2057
+ return false;
2058
+ };
2015
2059
  const watcher = watch(paths, {
2016
- ignored: [
2017
- /(^|[/\\])\./,
2018
- "**/node_modules/**",
2019
- "**/*.tmp.*"
2020
- ],
2060
+ ignored: shouldIgnore,
2021
2061
  persistent: true,
2022
2062
  ignoreInitial: true,
2023
2063
  awaitWriteFinish: {
@@ -2164,8 +2204,37 @@ class Syncer {
2164
2204
  skipped += result.skipped;
2165
2205
  deleted += result.deleted;
2166
2206
  }
2207
+ deleted += await this.purgeOrphans();
2167
2208
  return { uploaded, skipped, deleted };
2168
2209
  }
2210
+ async purgeOrphans() {
2211
+ let deleted = 0;
2212
+ const { files: allServerFiles } = await this.client.listFiles(this.username);
2213
+ const collectionNames = new Set(this.collections.map((c) => c.name));
2214
+ const orphans = allServerFiles.filter((f) => {
2215
+ const prefix = f.path.split("/")[0];
2216
+ return !collectionNames.has(prefix);
2217
+ });
2218
+ if (orphans.length === 0)
2219
+ return 0;
2220
+ this.log(`Purging ${orphans.length} orphaned file(s) from removed collections...`);
2221
+ await pooled(orphans, SYNC_CONCURRENCY, async (f) => {
2222
+ try {
2223
+ await this.client.deleteFile(this.username, f.path);
2224
+ deleted++;
2225
+ } catch {
2226
+ this.queue.enqueue({
2227
+ type: "delete",
2228
+ username: this.username,
2229
+ serverPath: f.path,
2230
+ content: null,
2231
+ queuedAt: new Date().toISOString()
2232
+ });
2233
+ }
2234
+ });
2235
+ this.log(` Purged ${deleted} orphaned file(s)`);
2236
+ return deleted;
2237
+ }
2169
2238
  async syncCollection(collection) {
2170
2239
  let uploaded = 0;
2171
2240
  let skipped = 0;
@@ -2908,27 +2977,8 @@ async function status() {
2908
2977
  async function ls(args) {
2909
2978
  const config = loadConfig();
2910
2979
  const client = createClient(config.server, config.token);
2911
- const prefix = args[0] || undefined;
2912
- const { files } = await client.listFiles(config.username, prefix);
2913
- if (files.length === 0) {
2914
- console.log(prefix ? `No files matching '${prefix}'.` : "No files in your contributor.");
2915
- return;
2916
- }
2917
- const maxPath = Math.max(...files.map((f) => f.path.length));
2918
- for (const f of files) {
2919
- const size = formatSize(f.size);
2920
- const date = new Date(f.modifiedAt).toLocaleString();
2921
- console.log(` ${f.path.padEnd(maxPath + 2)} ${size.padStart(8)} ${date}`);
2922
- }
2923
- console.log(`
2924
- ${files.length} file(s)`);
2925
- }
2926
- function formatSize(bytes) {
2927
- if (bytes < 1024)
2928
- return `${bytes} B`;
2929
- if (bytes < 1024 * 1024)
2930
- return `${(bytes / 1024).toFixed(1)} KB`;
2931
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2980
+ const output = await client.sh(`ls ${args.join(" ")}`);
2981
+ process.stdout.write(output);
2932
2982
  }
2933
2983
 
2934
2984
  // src/commands/cat.ts
@@ -2937,19 +2987,25 @@ async function cat(args) {
2937
2987
  console.error("Usage: sv cat <path>");
2938
2988
  process.exit(1);
2939
2989
  }
2940
- const filePath = args[0];
2941
2990
  const config = loadConfig();
2942
2991
  const client = createClient(config.server, config.token);
2943
- try {
2944
- const content = await client.getFile(config.username, filePath);
2945
- process.stdout.write(content);
2946
- } catch (e) {
2947
- if (e instanceof ApiError && e.status === 404) {
2948
- console.error(`File not found: ${filePath}`);
2949
- process.exit(1);
2950
- }
2951
- throw e;
2992
+ const output = await client.sh(`cat ${args.join(" ")}`);
2993
+ process.stdout.write(output);
2994
+ }
2995
+
2996
+ // src/commands/sh.ts
2997
+ async function sh(args) {
2998
+ if (args.length === 0) {
2999
+ console.error("Usage: sv sh <command>");
3000
+ console.error('Example: sv sh "ls -la yiliu/"');
3001
+ console.error('Example: sv sh "grep -r pattern ."');
3002
+ process.exit(1);
2952
3003
  }
3004
+ const cmd = args.join(" ");
3005
+ const config = loadConfig();
3006
+ const client = createClient(config.server, config.token);
3007
+ const output = await client.sh(cmd);
3008
+ process.stdout.write(output);
2953
3009
  }
2954
3010
 
2955
3011
  // src/commands/contributors.ts
@@ -2991,6 +3047,31 @@ Share this with the person you want to invite.`);
2991
3047
  }
2992
3048
  }
2993
3049
 
3050
+ // src/commands/upgrade.ts
3051
+ import { readFileSync as readFileSync2 } from "fs";
3052
+ import { resolve as resolve6 } from "path";
3053
+ var {$ } = globalThis.Bun;
3054
+ var INSTALL_SCRIPT_URL = "https://raw.githubusercontent.com/collaborator-ai/seedvault/main/install-cli.sh";
3055
+ async function upgrade() {
3056
+ const pkgPath = resolve6(import.meta.dirname, "..", "..", "package.json");
3057
+ let currentVersion = "unknown";
3058
+ try {
3059
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
3060
+ currentVersion = pkg.version;
3061
+ } catch {}
3062
+ console.log(`Current version: ${currentVersion}`);
3063
+ console.log(`Fetching latest from: ${INSTALL_SCRIPT_URL}
3064
+ `);
3065
+ const result = await $`curl -fsSL ${INSTALL_SCRIPT_URL} | bash -s -- --no-onboard`.quiet().nothrow();
3066
+ if (result.exitCode !== 0) {
3067
+ console.error("Upgrade failed:");
3068
+ console.error(result.stderr.toString());
3069
+ process.exit(1);
3070
+ }
3071
+ console.log(result.stdout.toString());
3072
+ console.log("Upgrade complete. Run 'sv --version' to verify.");
3073
+ }
3074
+
2994
3075
  // src/index.ts
2995
3076
  var USAGE = `
2996
3077
  Seedvault CLI
@@ -2998,9 +3079,10 @@ Seedvault CLI
2998
3079
  Usage: sv <command> [options]
2999
3080
 
3000
3081
  Setup:
3001
- init Interactive first-time setup
3002
- init --server URL --token T --username U Non-interactive (existing token)
3003
- init --server URL --name N Non-interactive (signup)
3082
+ init Interactive first-time setup
3083
+ init --server URL --token T Non-interactive (existing token)
3084
+ init --server URL --name N [--invite CODE] Non-interactive (signup)
3085
+ init --force Overwrite existing config
3004
3086
 
3005
3087
  Collections:
3006
3088
  add <path> [--name N] Add a collection path
@@ -3014,12 +3096,16 @@ Daemon:
3014
3096
  status Show sync status
3015
3097
 
3016
3098
  Files:
3017
- ls [prefix] List files in your contributor
3018
- cat <path> Read a file from the server
3099
+ sh <command> Run a shell command on the vault (ls, cat, grep, etc.)
3100
+ ls [args...] Shorthand for: sv sh "ls [args...]"
3101
+ cat <path> Shorthand for: sv sh "cat <path>"
3019
3102
 
3020
3103
  Vault:
3021
3104
  contributors List all contributors
3022
3105
  invite Generate an invite code (operator only)
3106
+
3107
+ Maintenance:
3108
+ upgrade Upgrade CLI to latest version
3023
3109
  `.trim();
3024
3110
  async function main() {
3025
3111
  const [cmd, ...args] = process.argv.slice(2);
@@ -3028,8 +3114,8 @@ async function main() {
3028
3114
  return;
3029
3115
  }
3030
3116
  if (cmd === "--version" || cmd === "-v") {
3031
- const pkgPath = resolve6(import.meta.dirname, "..", "package.json");
3032
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
3117
+ const pkgPath = resolve7(import.meta.dirname, "..", "package.json");
3118
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
3033
3119
  console.log(pkg.version);
3034
3120
  return;
3035
3121
  }
@@ -3049,6 +3135,8 @@ async function main() {
3049
3135
  return await stop();
3050
3136
  case "status":
3051
3137
  return await status();
3138
+ case "sh":
3139
+ return await sh(args);
3052
3140
  case "ls":
3053
3141
  return await ls(args);
3054
3142
  case "cat":
@@ -3057,6 +3145,8 @@ async function main() {
3057
3145
  return await contributors();
3058
3146
  case "invite":
3059
3147
  return await invite();
3148
+ case "upgrade":
3149
+ return await upgrade();
3060
3150
  default:
3061
3151
  console.error(`Unknown command: ${cmd}
3062
3152
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seedvault/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sv": "bin/sv.mjs"
@@ -15,6 +15,11 @@
15
15
  "dist",
16
16
  "bin"
17
17
  ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/collaborator-ai/seedvault.git",
21
+ "directory": "cli"
22
+ },
18
23
  "dependencies": {
19
24
  "chokidar": "^4"
20
25
  },