northbase 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -103,6 +103,32 @@ On success, prints a single confirmation line to stdout:
103
103
  PUT ok test/cli.md bytes=12 updated_at=2024-01-15T10:30:00.000Z
104
104
  ```
105
105
 
106
+ ### List remote files
107
+
108
+ Prints every file path stored remotely, one per line. Useful for discovery or scripting.
109
+
110
+ ```bash
111
+ northbase list # all files
112
+ northbase list memory/ # only paths starting with "memory/"
113
+ ```
114
+
115
+ ### Pull (bulk sync)
116
+
117
+ Downloads all remote files whose `updated_at` differs from the local cache. Useful for "discover and mirror all your notes so an agent can read them locally."
118
+
119
+ ```bash
120
+ northbase pull # sync everything
121
+ northbase pull memory/ # sync only files under memory/ prefix
122
+ ```
123
+
124
+ Prints a summary line to stdout:
125
+
126
+ ```
127
+ PULL ok files=12 downloaded=3 skipped=9
128
+ ```
129
+
130
+ Per-file download progress goes to stderr. Pull never deletes local files.
131
+
106
132
  ## Local mirror
107
133
 
108
134
  All files are mirrored at:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "northbase",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Local-first CLI for reading and writing text files stored in Supabase",
5
5
  "type": "module",
6
6
  "bin": {
package/src/northbase.mjs CHANGED
@@ -205,6 +205,68 @@ async function putFile(rel, content) {
205
205
  return { bytes, updated_at };
206
206
  }
207
207
 
208
+ // ── concurrency helper ────────────────────────────────────────────────────────
209
+
210
+ async function concurrentMap(items, limit, fn) {
211
+ const results = [];
212
+ let i = 0;
213
+ async function worker() {
214
+ while (i < items.length) {
215
+ const idx = i++;
216
+ results[idx] = await fn(items[idx]);
217
+ }
218
+ }
219
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
220
+ return results;
221
+ }
222
+
223
+ // ── list / pull commands ──────────────────────────────────────────────────────
224
+
225
+ async function cmdList(prefix) {
226
+ const supabase = await getAuthenticatedClient();
227
+ let query = supabase.from("files").select("path").order("path", { ascending: true });
228
+ if (prefix) query = query.like("path", `${prefix}%`);
229
+ const { data, error } = await query;
230
+ if (error) throw error;
231
+ for (const row of data) process.stdout.write(row.path + "\n");
232
+ }
233
+
234
+ async function cmdPull(prefix) {
235
+ ensureDir(ROOT);
236
+ const idx = loadIndex();
237
+ const supabase = await getAuthenticatedClient();
238
+
239
+ let query = supabase.from("files").select("path, updated_at").order("path", { ascending: true });
240
+ if (prefix) query = query.like("path", `${prefix}%`);
241
+ const { data: remote, error } = await query;
242
+ if (error) throw error;
243
+
244
+ let downloaded = 0, skipped = 0;
245
+
246
+ await concurrentMap(remote, 5, async (row) => {
247
+ const rel = row.path;
248
+ const cached = idx.files?.[rel];
249
+ if (cached?.updated_at === row.updated_at) {
250
+ skipped++;
251
+ return;
252
+ }
253
+ console.error("NORTHBASE PULL download", rel);
254
+ const { data: rows, error: err } = await supabase
255
+ .from("files").select("content, updated_at").eq("path", rel).limit(1);
256
+ if (err) throw err;
257
+ const content = rows?.[0]?.content ?? "";
258
+ const updated_at = rows?.[0]?.updated_at ?? row.updated_at;
259
+ const bytes = Buffer.byteLength(content, "utf8");
260
+ if (bytes > MAX_BYTES) throw new Error(`File too large (${bytes} bytes): ${rel}`);
261
+ writeLocal(rel, content);
262
+ idx.files[rel] = { updated_at, bytes };
263
+ downloaded++;
264
+ });
265
+
266
+ saveIndex(idx);
267
+ console.log(`PULL ok files=${remote.length} downloaded=${downloaded} skipped=${skipped}`);
268
+ }
269
+
208
270
  // ── interactive prompts ───────────────────────────────────────────────────────
209
271
 
210
272
  async function promptLine(question) {
@@ -299,9 +361,11 @@ async function cmdWhoami() {
299
361
  async function main() {
300
362
  const [cmd, ...args] = process.argv.slice(2);
301
363
 
302
- if (cmd === "login") { await cmdLogin(); return; }
303
- if (cmd === "logout") { await cmdLogout(); return; }
304
- if (cmd === "whoami") { await cmdWhoami(); return; }
364
+ if (cmd === "login") { await cmdLogin(); return; }
365
+ if (cmd === "logout") { await cmdLogout(); return; }
366
+ if (cmd === "whoami") { await cmdWhoami(); return; }
367
+ if (cmd === "list") { await cmdList(args[0]); return; }
368
+ if (cmd === "pull") { await cmdPull(args[0]); return; }
305
369
 
306
370
  if (cmd === "get") {
307
371
  const rel = args[0];
@@ -326,6 +390,8 @@ async function main() {
326
390
  console.log(" northbase login");
327
391
  console.log(" northbase logout");
328
392
  console.log(" northbase whoami");
393
+ console.log(" northbase list [prefix]");
394
+ console.log(" northbase pull [prefix]");
329
395
  console.log(" northbase get <path>");
330
396
  console.log(" northbase put <path>");
331
397
  process.exit(1);