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.
- package/.turbo/turbo-build.log +4 -0
- package/dist/index.js +133 -108
- package/package.json +12 -6
- package/src/index.ts +160 -120
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
|
-
|
|
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.
|
|
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
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
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}/
|
|
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(
|
|
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
|
-
//
|
|
137
|
+
// Whoami command
|
|
122
138
|
program
|
|
123
|
-
.command("
|
|
124
|
-
.description("
|
|
125
|
-
.
|
|
126
|
-
.action(async (options) => {
|
|
139
|
+
.command("whoami")
|
|
140
|
+
.description("Show current user info")
|
|
141
|
+
.action(async () => {
|
|
127
142
|
try {
|
|
128
|
-
const response = await apiRequest("/
|
|
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
|
|
146
|
+
console.error(`Error: ${error.error || "Failed to get user info"}`);
|
|
132
147
|
process.exit(1);
|
|
133
148
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
158
|
+
console.error("Error: Failed to get user info");
|
|
147
159
|
process.exit(1);
|
|
148
160
|
}
|
|
149
161
|
});
|
|
150
|
-
//
|
|
162
|
+
// Download command
|
|
151
163
|
program
|
|
152
|
-
.command("
|
|
153
|
-
.description("
|
|
154
|
-
.action(async (
|
|
164
|
+
.command("download")
|
|
165
|
+
.description("Download your page as markdown to current directory")
|
|
166
|
+
.action(async () => {
|
|
155
167
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
|
184
|
+
console.error(`Error: ${error.error || "Failed to fetch page"}`);
|
|
186
185
|
process.exit(1);
|
|
187
186
|
}
|
|
188
|
-
|
|
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
|
|
196
|
+
console.error("Error: Failed to download page");
|
|
192
197
|
process.exit(1);
|
|
193
198
|
}
|
|
194
199
|
});
|
|
195
|
-
//
|
|
200
|
+
// Upload command
|
|
196
201
|
program
|
|
197
|
-
.command("
|
|
198
|
-
.description("
|
|
199
|
-
.
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
console.error(`Error:
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
|
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("--
|
|
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.
|
|
284
|
+
if (!options.apiUrl && !options.reset) {
|
|
285
|
+
const customUrl = config.get("apiUrl");
|
|
262
286
|
console.log("\nConfiguration:");
|
|
263
|
-
console.log(` API URL: ${
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
.
|
|
56
|
-
|
|
57
|
-
|
|
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}/
|
|
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(
|
|
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
|
-
//
|
|
118
|
+
// Whoami command
|
|
104
119
|
program
|
|
105
|
-
.command("
|
|
106
|
-
.description("
|
|
107
|
-
.
|
|
108
|
-
.action(async (options) => {
|
|
120
|
+
.command("whoami")
|
|
121
|
+
.description("Show current user info")
|
|
122
|
+
.action(async () => {
|
|
109
123
|
try {
|
|
110
|
-
const response = await apiRequest("/
|
|
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
|
|
128
|
+
console.error(`Error: ${error.error || "Failed to get user info"}`);
|
|
115
129
|
process.exit(1);
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
console.log(`Page
|
|
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
|
|
140
|
+
console.error("Error: Failed to get user info");
|
|
130
141
|
process.exit(1);
|
|
131
142
|
}
|
|
132
143
|
});
|
|
133
144
|
|
|
134
|
-
//
|
|
145
|
+
// Download command
|
|
135
146
|
program
|
|
136
|
-
.command("
|
|
137
|
-
.description("
|
|
138
|
-
.action(async (
|
|
147
|
+
.command("download")
|
|
148
|
+
.description("Download your page as markdown to current directory")
|
|
149
|
+
.action(async () => {
|
|
139
150
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
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 (!
|
|
160
|
-
console.error(
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
172
|
+
console.error(`Error: ${error.error || "Failed to fetch page"}`);
|
|
173
173
|
process.exit(1);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
|
|
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
|
|
186
|
+
console.error("Error: Failed to download page");
|
|
179
187
|
process.exit(1);
|
|
180
188
|
}
|
|
181
189
|
});
|
|
182
190
|
|
|
183
|
-
//
|
|
191
|
+
// Upload command
|
|
184
192
|
program
|
|
185
|
-
.command("
|
|
186
|
-
.description("
|
|
187
|
-
.
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
if (!
|
|
192
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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("--
|
|
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.
|
|
299
|
+
if (!options.apiUrl && !options.reset) {
|
|
300
|
+
const customUrl = config.get("apiUrl");
|
|
264
301
|
console.log("\nConfiguration:");
|
|
265
|
-
console.log(
|
|
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
|
|