weplan 0.2.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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/index.js +487 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Albert Ilagan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# weplan
|
|
2
|
+
|
|
3
|
+
Deploy a self-contained HTML file and get back a shareable link — in one command, in under a second. Built for CLI and agent workflows.
|
|
4
|
+
|
|
5
|
+
```console
|
|
6
|
+
$ npx weplan create report.html
|
|
7
|
+
Deployed uf6vxjhmbz4f (12.3 kB, public)
|
|
8
|
+
|
|
9
|
+
https://plan.wecodeph.dev/p/uf6vxjhmbz4f
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
No account needed to try it: anonymous uploads are public and expire after 12 hours. Sign in (`weplan login`, Google via WorkOS) to keep pages, name them, and make them private.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
weplan create <file.html> [--name <name>] [--private] [--allow <emails>]
|
|
18
|
+
weplan update <slug> <file.html> # replace content, URL stays stable
|
|
19
|
+
weplan access <slug> [--public|--private] [--allow <emails>] [--revoke <emails>]
|
|
20
|
+
weplan list
|
|
21
|
+
weplan delete <slug>
|
|
22
|
+
weplan login | logout | whoami
|
|
23
|
+
weplan status
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `--json` on any command for machine-readable output; errors go to stderr as `{"error": "..."}` with exit code 1.
|
|
27
|
+
- Pass `-` as the file to read HTML from stdin.
|
|
28
|
+
- Private pages are viewable by you and allow-listed emails (they sign in in the browser); scripts can fetch them with `Authorization: Bearer <api key>`.
|
|
29
|
+
|
|
30
|
+
## Limits (free)
|
|
31
|
+
|
|
32
|
+
2 MB per page · 100 pages · 25 MB total · 200 uploads/day. Anonymous: 1 MB, 12-hour lifetime, 5 uploads/minute.
|
|
33
|
+
|
|
34
|
+
## For agents
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+
To share an HTML file as a link, run `npx weplan create <file.html> --json`
|
|
38
|
+
and give the user the `url` from the output. Add `--private --allow <emails>`
|
|
39
|
+
for sensitive content. To revise a shared page without changing its link:
|
|
40
|
+
`npx weplan update <slug> <file.html> --json`.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Zero runtime dependencies. Source and service: <https://github.com/albertilagan/weplan>.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
5
|
+
import { extname } from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
|
|
8
|
+
// ../../packages/core/src/index.ts
|
|
9
|
+
var LIMITS = {
|
|
10
|
+
anonymous: {
|
|
11
|
+
maxPageBytes: 1 * 1024 * 1024,
|
|
12
|
+
ttlSeconds: 12 * 60 * 60,
|
|
13
|
+
createsPerMinute: 5
|
|
14
|
+
},
|
|
15
|
+
free: {
|
|
16
|
+
maxPageBytes: 2 * 1024 * 1024,
|
|
17
|
+
maxPages: 100,
|
|
18
|
+
maxTotalBytes: 25 * 1024 * 1024,
|
|
19
|
+
writesPerDay: 200
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
class WeplanError extends Error {
|
|
23
|
+
status;
|
|
24
|
+
constructor(message, status) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.status = status;
|
|
27
|
+
this.name = "WeplanError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class WeplanClient {
|
|
32
|
+
baseUrl;
|
|
33
|
+
token;
|
|
34
|
+
fetchFn;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
37
|
+
this.token = options.token;
|
|
38
|
+
this.fetchFn = options.fetch ?? fetch;
|
|
39
|
+
}
|
|
40
|
+
async create(html, options = {}) {
|
|
41
|
+
const params = new URLSearchParams;
|
|
42
|
+
if (options.name)
|
|
43
|
+
params.set("name", options.name);
|
|
44
|
+
if (options.visibility)
|
|
45
|
+
params.set("visibility", options.visibility);
|
|
46
|
+
if (options.allow?.length)
|
|
47
|
+
params.set("allow", options.allow.join(","));
|
|
48
|
+
const query = params.size ? `?${params}` : "";
|
|
49
|
+
return this.request("POST", `/api/pages${query}`, { html });
|
|
50
|
+
}
|
|
51
|
+
async update(slug, html) {
|
|
52
|
+
return this.request("PUT", `/api/pages/${encodeURIComponent(slug)}`, { html });
|
|
53
|
+
}
|
|
54
|
+
async get(slug) {
|
|
55
|
+
return this.request("GET", `/api/pages/${encodeURIComponent(slug)}`);
|
|
56
|
+
}
|
|
57
|
+
async setAccess(slug, update) {
|
|
58
|
+
return this.request("PATCH", `/api/pages/${encodeURIComponent(slug)}`, {
|
|
59
|
+
json: update
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async list() {
|
|
63
|
+
const body = await this.request("GET", "/api/pages");
|
|
64
|
+
return body.pages;
|
|
65
|
+
}
|
|
66
|
+
async delete(slug) {
|
|
67
|
+
await this.request("DELETE", `/api/pages/${encodeURIComponent(slug)}`);
|
|
68
|
+
}
|
|
69
|
+
async health() {
|
|
70
|
+
return this.request("GET", "/health");
|
|
71
|
+
}
|
|
72
|
+
async authConfig() {
|
|
73
|
+
return this.request("GET", "/auth/config");
|
|
74
|
+
}
|
|
75
|
+
async exchange(accessToken) {
|
|
76
|
+
return this.request("POST", "/auth/exchange", {
|
|
77
|
+
json: { accessToken }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async exchangeCode(code) {
|
|
81
|
+
return this.request("POST", "/auth/exchange", { json: { code } });
|
|
82
|
+
}
|
|
83
|
+
async me() {
|
|
84
|
+
return this.request("GET", "/api/me");
|
|
85
|
+
}
|
|
86
|
+
async revokeKey() {
|
|
87
|
+
await this.request("DELETE", "/api/keys/current");
|
|
88
|
+
}
|
|
89
|
+
async request(method, path, body) {
|
|
90
|
+
const headers = { accept: "application/json" };
|
|
91
|
+
if (this.token)
|
|
92
|
+
headers["authorization"] = `Bearer ${this.token}`;
|
|
93
|
+
let payload;
|
|
94
|
+
if (body?.html !== undefined) {
|
|
95
|
+
headers["content-type"] = "text/html; charset=utf-8";
|
|
96
|
+
payload = body.html;
|
|
97
|
+
} else if (body?.json !== undefined) {
|
|
98
|
+
headers["content-type"] = "application/json";
|
|
99
|
+
payload = JSON.stringify(body.json);
|
|
100
|
+
}
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await this.fetchFn(`${this.baseUrl}${path}`, { method, headers, body: payload });
|
|
104
|
+
} catch (cause) {
|
|
105
|
+
throw new WeplanError(`could not reach ${this.baseUrl}: ${cause.message}`, 0);
|
|
106
|
+
}
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
let message = `${response.status} ${response.statusText}`;
|
|
109
|
+
try {
|
|
110
|
+
const parsed = await response.json();
|
|
111
|
+
if (parsed.error)
|
|
112
|
+
message = parsed.error;
|
|
113
|
+
} catch {}
|
|
114
|
+
throw new WeplanError(message, response.status);
|
|
115
|
+
}
|
|
116
|
+
return await response.json();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/config.ts
|
|
121
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
122
|
+
import { homedir } from "node:os";
|
|
123
|
+
import { dirname, join } from "node:path";
|
|
124
|
+
function configPath() {
|
|
125
|
+
const base = process.env["XDG_CONFIG_HOME"] || join(homedir(), ".config");
|
|
126
|
+
return join(base, "weplan", "config.json");
|
|
127
|
+
}
|
|
128
|
+
function readConfig() {
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(readFileSync(configPath(), "utf8"));
|
|
131
|
+
} catch {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function writeConfig(config) {
|
|
136
|
+
const path = configPath();
|
|
137
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
138
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
139
|
+
`, { mode: 384 });
|
|
140
|
+
}
|
|
141
|
+
function clearConfig() {
|
|
142
|
+
const path = configPath();
|
|
143
|
+
if (existsSync(path))
|
|
144
|
+
unlinkSync(path);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/login.ts
|
|
148
|
+
import { randomUUID } from "node:crypto";
|
|
149
|
+
import { createInterface } from "node:readline";
|
|
150
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
151
|
+
var describe = (err) => err.message ?? err.error_description ?? err.error;
|
|
152
|
+
async function postForm(url, fields) {
|
|
153
|
+
const response = await fetch(url, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
156
|
+
body: new URLSearchParams(fields)
|
|
157
|
+
});
|
|
158
|
+
return { ok: response.ok, body: await response.json() };
|
|
159
|
+
}
|
|
160
|
+
async function login(client, log, options = {}) {
|
|
161
|
+
const config = await client.authConfig();
|
|
162
|
+
if (!options.paste) {
|
|
163
|
+
const started = await startDeviceFlow(config);
|
|
164
|
+
if (started)
|
|
165
|
+
return deviceLogin(client, config, started, log);
|
|
166
|
+
log(" Device sign-in isn't available for this service; using the paste-a-code flow instead.");
|
|
167
|
+
}
|
|
168
|
+
return pasteLogin(client, config, log);
|
|
169
|
+
}
|
|
170
|
+
async function startDeviceFlow(config) {
|
|
171
|
+
try {
|
|
172
|
+
const start = await postForm(config.deviceAuthorizationUrl, {
|
|
173
|
+
client_id: config.clientId
|
|
174
|
+
});
|
|
175
|
+
return start.ok && "device_code" in start.body ? start.body : null;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function deviceLogin(client, config, device, log) {
|
|
181
|
+
log("");
|
|
182
|
+
log(` Open ${device.verification_uri_complete}`);
|
|
183
|
+
log(` and confirm the code ${device.user_code}`);
|
|
184
|
+
log("");
|
|
185
|
+
log(" Waiting for sign-in…");
|
|
186
|
+
let intervalMs = (device.interval || 5) * 1000;
|
|
187
|
+
const deadline = Date.now() + device.expires_in * 1000;
|
|
188
|
+
while (Date.now() < deadline) {
|
|
189
|
+
await sleep(intervalMs);
|
|
190
|
+
const poll = await postForm(config.tokenUrl, {
|
|
191
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
192
|
+
device_code: device.device_code,
|
|
193
|
+
client_id: config.clientId
|
|
194
|
+
});
|
|
195
|
+
if (poll.ok && "access_token" in poll.body) {
|
|
196
|
+
const exchange = await client.exchange(poll.body.access_token);
|
|
197
|
+
return { apiKey: exchange.apiKey, email: exchange.user.email };
|
|
198
|
+
}
|
|
199
|
+
const err = poll.body;
|
|
200
|
+
switch (err.error) {
|
|
201
|
+
case "authorization_pending":
|
|
202
|
+
continue;
|
|
203
|
+
case "slow_down":
|
|
204
|
+
intervalMs += 1000;
|
|
205
|
+
continue;
|
|
206
|
+
case "access_denied":
|
|
207
|
+
throw new Error("sign-in was denied");
|
|
208
|
+
case "expired_token":
|
|
209
|
+
throw new Error("sign-in code expired; run `weplan login` again");
|
|
210
|
+
default:
|
|
211
|
+
throw new Error(`sign-in failed: ${describe(err)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw new Error("sign-in timed out; run `weplan login` again");
|
|
215
|
+
}
|
|
216
|
+
async function pasteLogin(client, config, log) {
|
|
217
|
+
const state = randomUUID();
|
|
218
|
+
const authorize = new URL(config.authorizeUrl);
|
|
219
|
+
authorize.searchParams.set("response_type", "code");
|
|
220
|
+
authorize.searchParams.set("client_id", config.clientId);
|
|
221
|
+
authorize.searchParams.set("redirect_uri", config.cliCallbackUrl);
|
|
222
|
+
authorize.searchParams.set("provider", "authkit");
|
|
223
|
+
authorize.searchParams.set("state", state);
|
|
224
|
+
log("");
|
|
225
|
+
log(` Open ${authorize}`);
|
|
226
|
+
log(" then paste the code the page shows you.");
|
|
227
|
+
log("");
|
|
228
|
+
const pasted = (await prompt(" Code: ")).trim();
|
|
229
|
+
const separator = pasted.indexOf(".");
|
|
230
|
+
if (separator === -1 || pasted.slice(0, separator) !== state) {
|
|
231
|
+
throw new Error("invalid or expired sign-in code — start sign-in again and paste the full code");
|
|
232
|
+
}
|
|
233
|
+
const exchange = await client.exchangeCode(pasted.slice(separator + 1));
|
|
234
|
+
return { apiKey: exchange.apiKey, email: exchange.user.email };
|
|
235
|
+
}
|
|
236
|
+
function prompt(question) {
|
|
237
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
rl.question(question, (answer) => {
|
|
240
|
+
rl.close();
|
|
241
|
+
resolve(answer);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/index.ts
|
|
247
|
+
var DEFAULT_URL = "https://plan.wecodeph.dev";
|
|
248
|
+
var VERSION = "0.2.0";
|
|
249
|
+
var HELP = `weplan — deploy a self-contained HTML file, get a shareable link
|
|
250
|
+
|
|
251
|
+
Usage
|
|
252
|
+
weplan create <file.html> [--name <name>] [--private] [--allow <emails>]
|
|
253
|
+
weplan update <slug> <file.html> Replace a page's content, same URL
|
|
254
|
+
weplan access <slug> [--public|--private] [--allow <emails>] [--revoke <emails>]
|
|
255
|
+
weplan list List your pages
|
|
256
|
+
weplan delete <slug> Delete a page
|
|
257
|
+
weplan login [--paste] | logout | whoami Sign in with your work account (WorkOS)
|
|
258
|
+
weplan status Check the service is reachable
|
|
259
|
+
|
|
260
|
+
Options
|
|
261
|
+
--name <name> Human-readable slug (3-63 chars, a-z 0-9 hyphen); requires login
|
|
262
|
+
--private Only you and allow-listed emails can view; requires login
|
|
263
|
+
--allow <emails> Comma-separated emails to grant view access (implies --private)
|
|
264
|
+
--revoke <emails> Comma-separated emails to remove from the allow list
|
|
265
|
+
--public Make the page viewable by anyone with the link
|
|
266
|
+
--paste Login: use the paste-a-code flow instead of the device code flow
|
|
267
|
+
--json Machine-readable JSON output (for agents)
|
|
268
|
+
--url <url> Service URL (default: $WEPLAN_URL, then saved config${DEFAULT_URL ? `, then ${DEFAULT_URL}` : ""})
|
|
269
|
+
--token <token> API key (default: $WEPLAN_TOKEN, then saved config from \`weplan login\`)
|
|
270
|
+
-h, --help Show this help
|
|
271
|
+
-v, --version Show version
|
|
272
|
+
|
|
273
|
+
Without login, uploads are public and expire after 12 hours. Pass "-" as the file to read stdin.
|
|
274
|
+
`;
|
|
275
|
+
function fail(message, asJson) {
|
|
276
|
+
process.stderr.write(asJson ? JSON.stringify({ error: message }) + `
|
|
277
|
+
` : `error: ${message}
|
|
278
|
+
`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
function readHtml(file, asJson) {
|
|
282
|
+
if (file === "-")
|
|
283
|
+
return readFileSync2(0, "utf8");
|
|
284
|
+
const ext = extname(file).toLowerCase();
|
|
285
|
+
if (ext !== ".html" && ext !== ".htm") {
|
|
286
|
+
fail(`expected an .html file, got "${file}" (weplan serves everything as text/html)`, asJson);
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
return readFileSync2(file, "utf8");
|
|
290
|
+
} catch (cause) {
|
|
291
|
+
fail(`could not read ${file}: ${cause.message}`, asJson);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function formatSize(bytes) {
|
|
295
|
+
if (bytes < 1024)
|
|
296
|
+
return `${bytes} B`;
|
|
297
|
+
if (bytes < 1024 * 1024)
|
|
298
|
+
return `${(bytes / 1024).toFixed(1)} kB`;
|
|
299
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
300
|
+
}
|
|
301
|
+
function describeAccess(page) {
|
|
302
|
+
if (page.visibility === "public")
|
|
303
|
+
return "public";
|
|
304
|
+
const count = page.allow?.length ?? 0;
|
|
305
|
+
return `private (you${count ? ` + ${count} allowed` : ""})`;
|
|
306
|
+
}
|
|
307
|
+
function printPage(page, verb, asJson) {
|
|
308
|
+
if (asJson) {
|
|
309
|
+
process.stdout.write(JSON.stringify(page) + `
|
|
310
|
+
`);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const lines = [`${verb} ${page.slug} (${formatSize(page.size)}, ${describeAccess(page)})`, "", ` ${page.url}`];
|
|
314
|
+
if (page.allow?.length)
|
|
315
|
+
lines.push("", ` allowed: ${page.allow.join(", ")}`);
|
|
316
|
+
if (page.expiresAt) {
|
|
317
|
+
lines.push("", ` anonymous upload — expires ${page.expiresAt}. Run \`weplan login\` to keep pages.`);
|
|
318
|
+
}
|
|
319
|
+
process.stdout.write(lines.join(`
|
|
320
|
+
`) + `
|
|
321
|
+
`);
|
|
322
|
+
}
|
|
323
|
+
function splitEmails(value) {
|
|
324
|
+
if (!value)
|
|
325
|
+
return;
|
|
326
|
+
return value.split(",").map((e) => e.trim()).filter(Boolean);
|
|
327
|
+
}
|
|
328
|
+
async function main() {
|
|
329
|
+
const { values: flags, positionals } = parseArgs({
|
|
330
|
+
args: process.argv.slice(2),
|
|
331
|
+
allowPositionals: true,
|
|
332
|
+
options: {
|
|
333
|
+
name: { type: "string" },
|
|
334
|
+
private: { type: "boolean", default: false },
|
|
335
|
+
public: { type: "boolean", default: false },
|
|
336
|
+
allow: { type: "string" },
|
|
337
|
+
revoke: { type: "string" },
|
|
338
|
+
paste: { type: "boolean", default: false },
|
|
339
|
+
json: { type: "boolean", default: false },
|
|
340
|
+
url: { type: "string" },
|
|
341
|
+
token: { type: "string" },
|
|
342
|
+
help: { type: "boolean", short: "h", default: false },
|
|
343
|
+
version: { type: "boolean", short: "v", default: false }
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
const asJson = flags.json;
|
|
347
|
+
if (flags.version) {
|
|
348
|
+
process.stdout.write(VERSION + `
|
|
349
|
+
`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const command = positionals[0];
|
|
353
|
+
if (flags.help || !command) {
|
|
354
|
+
process.stdout.write(HELP);
|
|
355
|
+
if (!command && !flags.help)
|
|
356
|
+
process.exit(1);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (flags.private && flags.public)
|
|
360
|
+
fail("--private and --public are mutually exclusive", asJson);
|
|
361
|
+
const saved = readConfig();
|
|
362
|
+
const baseUrl = flags.url ?? process.env["WEPLAN_URL"] ?? saved.url ?? DEFAULT_URL;
|
|
363
|
+
if (!baseUrl)
|
|
364
|
+
fail("no service URL configured: pass --url or set WEPLAN_URL", asJson);
|
|
365
|
+
const token = flags.token ?? process.env["WEPLAN_TOKEN"] ?? saved.apiKey;
|
|
366
|
+
const client = new WeplanClient({ baseUrl, token });
|
|
367
|
+
switch (command) {
|
|
368
|
+
case "create": {
|
|
369
|
+
const file = positionals[1];
|
|
370
|
+
if (!file)
|
|
371
|
+
fail("usage: weplan create <file.html> [--name <name>] [--private] [--allow <emails>]", asJson);
|
|
372
|
+
const allow = splitEmails(flags.allow);
|
|
373
|
+
const page = await client.create(readHtml(file, asJson), {
|
|
374
|
+
name: flags.name,
|
|
375
|
+
visibility: flags.private || allow?.length ? "private" : undefined,
|
|
376
|
+
allow
|
|
377
|
+
});
|
|
378
|
+
printPage(page, "Deployed", asJson);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
case "update": {
|
|
382
|
+
const [, slug, file] = positionals;
|
|
383
|
+
if (!slug || !file)
|
|
384
|
+
fail("usage: weplan update <slug> <file.html>", asJson);
|
|
385
|
+
printPage(await client.update(slug, readHtml(file, asJson)), "Updated", asJson);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
case "access": {
|
|
389
|
+
const slug = positionals[1];
|
|
390
|
+
if (!slug)
|
|
391
|
+
fail("usage: weplan access <slug> [--public|--private] [--allow <emails>] [--revoke <emails>]", asJson);
|
|
392
|
+
const update = {
|
|
393
|
+
visibility: flags.private ? "private" : flags.public ? "public" : undefined,
|
|
394
|
+
allow: splitEmails(flags.allow),
|
|
395
|
+
revoke: splitEmails(flags.revoke)
|
|
396
|
+
};
|
|
397
|
+
const changed = update.visibility || update.allow || update.revoke;
|
|
398
|
+
const page = changed ? await client.setAccess(slug, update) : await client.get(slug);
|
|
399
|
+
printPage(page, changed ? "Updated access for" : "Page", asJson);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
case "list": {
|
|
403
|
+
const pages = await client.list();
|
|
404
|
+
if (asJson) {
|
|
405
|
+
process.stdout.write(JSON.stringify({ pages }) + `
|
|
406
|
+
`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (pages.length === 0) {
|
|
410
|
+
process.stdout.write(`no pages yet
|
|
411
|
+
`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
for (const page of pages) {
|
|
415
|
+
process.stdout.write(`${page.slug.padEnd(24)} ${formatSize(page.size).padStart(9)} ${page.visibility.padEnd(7)} ${page.updatedAt} ${page.url}
|
|
416
|
+
`);
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
case "delete": {
|
|
421
|
+
const slug = positionals[1];
|
|
422
|
+
if (!slug)
|
|
423
|
+
fail("usage: weplan delete <slug>", asJson);
|
|
424
|
+
await client.delete(slug);
|
|
425
|
+
process.stdout.write(asJson ? JSON.stringify({ deleted: slug }) + `
|
|
426
|
+
` : `Deleted ${slug}
|
|
427
|
+
`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
case "login": {
|
|
431
|
+
const result = await login(client, (line) => process.stderr.write(line + `
|
|
432
|
+
`), { paste: flags.paste });
|
|
433
|
+
writeConfig({ url: baseUrl, apiKey: result.apiKey, email: result.email });
|
|
434
|
+
process.stdout.write(asJson ? JSON.stringify({ email: result.email, config: configPath() }) + `
|
|
435
|
+
` : `
|
|
436
|
+
Signed in as ${result.email}. Credentials saved to ${configPath()}
|
|
437
|
+
`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
case "logout": {
|
|
441
|
+
if (saved.apiKey) {
|
|
442
|
+
try {
|
|
443
|
+
await new WeplanClient({ baseUrl, token: saved.apiKey }).revokeKey();
|
|
444
|
+
} catch {}
|
|
445
|
+
}
|
|
446
|
+
clearConfig();
|
|
447
|
+
process.stdout.write(asJson ? JSON.stringify({ loggedOut: true }) + `
|
|
448
|
+
` : `Signed out.
|
|
449
|
+
`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
case "whoami": {
|
|
453
|
+
if (!token)
|
|
454
|
+
fail("not signed in; run `weplan login`", asJson);
|
|
455
|
+
const me = await client.me();
|
|
456
|
+
if (asJson) {
|
|
457
|
+
process.stdout.write(JSON.stringify(me) + `
|
|
458
|
+
`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const { usage } = me;
|
|
462
|
+
process.stdout.write(`${me.email || me.id}
|
|
463
|
+
pages ${usage.pages}/${usage.limits.maxPages}
|
|
464
|
+
storage ${formatSize(usage.bytes)} / ${formatSize(usage.limits.maxTotalBytes)}
|
|
465
|
+
uploads ${usage.writesToday}/${usage.limits.writesPerDay} today
|
|
466
|
+
`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
case "status": {
|
|
470
|
+
const health = await client.health();
|
|
471
|
+
process.stdout.write(asJson ? JSON.stringify(health) + `
|
|
472
|
+
` : `ok — ${baseUrl}
|
|
473
|
+
login: ${health.loginEnabled ? "enabled" : "not configured"}
|
|
474
|
+
anonymous uploads: ${health.anonymousUploads ? "enabled (12h expiry)" : "disabled"}
|
|
475
|
+
`);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
default:
|
|
479
|
+
fail(`unknown command "${command}" — run weplan --help`, asJson);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
main().catch((cause) => {
|
|
483
|
+
const asJson = process.argv.includes("--json");
|
|
484
|
+
if (cause instanceof WeplanError)
|
|
485
|
+
fail(cause.message, asJson);
|
|
486
|
+
fail(cause.message ?? String(cause), asJson);
|
|
487
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "weplan",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Deploy a self-contained HTML file and get back a shareable link. Built for CLI and agent workflows.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"weplan": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cloudflare",
|
|
19
|
+
"deploy",
|
|
20
|
+
"html",
|
|
21
|
+
"share",
|
|
22
|
+
"agent",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "bun build src/index.ts --target=node --outdir=dist --banner '#!/usr/bin/env node'",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"dev": "bun run src/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "26.4.0",
|
|
32
|
+
"@weplan/core": "workspace:*",
|
|
33
|
+
"typescript": "7.0.2"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://plan.wecodeph.dev",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/albertilagan/weplan.git",
|
|
39
|
+
"directory": "apps/cli"
|
|
40
|
+
},
|
|
41
|
+
"bugs": "https://github.com/albertilagan/weplan/issues"
|
|
42
|
+
}
|