northbase 0.1.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/README.md +142 -0
- package/package.json +22 -0
- package/src/mybot.mjs +337 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# mybot-notes-cli
|
|
2
|
+
|
|
3
|
+
A local-first CLI for reading and writing text files stored in a Supabase `public.files` table. Uses email/password auth with RLS so each user only sees their own files. Session is stored locally — no env file required.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js >= 18
|
|
8
|
+
- A Supabase project with the following table and RLS enabled:
|
|
9
|
+
|
|
10
|
+
```sql
|
|
11
|
+
create table public.files (
|
|
12
|
+
path text primary key,
|
|
13
|
+
content text not null default '',
|
|
14
|
+
owner_id uuid not null references auth.users(id),
|
|
15
|
+
created_at timestamptz not null default now(),
|
|
16
|
+
updated_at timestamptz not null default now()
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
-- RLS
|
|
20
|
+
alter table public.files enable row level security;
|
|
21
|
+
|
|
22
|
+
create policy "owner access" on public.files
|
|
23
|
+
for all using (owner_id = auth.uid());
|
|
24
|
+
|
|
25
|
+
-- Auto-update updated_at on every write
|
|
26
|
+
create or replace function public.set_updated_at()
|
|
27
|
+
returns trigger language plpgsql as $$
|
|
28
|
+
begin
|
|
29
|
+
new.updated_at := now();
|
|
30
|
+
return new;
|
|
31
|
+
end;
|
|
32
|
+
$$;
|
|
33
|
+
|
|
34
|
+
create trigger files_updated_at
|
|
35
|
+
before update on public.files
|
|
36
|
+
for each row execute procedure public.set_updated_at();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install
|
|
43
|
+
npm link # makes `mybot` available globally on your PATH
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
No environment file needed. The Supabase project URL and anon key are public constants baked into the CLI.
|
|
47
|
+
|
|
48
|
+
## Auth
|
|
49
|
+
|
|
50
|
+
### Log in
|
|
51
|
+
|
|
52
|
+
Prompts for your email and password interactively (password is not echoed):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
mybot login
|
|
56
|
+
# Email: you@example.com
|
|
57
|
+
# Password:
|
|
58
|
+
# Logged in.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Your session (access + refresh tokens) is stored at `~/.mybot/session.json` with mode `600`. Tokens are refreshed automatically when they expire — you should only need to log in once.
|
|
62
|
+
|
|
63
|
+
### Log out
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
mybot logout
|
|
67
|
+
# Logged out.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Deletes `~/.mybot/session.json` and signs out server-side.
|
|
71
|
+
|
|
72
|
+
### Check who you are
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
mybot whoami
|
|
76
|
+
# Logged in as you@example.com (uuid...)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
### Get a file
|
|
82
|
+
|
|
83
|
+
Fetches file content and prints it to stdout. Uses a local cache at `~/.mybot/files/` and only downloads from Supabase when the remote `updated_at` timestamp differs from the cached value.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
mybot get ideas.md
|
|
87
|
+
mybot get notes/todo.txt
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Put a file
|
|
91
|
+
|
|
92
|
+
Reads content from stdin, upserts it to Supabase, then updates the local cache.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
printf "hello world\n" | mybot put test/cli.md
|
|
96
|
+
cat my-local-file.md | mybot put ideas.md
|
|
97
|
+
echo "updated content" | mybot put notes/todo.txt
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
On success, prints a single confirmation line to stdout:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
PUT ok test/cli.md bytes=12 updated_at=2024-01-15T10:30:00.000Z
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Local mirror
|
|
107
|
+
|
|
108
|
+
All files are mirrored at:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
~/.mybot/files/<path>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Metadata (timestamps and byte counts) is stored at:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
~/.mybot/index.json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Session tokens are stored at:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
~/.mybot/session.json (mode 600 — readable only by you)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The CLI compares `updated_at` timestamps before downloading — if your local copy is current, no content fetch is made.
|
|
127
|
+
|
|
128
|
+
## Limits
|
|
129
|
+
|
|
130
|
+
- Maximum file size: **500 KB** per file (enforced on both get and put)
|
|
131
|
+
- Paths must not contain `.`, `..`, empty segments, or leading slashes
|
|
132
|
+
|
|
133
|
+
## Debug output
|
|
134
|
+
|
|
135
|
+
Debug logs go to stderr so they never pollute stdout pipelines:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
MYBOT GET local-hit ideas.md # served from local cache
|
|
139
|
+
MYBOT GET remote-refresh ideas.md # downloaded from Supabase
|
|
140
|
+
MYBOT PUT test/cli.md bytes=12 updated_at=2024-01-15T10:30:00.000Z
|
|
141
|
+
MYBOT session refreshing # printed when access token is silently renewed
|
|
142
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "northbase",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Local-first CLI for reading and writing text files stored in Supabase",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"northbase": "./src/mybot.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@supabase/supabase-js": "^2.45.4"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cli",
|
|
17
|
+
"supabase",
|
|
18
|
+
"notes",
|
|
19
|
+
"files"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|
package/src/mybot.mjs
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { createClient } from "@supabase/supabase-js";
|
|
7
|
+
|
|
8
|
+
// ── constants ─────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const SUPABASE_URL = "https://ivxgpjracfctkkdhlwgm.supabase.co";
|
|
11
|
+
const SUPABASE_KEY = "sb_publishable_LZkkAwsx9q5KgIAeoZAO_A_U88rfFHL";
|
|
12
|
+
|
|
13
|
+
const MYBOT_DIR = path.join(os.homedir(), ".mybot");
|
|
14
|
+
const ROOT = path.join(MYBOT_DIR, "files");
|
|
15
|
+
const INDEX_PATH = path.join(MYBOT_DIR, "index.json");
|
|
16
|
+
const SESSION_PATH = path.join(MYBOT_DIR, "session.json");
|
|
17
|
+
const MAX_BYTES = 500_000;
|
|
18
|
+
|
|
19
|
+
// ── directory / index helpers ─────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function ensureDir(p) {
|
|
22
|
+
fs.mkdirSync(p, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadIndex() {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(INDEX_PATH, "utf8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return { files: {} };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveIndex(idx) {
|
|
34
|
+
ensureDir(path.dirname(INDEX_PATH));
|
|
35
|
+
fs.writeFileSync(INDEX_PATH, JSON.stringify(idx, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── session helpers ───────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function loadSession() {
|
|
41
|
+
try {
|
|
42
|
+
const s = JSON.parse(fs.readFileSync(SESSION_PATH, "utf8"));
|
|
43
|
+
if (!s?.access_token || !s?.refresh_token) throw new Error("incomplete");
|
|
44
|
+
return s;
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error("Not logged in. Run `mybot login`.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function saveSession(session) {
|
|
51
|
+
ensureDir(MYBOT_DIR);
|
|
52
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function deleteSession() {
|
|
56
|
+
try { fs.unlinkSync(SESSION_PATH); } catch { /* already gone */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── authenticated supabase client ─────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
async function getAuthenticatedClient() {
|
|
62
|
+
const stored = loadSession(); // throws "Not logged in" if missing/corrupt
|
|
63
|
+
|
|
64
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
|
|
65
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
69
|
+
const isExpired = stored.expires_at && nowSec >= stored.expires_at - 60;
|
|
70
|
+
|
|
71
|
+
if (isExpired) {
|
|
72
|
+
console.error("MYBOT session refreshing");
|
|
73
|
+
const { data, error } = await supabase.auth.refreshSession({
|
|
74
|
+
refresh_token: stored.refresh_token,
|
|
75
|
+
});
|
|
76
|
+
if (error) throw new Error("Session expired. Run `mybot login` again.");
|
|
77
|
+
saveSession(data.session);
|
|
78
|
+
const { error: setErr } = await supabase.auth.setSession({
|
|
79
|
+
access_token: data.session.access_token,
|
|
80
|
+
refresh_token: data.session.refresh_token,
|
|
81
|
+
});
|
|
82
|
+
if (setErr) throw setErr;
|
|
83
|
+
} else {
|
|
84
|
+
const { error } = await supabase.auth.setSession({
|
|
85
|
+
access_token: stored.access_token,
|
|
86
|
+
refresh_token: stored.refresh_token,
|
|
87
|
+
});
|
|
88
|
+
if (error) throw new Error("Session invalid. Run `mybot login` again.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return supabase;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── path helpers ──────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function safeRel(pth) {
|
|
97
|
+
const cleaned = (pth ?? "").replaceAll("\\", "/").replace(/^\/+/, "");
|
|
98
|
+
const parts = cleaned.split("/");
|
|
99
|
+
if (!cleaned || parts.some((s) => s === "." || s === ".." || s.trim() === "")) {
|
|
100
|
+
throw new Error(`Unsafe path: ${pth}`);
|
|
101
|
+
}
|
|
102
|
+
return cleaned;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function localFullPath(rel) {
|
|
106
|
+
return path.join(ROOT, safeRel(rel));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readLocal(rel) {
|
|
110
|
+
const full = localFullPath(rel);
|
|
111
|
+
if (!fs.existsSync(full)) return null;
|
|
112
|
+
return fs.readFileSync(full, "utf8");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writeLocal(rel, content) {
|
|
116
|
+
const full = localFullPath(rel);
|
|
117
|
+
ensureDir(path.dirname(full));
|
|
118
|
+
fs.writeFileSync(full, content, "utf8");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── supabase queries ──────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
async function fetchRemoteUpdatedAt(supabase, rel) {
|
|
124
|
+
const relSafe = safeRel(rel);
|
|
125
|
+
const { data, error } = await supabase
|
|
126
|
+
.from("files")
|
|
127
|
+
.select("updated_at")
|
|
128
|
+
.eq("path", relSafe)
|
|
129
|
+
.limit(1);
|
|
130
|
+
if (error) throw error;
|
|
131
|
+
return data?.[0]?.updated_at ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function fetchRemoteContent(supabase, rel) {
|
|
135
|
+
const relSafe = safeRel(rel);
|
|
136
|
+
const { data, error } = await supabase
|
|
137
|
+
.from("files")
|
|
138
|
+
.select("content, updated_at")
|
|
139
|
+
.eq("path", relSafe)
|
|
140
|
+
.limit(1);
|
|
141
|
+
if (error) throw error;
|
|
142
|
+
return { content: data?.[0]?.content ?? "", updated_at: data?.[0]?.updated_at ?? null };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── core commands ─────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
async function getFile(rel) {
|
|
148
|
+
const relSafe = safeRel(rel);
|
|
149
|
+
const supabase = await getAuthenticatedClient();
|
|
150
|
+
ensureDir(ROOT);
|
|
151
|
+
const idx = loadIndex();
|
|
152
|
+
const local = readLocal(relSafe);
|
|
153
|
+
const cached = idx.files?.[relSafe];
|
|
154
|
+
|
|
155
|
+
if (local === null) {
|
|
156
|
+
console.error("MYBOT GET remote-refresh", relSafe);
|
|
157
|
+
const { content, updated_at } = await fetchRemoteContent(supabase, relSafe);
|
|
158
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
159
|
+
if (bytes > MAX_BYTES) throw new Error(`File too large (${bytes} bytes)`);
|
|
160
|
+
writeLocal(relSafe, content);
|
|
161
|
+
idx.files[relSafe] = { updated_at, bytes };
|
|
162
|
+
saveIndex(idx);
|
|
163
|
+
return content;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const remoteUpdatedAt = await fetchRemoteUpdatedAt(supabase, relSafe);
|
|
167
|
+
if (!remoteUpdatedAt) return local;
|
|
168
|
+
|
|
169
|
+
if (cached?.updated_at === remoteUpdatedAt) {
|
|
170
|
+
console.error("MYBOT GET local-hit", relSafe);
|
|
171
|
+
return local;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.error("MYBOT GET remote-refresh", relSafe);
|
|
175
|
+
const { content, updated_at } = await fetchRemoteContent(supabase, relSafe);
|
|
176
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
177
|
+
if (bytes > MAX_BYTES) throw new Error(`File too large (${bytes} bytes)`);
|
|
178
|
+
writeLocal(relSafe, content);
|
|
179
|
+
idx.files[relSafe] = { updated_at, bytes };
|
|
180
|
+
saveIndex(idx);
|
|
181
|
+
return content;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function putFile(rel, content) {
|
|
185
|
+
const relSafe = safeRel(rel);
|
|
186
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
187
|
+
if (bytes > MAX_BYTES) throw new Error(`File too large (${bytes} bytes)`);
|
|
188
|
+
|
|
189
|
+
const supabase = await getAuthenticatedClient();
|
|
190
|
+
ensureDir(ROOT);
|
|
191
|
+
const idx = loadIndex();
|
|
192
|
+
|
|
193
|
+
const { error } = await supabase
|
|
194
|
+
.from("files")
|
|
195
|
+
.upsert({ path: relSafe, content }, { onConflict: "path" });
|
|
196
|
+
if (error) throw error;
|
|
197
|
+
|
|
198
|
+
const updated_at = await fetchRemoteUpdatedAt(supabase, relSafe);
|
|
199
|
+
|
|
200
|
+
writeLocal(relSafe, content);
|
|
201
|
+
idx.files[relSafe] = { updated_at, bytes };
|
|
202
|
+
saveIndex(idx);
|
|
203
|
+
|
|
204
|
+
console.error("MYBOT PUT", relSafe, `bytes=${bytes}`, `updated_at=${updated_at}`);
|
|
205
|
+
return { bytes, updated_at };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── interactive prompts ───────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
async function promptLine(question) {
|
|
211
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
212
|
+
try {
|
|
213
|
+
return await rl.question(question);
|
|
214
|
+
} finally {
|
|
215
|
+
rl.close();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function promptPassword(question) {
|
|
220
|
+
if (!process.stdin.isTTY) {
|
|
221
|
+
throw new Error("`mybot login` requires an interactive terminal (stdin is not a TTY).");
|
|
222
|
+
}
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
process.stdout.write(question);
|
|
225
|
+
let pass = "";
|
|
226
|
+
|
|
227
|
+
const onData = (buf) => {
|
|
228
|
+
const ch = buf.toString("utf8");
|
|
229
|
+
if (ch === "\r" || ch === "\n" || ch === "\u0004") {
|
|
230
|
+
// enter / ctrl-d
|
|
231
|
+
process.stdin.setRawMode(false);
|
|
232
|
+
process.stdin.removeListener("data", onData);
|
|
233
|
+
process.stdin.pause();
|
|
234
|
+
process.stdout.write("\n");
|
|
235
|
+
resolve(pass);
|
|
236
|
+
} else if (ch === "\u0003") {
|
|
237
|
+
// ctrl-c
|
|
238
|
+
process.stdin.setRawMode(false);
|
|
239
|
+
process.stdout.write("\n");
|
|
240
|
+
process.exit(1);
|
|
241
|
+
} else if (ch === "\u007f" || ch === "\b") {
|
|
242
|
+
// backspace
|
|
243
|
+
if (pass.length > 0) pass = pass.slice(0, -1);
|
|
244
|
+
} else {
|
|
245
|
+
pass += ch;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
process.stdin.setRawMode(true);
|
|
250
|
+
process.stdin.resume();
|
|
251
|
+
process.stdin.on("data", onData);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── auth commands ─────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
async function cmdLogin() {
|
|
258
|
+
const email = await promptLine("Email: ");
|
|
259
|
+
const password = await promptPassword("Password: ");
|
|
260
|
+
|
|
261
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
|
|
262
|
+
auth: { persistSession: false },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
|
|
266
|
+
if (error) throw error;
|
|
267
|
+
|
|
268
|
+
saveSession(data.session);
|
|
269
|
+
console.log("Logged in.");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function cmdLogout() {
|
|
273
|
+
let supabase;
|
|
274
|
+
try {
|
|
275
|
+
supabase = await getAuthenticatedClient();
|
|
276
|
+
} catch { /* not logged in — still clean up local session */ }
|
|
277
|
+
|
|
278
|
+
if (supabase) {
|
|
279
|
+
try { await supabase.auth.signOut(); } catch { /* best-effort */ }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
deleteSession();
|
|
283
|
+
console.log("Logged out.");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function cmdWhoami() {
|
|
287
|
+
let stored;
|
|
288
|
+
try { stored = loadSession(); } catch {
|
|
289
|
+
console.log("Not logged in.");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const email = stored.user?.email ?? "(unknown)";
|
|
293
|
+
const id = stored.user?.id ?? "(unknown)";
|
|
294
|
+
console.log(`Logged in as ${email} (${id})`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── main ──────────────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
async function main() {
|
|
300
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
301
|
+
|
|
302
|
+
if (cmd === "login") { await cmdLogin(); return; }
|
|
303
|
+
if (cmd === "logout") { await cmdLogout(); return; }
|
|
304
|
+
if (cmd === "whoami") { await cmdWhoami(); return; }
|
|
305
|
+
|
|
306
|
+
if (cmd === "get") {
|
|
307
|
+
const rel = args[0];
|
|
308
|
+
if (!rel) { console.log("Usage: mybot get <path>"); process.exit(1); }
|
|
309
|
+
const content = await getFile(rel);
|
|
310
|
+
process.stdout.write(content);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (cmd === "put") {
|
|
315
|
+
const rel = args[0];
|
|
316
|
+
if (!rel) { console.log("Usage: mybot put <path>"); process.exit(1); }
|
|
317
|
+
const chunks = [];
|
|
318
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
319
|
+
const content = Buffer.concat(chunks).toString("utf8");
|
|
320
|
+
const result = await putFile(rel, content);
|
|
321
|
+
console.log(`PUT ok ${safeRel(rel)} bytes=${result.bytes} updated_at=${result.updated_at}`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log("Usage:");
|
|
326
|
+
console.log(" mybot login");
|
|
327
|
+
console.log(" mybot logout");
|
|
328
|
+
console.log(" mybot whoami");
|
|
329
|
+
console.log(" mybot get <path>");
|
|
330
|
+
console.log(" mybot put <path>");
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
main().catch((e) => {
|
|
335
|
+
console.error("MYBOT:", e?.message ?? e);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
});
|