delivered-cli 0.1.0 → 0.3.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.
@@ -0,0 +1,4 @@
1
+
2
+ > delivered-cli@0.1.0 build /home/hyapadi/DeliveredConvexExpoJS/cli
3
+ > tsc
4
+
package/dist/index.js CHANGED
@@ -45,12 +45,14 @@ const readline = __importStar(require("readline"));
45
45
  const config = new conf_1.default({
46
46
  projectName: "delivered-cli",
47
47
  });
48
- const API_URL = config.get("apiUrl") || "https://delivered.md/api/v1";
48
+ // Convex HTTP endpoint URL
49
+ const DEFAULT_API_URL = "https://fabulous-chipmunk-797.convex.site/api";
50
+ const API_URL = config.get("apiUrl") || DEFAULT_API_URL;
49
51
  const program = new commander_1.Command();
50
52
  program
51
53
  .name("delivered")
52
54
  .description("CLI for delivered.md - Focus. Ship. Repeat.")
53
- .version("0.1.0");
55
+ .version("0.3.0");
54
56
  // Helper to get API key
55
57
  function getApiKey() {
56
58
  const apiKey = config.get("apiKey");
@@ -68,6 +70,7 @@ async function apiRequest(endpoint, options = {}) {
68
70
  ...options,
69
71
  headers: {
70
72
  Authorization: `Bearer ${apiKey}`,
73
+ "Content-Type": "application/json",
71
74
  ...options.headers,
72
75
  },
73
76
  });
@@ -77,38 +80,51 @@ async function apiRequest(endpoint, options = {}) {
77
80
  program
78
81
  .command("login")
79
82
  .description("Login with your API key")
80
- .action(async () => {
81
- const rl = readline.createInterface({
82
- input: process.stdin,
83
- output: process.stdout,
84
- });
85
- console.log("\nTo get your API key:");
86
- console.log("1. Go to https://delivered.md/settings");
87
- console.log("2. Create a new API key");
88
- console.log("3. Copy and paste it below\n");
89
- rl.question("API Key: ", async (apiKey) => {
90
- rl.close();
83
+ .argument("[api-key]", "API key (or omit to enter interactively)")
84
+ .action(async (apiKeyArg) => {
85
+ const processLogin = async (apiKey) => {
91
86
  if (!apiKey || !apiKey.startsWith("dlv_")) {
92
87
  console.error("Error: Invalid API key format. Keys start with 'dlv_'");
93
88
  process.exit(1);
94
89
  }
95
- // Test the key
90
+ // Test the key by calling whoami
96
91
  try {
97
- const response = await fetch(`${API_URL}/page`, {
92
+ const response = await fetch(`${API_URL}/whoami`, {
98
93
  headers: { Authorization: `Bearer ${apiKey}` },
99
94
  });
100
95
  if (response.status === 401) {
101
96
  console.error("Error: Invalid API key");
102
97
  process.exit(1);
103
98
  }
99
+ const data = await response.json();
104
100
  config.set("apiKey", apiKey);
105
- console.log("\nLogged in successfully!");
101
+ console.log(`\nLogged in as ${data.username || data.email}!`);
102
+ if (data.pageUrl) {
103
+ console.log(`Your page: ${data.pageUrl}`);
104
+ }
106
105
  }
107
106
  catch (error) {
108
107
  console.error("Error: Failed to validate API key");
109
108
  process.exit(1);
110
109
  }
111
- });
110
+ };
111
+ if (apiKeyArg) {
112
+ await processLogin(apiKeyArg);
113
+ }
114
+ else {
115
+ const rl = readline.createInterface({
116
+ input: process.stdin,
117
+ output: process.stdout,
118
+ });
119
+ console.log("\nTo get your API key:");
120
+ console.log("1. Go to https://app.delivered.md/settings");
121
+ console.log("2. Create a new API key");
122
+ console.log("3. Copy and paste it below\n");
123
+ rl.question("API Key: ", async (apiKey) => {
124
+ rl.close();
125
+ await processLogin(apiKey);
126
+ });
127
+ }
112
128
  });
113
129
  // Logout command
114
130
  program
@@ -118,150 +134,159 @@ program
118
134
  config.delete("apiKey");
119
135
  console.log("Logged out successfully.");
120
136
  });
121
- // Pull command
137
+ // Whoami command
122
138
  program
123
- .command("pull")
124
- .description("Download your page as markdown")
125
- .option("-o, --output <file>", "Output file path")
126
- .action(async (options) => {
139
+ .command("whoami")
140
+ .description("Show current user info")
141
+ .action(async () => {
127
142
  try {
128
- const response = await apiRequest("/page");
143
+ const response = await apiRequest("/whoami");
129
144
  if (!response.ok) {
130
145
  const error = await response.json();
131
- console.error(`Error: ${error.error || "Failed to fetch page"}`);
146
+ console.error(`Error: ${error.error || "Failed to get user info"}`);
132
147
  process.exit(1);
133
148
  }
134
- const markdown = await response.text();
135
- if (options.output) {
136
- const outputPath = path.resolve(options.output);
137
- fs.writeFileSync(outputPath, markdown);
138
- console.log(`Page saved to ${outputPath}`);
139
- }
140
- else {
141
- // Output to stdout
142
- console.log(markdown);
149
+ const data = await response.json();
150
+ console.log(`\nUsername: ${data.username || "(not set)"}`);
151
+ console.log(`Name: ${data.name || "(not set)"}`);
152
+ console.log(`Email: ${data.email || "(not set)"}`);
153
+ if (data.pageUrl) {
154
+ console.log(`Page URL: ${data.pageUrl}`);
143
155
  }
144
156
  }
145
157
  catch (error) {
146
- console.error("Error: Failed to pull page");
158
+ console.error("Error: Failed to get user info");
147
159
  process.exit(1);
148
160
  }
149
161
  });
150
- // Push command
162
+ // Download command
151
163
  program
152
- .command("push [file]")
153
- .description("Upload markdown content to your page")
154
- .action(async (file) => {
164
+ .command("download")
165
+ .description("Download your page as markdown to current directory")
166
+ .action(async () => {
155
167
  try {
156
- let content;
157
- if (file) {
158
- // Read from file
159
- const filePath = path.resolve(file);
160
- if (!fs.existsSync(filePath)) {
161
- console.error(`Error: File not found: ${filePath}`);
162
- process.exit(1);
163
- }
164
- content = fs.readFileSync(filePath, "utf-8");
165
- }
166
- else {
167
- // Read from stdin
168
- const chunks = [];
169
- for await (const chunk of process.stdin) {
170
- chunks.push(chunk);
171
- }
172
- content = Buffer.concat(chunks).toString("utf-8");
168
+ // Get username first
169
+ const whoamiResponse = await apiRequest("/whoami");
170
+ if (!whoamiResponse.ok) {
171
+ console.error("Error: Failed to get user info");
172
+ process.exit(1);
173
173
  }
174
- if (!content.trim()) {
175
- console.error("Error: Content cannot be empty");
174
+ const userInfo = await whoamiResponse.json();
175
+ const username = userInfo.username;
176
+ if (!username) {
177
+ console.error("Error: No username set. Visit https://app.delivered.md to claim a username first.");
176
178
  process.exit(1);
177
179
  }
178
- const response = await apiRequest("/page", {
179
- method: "PUT",
180
- headers: { "Content-Type": "text/markdown" },
181
- body: content,
182
- });
180
+ // Fetch page content
181
+ const response = await apiRequest("/page");
183
182
  if (!response.ok) {
184
183
  const error = await response.json();
185
- console.error(`Error: ${error.error || "Failed to push page"}`);
184
+ console.error(`Error: ${error.error || "Failed to fetch page"}`);
186
185
  process.exit(1);
187
186
  }
188
- console.log("Page updated successfully!");
187
+ const data = await response.json();
188
+ const markdown = data.content;
189
+ // Save to current directory as {username}.md
190
+ const filename = `${username}.md`;
191
+ const outputPath = path.resolve(process.cwd(), filename);
192
+ fs.writeFileSync(outputPath, markdown);
193
+ console.log(`\nDownloaded your page to ./${filename}`);
189
194
  }
190
195
  catch (error) {
191
- console.error("Error: Failed to push page");
196
+ console.error("Error: Failed to download page");
192
197
  process.exit(1);
193
198
  }
194
199
  });
195
- // Snapshots command
200
+ // Upload command
196
201
  program
197
- .command("snapshots")
198
- .description("List your page snapshots")
199
- .action(async () => {
202
+ .command("upload")
203
+ .description("Upload a markdown file to your page")
204
+ .argument("<file>", "Path to markdown file")
205
+ .action(async (file) => {
200
206
  try {
201
- const response = await apiRequest("/snapshots");
202
- if (!response.ok) {
203
- const error = await response.json();
204
- console.error(`Error: ${error.error || "Failed to fetch snapshots"}`);
207
+ // Read from file
208
+ const filePath = path.resolve(file);
209
+ if (!fs.existsSync(filePath)) {
210
+ console.error(`Error: File not found: ${filePath}`);
205
211
  process.exit(1);
206
212
  }
207
- const data = await response.json();
208
- const snapshots = data.snapshots;
209
- if (snapshots.length === 0) {
210
- console.log("No snapshots found.");
211
- return;
213
+ const content = fs.readFileSync(filePath, "utf-8");
214
+ if (!content.trim()) {
215
+ console.error("Error: File is empty");
216
+ process.exit(1);
212
217
  }
213
- console.log("\nSnapshots:\n");
214
- snapshots.forEach((snapshot, index) => {
215
- const date = new Date(snapshot.createdAt).toLocaleString();
216
- const accessIcon = snapshot.accessible ? " " : "[locked]";
217
- console.log(`${index + 1}. ${date} ${accessIcon}`);
218
- console.log(` ID: ${snapshot.id}\n`);
219
- });
220
- }
221
- catch (error) {
222
- console.error("Error: Failed to fetch snapshots");
223
- process.exit(1);
224
- }
225
- });
226
- // Revert command
227
- program
228
- .command("revert <snapshot-id>")
229
- .description("Revert to a specific snapshot")
230
- .action(async (snapshotId) => {
231
- try {
232
- const response = await apiRequest("/revert", {
233
- method: "POST",
234
- headers: { "Content-Type": "application/json" },
235
- body: JSON.stringify({ snapshotId }),
218
+ // Check file size locally first (100KB limit)
219
+ const sizeBytes = Buffer.byteLength(content, "utf-8");
220
+ const maxSizeBytes = 100 * 1024;
221
+ if (sizeBytes > maxSizeBytes) {
222
+ const wordCount = content.trim().split(/\s+/).filter(Boolean).length;
223
+ const maxWords = Math.floor(maxSizeBytes / 6);
224
+ console.error(`\nError: File too large.`);
225
+ console.error(`Your file: ~${wordCount.toLocaleString()} words (${Math.round(sizeBytes / 1024)}KB)`);
226
+ console.error(`Maximum: ~${maxWords.toLocaleString()} words (100KB)`);
227
+ process.exit(1);
228
+ }
229
+ // Upload
230
+ const response = await apiRequest("/page", {
231
+ method: "PUT",
232
+ body: JSON.stringify({ content }),
236
233
  });
237
234
  if (!response.ok) {
238
235
  const error = await response.json();
239
- console.error(`Error: ${error.error || "Failed to revert"}`);
236
+ // Handle specific error types
237
+ if (error.error === "FILE_TOO_LARGE") {
238
+ console.error(`\nError: File too large.`);
239
+ console.error(`Your file: ~${error.wordCount?.toLocaleString() || "?"} words (${Math.round((error.currentSize || 0) / 1024)}KB)`);
240
+ console.error(`Maximum: ~${error.maxWords?.toLocaleString() || "16,000"} words (100KB)`);
241
+ process.exit(1);
242
+ }
243
+ if (error.error === "RATE_LIMIT") {
244
+ console.error(`\nError: Upload limit reached (${error.maxUploads || 50}/day).`);
245
+ console.error(`You can upload again in ${error.nextUploadIn || "a while"}.`);
246
+ process.exit(1);
247
+ }
248
+ console.error(`Error: ${error.message || error.error || "Failed to upload"}`);
240
249
  process.exit(1);
241
250
  }
242
- const data = await response.json();
243
- console.log(`Reverted successfully to snapshot from ${new Date(data.revertedTo).toLocaleString()}`);
251
+ // Get username for the success message
252
+ const whoamiResponse = await apiRequest("/whoami");
253
+ let pageUrl = "https://delivered.md";
254
+ if (whoamiResponse.ok) {
255
+ const userInfo = await whoamiResponse.json();
256
+ if (userInfo.username) {
257
+ pageUrl = `https://delivered.md/${userInfo.username}`;
258
+ }
259
+ }
260
+ console.log(`\nUploaded successfully!`);
261
+ console.log(`View your page: ${pageUrl}`);
244
262
  }
245
263
  catch (error) {
246
- console.error("Error: Failed to revert");
264
+ console.error("Error: Failed to upload page");
247
265
  process.exit(1);
248
266
  }
249
267
  });
268
+ // Note: snapshots and revert commands can be added later when HTTP endpoints are implemented
250
269
  // Config command
251
270
  program
252
271
  .command("config")
253
272
  .description("View or set configuration")
254
273
  .option("--api-url <url>", "Set custom API URL")
255
- .option("--show", "Show current configuration")
274
+ .option("--reset", "Reset to default configuration")
256
275
  .action((options) => {
276
+ if (options.reset) {
277
+ config.delete("apiUrl");
278
+ console.log("Configuration reset to defaults.");
279
+ }
257
280
  if (options.apiUrl) {
258
281
  config.set("apiUrl", options.apiUrl);
259
282
  console.log(`API URL set to: ${options.apiUrl}`);
260
283
  }
261
- if (options.show || (!options.apiUrl)) {
284
+ if (!options.apiUrl && !options.reset) {
285
+ const customUrl = config.get("apiUrl");
262
286
  console.log("\nConfiguration:");
263
- console.log(` API URL: ${config.get("apiUrl") || API_URL} (default)`);
287
+ console.log(` API URL: ${customUrl || DEFAULT_API_URL}${customUrl ? "" : " (default)"}`);
264
288
  console.log(` Logged in: ${config.get("apiKey") ? "Yes" : "No"}`);
289
+ console.log(` Config file: ${config.path}`);
265
290
  }
266
291
  });
267
292
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivered-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for delivered.md - Focus. Ship. Repeat.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -9,21 +9,27 @@
9
9
  "scripts": {
10
10
  "build": "tsc",
11
11
  "dev": "tsc -w",
12
- "start": "node dist/index.js"
12
+ "start": "node dist/index.js",
13
+ "prepublishOnly": "npm run build"
13
14
  },
14
15
  "keywords": [
15
16
  "delivered",
16
17
  "markdown",
17
18
  "productivity",
18
19
  "goals",
19
- "cli"
20
+ "cli",
21
+ "accountability"
20
22
  ],
21
- "author": "",
23
+ "author": "hyapadi",
22
24
  "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/anomalyco/delivered"
28
+ },
29
+ "homepage": "https://delivered.md",
23
30
  "dependencies": {
24
31
  "commander": "^12.1.0",
25
- "conf": "^12.0.0",
26
- "node-fetch": "^3.3.2"
32
+ "conf": "^12.0.0"
27
33
  },
28
34
  "devDependencies": {
29
35
  "@types/node": "^20.10.0",
package/src/index.ts CHANGED
@@ -10,14 +10,16 @@ const config = new Conf<{ apiKey?: string; apiUrl?: string }>({
10
10
  projectName: "delivered-cli",
11
11
  });
12
12
 
13
- const API_URL = config.get("apiUrl") || "https://delivered.md/api/v1";
13
+ // Convex HTTP endpoint URL
14
+ const DEFAULT_API_URL = "https://fabulous-chipmunk-797.convex.site/api";
15
+ const API_URL = config.get("apiUrl") || DEFAULT_API_URL;
14
16
 
15
17
  const program = new Command();
16
18
 
17
19
  program
18
20
  .name("delivered")
19
21
  .description("CLI for delivered.md - Focus. Ship. Repeat.")
20
- .version("0.1.0");
22
+ .version("0.3.0");
21
23
 
22
24
  // Helper to get API key
23
25
  function getApiKey(): string {
@@ -32,7 +34,7 @@ function getApiKey(): string {
32
34
  // Helper for API requests
33
35
  async function apiRequest(
34
36
  endpoint: string,
35
- options: RequestInit = {}
37
+ options: RequestInit = {},
36
38
  ): Promise<Response> {
37
39
  const apiKey = getApiKey();
38
40
  const url = `${API_URL}${endpoint}`;
@@ -41,6 +43,7 @@ async function apiRequest(
41
43
  ...options,
42
44
  headers: {
43
45
  Authorization: `Bearer ${apiKey}`,
46
+ "Content-Type": "application/json",
44
47
  ...options.headers,
45
48
  },
46
49
  });
@@ -52,28 +55,17 @@ async function apiRequest(
52
55
  program
53
56
  .command("login")
54
57
  .description("Login with your API key")
55
- .action(async () => {
56
- const rl = readline.createInterface({
57
- input: process.stdin,
58
- output: process.stdout,
59
- });
60
-
61
- console.log("\nTo get your API key:");
62
- console.log("1. Go to https://delivered.md/settings");
63
- console.log("2. Create a new API key");
64
- console.log("3. Copy and paste it below\n");
65
-
66
- rl.question("API Key: ", async (apiKey) => {
67
- rl.close();
68
-
58
+ .argument("[api-key]", "API key (or omit to enter interactively)")
59
+ .action(async (apiKeyArg?: string) => {
60
+ const processLogin = async (apiKey: string) => {
69
61
  if (!apiKey || !apiKey.startsWith("dlv_")) {
70
62
  console.error("Error: Invalid API key format. Keys start with 'dlv_'");
71
63
  process.exit(1);
72
64
  }
73
65
 
74
- // Test the key
66
+ // Test the key by calling whoami
75
67
  try {
76
- const response = await fetch(`${API_URL}/page`, {
68
+ const response = await fetch(`${API_URL}/whoami`, {
77
69
  headers: { Authorization: `Bearer ${apiKey}` },
78
70
  });
79
71
 
@@ -82,13 +74,36 @@ program
82
74
  process.exit(1);
83
75
  }
84
76
 
77
+ const data = await response.json();
85
78
  config.set("apiKey", apiKey);
86
- console.log("\nLogged in successfully!");
79
+ console.log(`\nLogged in as ${data.username || data.email}!`);
80
+ if (data.pageUrl) {
81
+ console.log(`Your page: ${data.pageUrl}`);
82
+ }
87
83
  } catch (error) {
88
84
  console.error("Error: Failed to validate API key");
89
85
  process.exit(1);
90
86
  }
91
- });
87
+ };
88
+
89
+ if (apiKeyArg) {
90
+ await processLogin(apiKeyArg);
91
+ } else {
92
+ const rl = readline.createInterface({
93
+ input: process.stdin,
94
+ output: process.stdout,
95
+ });
96
+
97
+ console.log("\nTo get your API key:");
98
+ console.log("1. Go to https://app.delivered.md/settings");
99
+ console.log("2. Create a new API key");
100
+ console.log("3. Copy and paste it below\n");
101
+
102
+ rl.question("API Key: ", async (apiKey) => {
103
+ rl.close();
104
+ await processLogin(apiKey);
105
+ });
106
+ }
92
107
  });
93
108
 
94
109
  // Logout command
@@ -100,170 +115,195 @@ program
100
115
  console.log("Logged out successfully.");
101
116
  });
102
117
 
103
- // Pull command
118
+ // Whoami command
104
119
  program
105
- .command("pull")
106
- .description("Download your page as markdown")
107
- .option("-o, --output <file>", "Output file path")
108
- .action(async (options) => {
120
+ .command("whoami")
121
+ .description("Show current user info")
122
+ .action(async () => {
109
123
  try {
110
- const response = await apiRequest("/page");
124
+ const response = await apiRequest("/whoami");
111
125
 
112
126
  if (!response.ok) {
113
127
  const error = await response.json();
114
- console.error(`Error: ${error.error || "Failed to fetch page"}`);
128
+ console.error(`Error: ${error.error || "Failed to get user info"}`);
115
129
  process.exit(1);
116
130
  }
117
131
 
118
- const markdown = await response.text();
119
-
120
- if (options.output) {
121
- const outputPath = path.resolve(options.output);
122
- fs.writeFileSync(outputPath, markdown);
123
- console.log(`Page saved to ${outputPath}`);
124
- } else {
125
- // Output to stdout
126
- console.log(markdown);
132
+ const data = await response.json();
133
+ console.log(`\nUsername: ${data.username || "(not set)"}`);
134
+ console.log(`Name: ${data.name || "(not set)"}`);
135
+ console.log(`Email: ${data.email || "(not set)"}`);
136
+ if (data.pageUrl) {
137
+ console.log(`Page URL: ${data.pageUrl}`);
127
138
  }
128
139
  } catch (error) {
129
- console.error("Error: Failed to pull page");
140
+ console.error("Error: Failed to get user info");
130
141
  process.exit(1);
131
142
  }
132
143
  });
133
144
 
134
- // Push command
145
+ // Download command
135
146
  program
136
- .command("push [file]")
137
- .description("Upload markdown content to your page")
138
- .action(async (file) => {
147
+ .command("download")
148
+ .description("Download your page as markdown to current directory")
149
+ .action(async () => {
139
150
  try {
140
- let content: string;
141
-
142
- if (file) {
143
- // Read from file
144
- const filePath = path.resolve(file);
145
- if (!fs.existsSync(filePath)) {
146
- console.error(`Error: File not found: ${filePath}`);
147
- process.exit(1);
148
- }
149
- content = fs.readFileSync(filePath, "utf-8");
150
- } else {
151
- // Read from stdin
152
- const chunks: Buffer[] = [];
153
- for await (const chunk of process.stdin) {
154
- chunks.push(chunk);
155
- }
156
- content = Buffer.concat(chunks).toString("utf-8");
151
+ // Get username first
152
+ const whoamiResponse = await apiRequest("/whoami");
153
+ if (!whoamiResponse.ok) {
154
+ console.error("Error: Failed to get user info");
155
+ process.exit(1);
157
156
  }
157
+ const userInfo = await whoamiResponse.json();
158
+ const username = userInfo.username;
158
159
 
159
- if (!content.trim()) {
160
- console.error("Error: Content cannot be empty");
160
+ if (!username) {
161
+ console.error(
162
+ "Error: No username set. Visit https://app.delivered.md to claim a username first.",
163
+ );
161
164
  process.exit(1);
162
165
  }
163
166
 
164
- const response = await apiRequest("/page", {
165
- method: "PUT",
166
- headers: { "Content-Type": "text/markdown" },
167
- body: content,
168
- });
167
+ // Fetch page content
168
+ const response = await apiRequest("/page");
169
169
 
170
170
  if (!response.ok) {
171
171
  const error = await response.json();
172
- console.error(`Error: ${error.error || "Failed to push page"}`);
172
+ console.error(`Error: ${error.error || "Failed to fetch page"}`);
173
173
  process.exit(1);
174
174
  }
175
175
 
176
- console.log("Page updated successfully!");
176
+ const data = await response.json();
177
+ const markdown = data.content;
178
+
179
+ // Save to current directory as {username}.md
180
+ const filename = `${username}.md`;
181
+ const outputPath = path.resolve(process.cwd(), filename);
182
+ fs.writeFileSync(outputPath, markdown);
183
+
184
+ console.log(`\nDownloaded your page to ./${filename}`);
177
185
  } catch (error) {
178
- console.error("Error: Failed to push page");
186
+ console.error("Error: Failed to download page");
179
187
  process.exit(1);
180
188
  }
181
189
  });
182
190
 
183
- // Snapshots command
191
+ // Upload command
184
192
  program
185
- .command("snapshots")
186
- .description("List your page snapshots")
187
- .action(async () => {
193
+ .command("upload")
194
+ .description("Upload a markdown file to your page")
195
+ .argument("<file>", "Path to markdown file")
196
+ .action(async (file) => {
188
197
  try {
189
- const response = await apiRequest("/snapshots");
190
-
191
- if (!response.ok) {
192
- const error = await response.json();
193
- console.error(`Error: ${error.error || "Failed to fetch snapshots"}`);
198
+ // Read from file
199
+ const filePath = path.resolve(file);
200
+ if (!fs.existsSync(filePath)) {
201
+ console.error(`Error: File not found: ${filePath}`);
194
202
  process.exit(1);
195
203
  }
204
+ const content = fs.readFileSync(filePath, "utf-8");
196
205
 
197
- const data = await response.json();
198
- const snapshots = data.snapshots;
199
-
200
- if (snapshots.length === 0) {
201
- console.log("No snapshots found.");
202
- return;
206
+ if (!content.trim()) {
207
+ console.error("Error: File is empty");
208
+ process.exit(1);
203
209
  }
204
210
 
205
- console.log("\nSnapshots:\n");
206
- snapshots.forEach(
207
- (
208
- snapshot: { id: string; createdAt: number; accessible: boolean },
209
- index: number
210
- ) => {
211
- const date = new Date(snapshot.createdAt).toLocaleString();
212
- const accessIcon = snapshot.accessible ? " " : "[locked]";
213
- console.log(`${index + 1}. ${date} ${accessIcon}`);
214
- console.log(` ID: ${snapshot.id}\n`);
215
- }
216
- );
217
- } catch (error) {
218
- console.error("Error: Failed to fetch snapshots");
219
- process.exit(1);
220
- }
221
- });
211
+ // Check file size locally first (100KB limit)
212
+ const sizeBytes = Buffer.byteLength(content, "utf-8");
213
+ const maxSizeBytes = 100 * 1024;
214
+ if (sizeBytes > maxSizeBytes) {
215
+ const wordCount = content.trim().split(/\s+/).filter(Boolean).length;
216
+ const maxWords = Math.floor(maxSizeBytes / 6);
217
+ console.error(`\nError: File too large.`);
218
+ console.error(
219
+ `Your file: ~${wordCount.toLocaleString()} words (${Math.round(sizeBytes / 1024)}KB)`,
220
+ );
221
+ console.error(`Maximum: ~${maxWords.toLocaleString()} words (100KB)`);
222
+ process.exit(1);
223
+ }
222
224
 
223
- // Revert command
224
- program
225
- .command("revert <snapshot-id>")
226
- .description("Revert to a specific snapshot")
227
- .action(async (snapshotId) => {
228
- try {
229
- const response = await apiRequest("/revert", {
230
- method: "POST",
231
- headers: { "Content-Type": "application/json" },
232
- body: JSON.stringify({ snapshotId }),
225
+ // Upload
226
+ const response = await apiRequest("/page", {
227
+ method: "PUT",
228
+ body: JSON.stringify({ content }),
233
229
  });
234
230
 
235
231
  if (!response.ok) {
236
232
  const error = await response.json();
237
- console.error(`Error: ${error.error || "Failed to revert"}`);
233
+
234
+ // Handle specific error types
235
+ if (error.error === "FILE_TOO_LARGE") {
236
+ console.error(`\nError: File too large.`);
237
+ console.error(
238
+ `Your file: ~${error.wordCount?.toLocaleString() || "?"} words (${Math.round((error.currentSize || 0) / 1024)}KB)`,
239
+ );
240
+ console.error(
241
+ `Maximum: ~${error.maxWords?.toLocaleString() || "16,000"} words (100KB)`,
242
+ );
243
+ process.exit(1);
244
+ }
245
+
246
+ if (error.error === "RATE_LIMIT") {
247
+ console.error(
248
+ `\nError: Upload limit reached (${error.maxUploads || 50}/day).`,
249
+ );
250
+ console.error(
251
+ `You can upload again in ${error.nextUploadIn || "a while"}.`,
252
+ );
253
+ process.exit(1);
254
+ }
255
+
256
+ console.error(
257
+ `Error: ${error.message || error.error || "Failed to upload"}`,
258
+ );
238
259
  process.exit(1);
239
260
  }
240
261
 
241
- const data = await response.json();
242
- console.log(
243
- `Reverted successfully to snapshot from ${new Date(data.revertedTo).toLocaleString()}`
244
- );
262
+ // Get username for the success message
263
+ const whoamiResponse = await apiRequest("/whoami");
264
+ let pageUrl = "https://delivered.md";
265
+ if (whoamiResponse.ok) {
266
+ const userInfo = await whoamiResponse.json();
267
+ if (userInfo.username) {
268
+ pageUrl = `https://delivered.md/${userInfo.username}`;
269
+ }
270
+ }
271
+
272
+ console.log(`\nUploaded successfully!`);
273
+ console.log(`View your page: ${pageUrl}`);
245
274
  } catch (error) {
246
- console.error("Error: Failed to revert");
275
+ console.error("Error: Failed to upload page");
247
276
  process.exit(1);
248
277
  }
249
278
  });
250
279
 
280
+ // Note: snapshots and revert commands can be added later when HTTP endpoints are implemented
281
+
251
282
  // Config command
252
283
  program
253
284
  .command("config")
254
285
  .description("View or set configuration")
255
286
  .option("--api-url <url>", "Set custom API URL")
256
- .option("--show", "Show current configuration")
287
+ .option("--reset", "Reset to default configuration")
257
288
  .action((options) => {
289
+ if (options.reset) {
290
+ config.delete("apiUrl");
291
+ console.log("Configuration reset to defaults.");
292
+ }
293
+
258
294
  if (options.apiUrl) {
259
295
  config.set("apiUrl", options.apiUrl);
260
296
  console.log(`API URL set to: ${options.apiUrl}`);
261
297
  }
262
298
 
263
- if (options.show || (!options.apiUrl)) {
299
+ if (!options.apiUrl && !options.reset) {
300
+ const customUrl = config.get("apiUrl");
264
301
  console.log("\nConfiguration:");
265
- console.log(` API URL: ${config.get("apiUrl") || API_URL} (default)`);
302
+ console.log(
303
+ ` API URL: ${customUrl || DEFAULT_API_URL}${customUrl ? "" : " (default)"}`,
304
+ );
266
305
  console.log(` Logged in: ${config.get("apiKey") ? "Yes" : "No"}`);
306
+ console.log(` Config file: ${config.path}`);
267
307
  }
268
308
  });
269
309