pwahatch-cli 1.0.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.
Files changed (3) hide show
  1. package/README.md +95 -0
  2. package/bin/pwahatch.mjs +312 -0
  3. package/package.json +19 -0
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # pwahatch
2
+
3
+ Submit PWAs to [Hatch](https://pwahatch.vercel.app) from your terminal. Zero dependencies.
4
+
5
+ ## Quick start
6
+
7
+ ```sh
8
+ npx pwahatch login
9
+ npx pwahatch submit https://your-pwa.com
10
+ ```
11
+
12
+ The CLI crawls your site's web app manifest, shows a preview, then walks you through category and visibility before submitting.
13
+
14
+ ```
15
+ Crawling https://monieplan.xyz...
16
+
17
+ ✓ Found manifest
18
+ Name: MoniePlan
19
+ Description: Your personalized budget in minutes.
20
+ Icons: 6 found
21
+ Theme: #009957
22
+
23
+ Categories:
24
+ 1. business
25
+ 2. education
26
+ ...
27
+ 12. productivity
28
+
29
+ Pick a number (enter to skip): 12
30
+ Public? (Y/n): Y
31
+
32
+ Submitting...
33
+
34
+ ✓ Submitted! Install page: https://pwahatch.vercel.app/monieplan-a3kx9f
35
+ ```
36
+
37
+ ## Commands
38
+
39
+ ### `pwahatch login`
40
+
41
+ Authenticate with your Hatch account. Prompts for email and password, then stores the session token locally.
42
+
43
+ ```sh
44
+ pwahatch login # defaults to production
45
+ pwahatch login --url http://localhost:3000 # point at a local dev server
46
+ ```
47
+
48
+ ### `pwahatch submit <url>`
49
+
50
+ Submit a PWA. The URL must point to a site with a valid web app manifest.
51
+
52
+ ```sh
53
+ pwahatch submit https://your-pwa.com
54
+ ```
55
+
56
+ Skip the interactive prompts with flags:
57
+
58
+ ```sh
59
+ pwahatch submit https://your-pwa.com --category productivity --private
60
+ ```
61
+
62
+ | Flag | Description |
63
+ |---|---|
64
+ | `--category <name>` | Set the category (see list below) |
65
+ | `--private` | List the app as private |
66
+ | `--public` | List the app as public (default) |
67
+
68
+ ### `pwahatch logout`
69
+
70
+ Clear the stored session token.
71
+
72
+ ```sh
73
+ pwahatch logout
74
+ ```
75
+
76
+ ## Categories
77
+
78
+ `business` `education` `entertainment` `finance` `food & drink` `games` `health & fitness` `lifestyle` `music` `news` `photo & video` `productivity` `shopping` `social` `sports` `travel` `utilities` `weather`
79
+
80
+ Pass any of these to `--category`. The value is case-insensitive.
81
+
82
+ ## Configuration
83
+
84
+ Session data is stored at `~/.pwahatch/config.json` with `0600` permissions. The file contains your session token and the base URL of the Hatch server you logged into.
85
+
86
+ To switch servers, log in again with a different `--url`. To reset everything, run `pwahatch logout`.
87
+
88
+ ## Requirements
89
+
90
+ - Node.js 18.15 or later
91
+ - A [Hatch](https://pwahatch.vercel.app) account
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ const CONFIG_DIR = join(homedir(), ".pwahatch");
8
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
9
+ const DEFAULT_URL = "http://localhost:3000";
10
+
11
+ const CATEGORIES = [
12
+ "business",
13
+ "education",
14
+ "entertainment",
15
+ "finance",
16
+ "food & drink",
17
+ "games",
18
+ "health & fitness",
19
+ "lifestyle",
20
+ "music",
21
+ "news",
22
+ "photo & video",
23
+ "productivity",
24
+ "shopping",
25
+ "social",
26
+ "sports",
27
+ "travel",
28
+ "utilities",
29
+ "weather",
30
+ ];
31
+
32
+ // ── Config ──────────────────────────────────────────────
33
+
34
+ function loadConfig() {
35
+ try {
36
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+
42
+ function saveConfig(config) {
43
+ mkdirSync(CONFIG_DIR, { recursive: true });
44
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
45
+ mode: 0o600,
46
+ });
47
+ }
48
+
49
+ // ── Prompts ─────────────────────────────────────────────
50
+
51
+ function ask(question) {
52
+ return new Promise((resolve) => {
53
+ process.stdout.write(question);
54
+ process.stdin.setEncoding("utf-8");
55
+ process.stdin.resume();
56
+ process.stdin.once("data", (data) => {
57
+ process.stdin.pause();
58
+ resolve(data.toString().trim());
59
+ });
60
+ });
61
+ }
62
+
63
+ function askPassword(question) {
64
+ return new Promise((resolve) => {
65
+ process.stdout.write(question);
66
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
67
+ process.stdin.resume();
68
+ process.stdin.setEncoding("utf-8");
69
+
70
+ let input = "";
71
+ const onData = (ch) => {
72
+ if (ch === "\n" || ch === "\r") {
73
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
74
+ process.stdin.pause();
75
+ process.stdin.removeListener("data", onData);
76
+ process.stdout.write("\n");
77
+ resolve(input);
78
+ } else if (ch === "\u0003") {
79
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
80
+ process.exit();
81
+ } else if (ch === "\u007f" || ch === "\b") {
82
+ input = input.slice(0, -1);
83
+ } else {
84
+ input += ch;
85
+ }
86
+ };
87
+ process.stdin.on("data", onData);
88
+ });
89
+ }
90
+
91
+ // ── Manifest preview ────────────────────────────────────
92
+
93
+ async function crawlManifest(siteUrl) {
94
+ const url = siteUrl.startsWith("http") ? siteUrl : `https://${siteUrl}`;
95
+
96
+ const res = await fetch(url, {
97
+ headers: { "User-Agent": "pwahatch-cli/1.0" },
98
+ });
99
+ if (!res.ok) throw new Error(`Failed to fetch ${url} (${res.status})`);
100
+
101
+ const html = await res.text();
102
+ const match = html.match(
103
+ /<link[^>]*rel=["']manifest["'][^>]*href=["']([^"']+)["']|<link[^>]*href=["']([^"']+)["'][^>]*rel=["']manifest["']/i
104
+ );
105
+ if (!match) throw new Error("No web app manifest found on that page");
106
+
107
+ const manifestUrl = new URL(match[1] || match[2], url).href;
108
+ const mRes = await fetch(manifestUrl, {
109
+ headers: { "User-Agent": "pwahatch-cli/1.0" },
110
+ });
111
+ if (!mRes.ok)
112
+ throw new Error(`Failed to fetch manifest (${mRes.status})`);
113
+
114
+ const manifest = await mRes.json();
115
+
116
+ return {
117
+ name: manifest.name || manifest.short_name || "Untitled",
118
+ description: manifest.description || null,
119
+ icons: Array.isArray(manifest.icons) ? manifest.icons.length : 0,
120
+ themeColor: manifest.theme_color || null,
121
+ };
122
+ }
123
+
124
+ // ── Commands ────────────────────────────────────────────
125
+
126
+ async function login(args) {
127
+ let baseUrl = DEFAULT_URL;
128
+ const urlIdx = args.indexOf("--url");
129
+ if (urlIdx !== -1 && args[urlIdx + 1]) {
130
+ baseUrl = args[urlIdx + 1].replace(/\/$/, "");
131
+ }
132
+
133
+ console.log();
134
+ const email = await ask(" Email: ");
135
+ const password = await askPassword(" Password: ");
136
+
137
+ if (!email || !password) {
138
+ console.error("\n \x1b[31m✗\x1b[0m Email and password required");
139
+ process.exit(1);
140
+ }
141
+
142
+ const res = await fetch(`${baseUrl}/api/auth/sign-in/email`, {
143
+ method: "POST",
144
+ headers: { "Content-Type": "application/json" },
145
+ body: JSON.stringify({ email, password }),
146
+ redirect: "manual",
147
+ });
148
+
149
+ if (!res.ok && res.status !== 302) {
150
+ const data = await res.json().catch(() => ({}));
151
+ console.error(
152
+ `\n \x1b[31m✗\x1b[0m ${data.message || "Invalid credentials"}`
153
+ );
154
+ process.exit(1);
155
+ }
156
+
157
+ // Extract session token from Set-Cookie
158
+ const cookies = res.headers.getSetCookie?.() || [];
159
+ let token = null;
160
+ for (const c of cookies) {
161
+ const m = c.match(
162
+ /(?:better-auth\.session_token|session_token)=([^;]+)/
163
+ );
164
+ if (m) {
165
+ token = m[1];
166
+ break;
167
+ }
168
+ }
169
+
170
+ // Fallback: check response body
171
+ if (!token) {
172
+ try {
173
+ const text = await res.text();
174
+ const data = JSON.parse(text);
175
+ token = data.session?.token || data.token;
176
+ } catch {}
177
+ }
178
+
179
+ if (!token) {
180
+ console.error("\n \x1b[31m✗\x1b[0m Could not extract session token");
181
+ process.exit(1);
182
+ }
183
+
184
+ saveConfig({ token, baseUrl });
185
+ console.log(`\n \x1b[32m✓\x1b[0m Logged in as ${email}\n`);
186
+ }
187
+
188
+ async function submit(args) {
189
+ const config = loadConfig();
190
+ if (!config.token) {
191
+ console.error("\n \x1b[31m✗\x1b[0m Not logged in. Run: pwahatch login\n");
192
+ process.exit(1);
193
+ }
194
+
195
+ // Parse args
196
+ let url = null;
197
+ let category = null;
198
+ let isPublic = true;
199
+ let hasVisFlag = false;
200
+
201
+ for (let i = 0; i < args.length; i++) {
202
+ if (args[i] === "--category" && args[i + 1]) {
203
+ category = args[++i].toLowerCase();
204
+ } else if (args[i] === "--private") {
205
+ isPublic = false;
206
+ hasVisFlag = true;
207
+ } else if (args[i] === "--public") {
208
+ isPublic = true;
209
+ hasVisFlag = true;
210
+ } else if (!args[i].startsWith("--")) {
211
+ url = args[i];
212
+ }
213
+ }
214
+
215
+ if (!url) {
216
+ console.error("\n \x1b[31m✗\x1b[0m Usage: pwahatch submit <url>\n");
217
+ process.exit(1);
218
+ }
219
+
220
+ // Preview
221
+ console.log(`\n Crawling ${url}...`);
222
+ let manifest;
223
+ try {
224
+ manifest = await crawlManifest(url);
225
+ } catch (e) {
226
+ console.error(`\n \x1b[31m✗\x1b[0m ${e.message}\n`);
227
+ process.exit(1);
228
+ }
229
+
230
+ console.log(`\n \x1b[32m✓\x1b[0m Found manifest`);
231
+ console.log(` Name: ${manifest.name}`);
232
+ if (manifest.description)
233
+ console.log(` Description: ${manifest.description}`);
234
+ console.log(` Icons: ${manifest.icons} found`);
235
+ if (manifest.themeColor)
236
+ console.log(` Theme: ${manifest.themeColor}`);
237
+ console.log();
238
+
239
+ // Prompt for category
240
+ if (!category) {
241
+ console.log(" Categories:");
242
+ CATEGORIES.forEach((c, i) =>
243
+ console.log(` ${String(i + 1).padStart(2)}. ${c}`)
244
+ );
245
+ console.log();
246
+ const choice = await ask(" Pick a number (enter to skip): ");
247
+ const idx = parseInt(choice, 10);
248
+ if (idx >= 1 && idx <= CATEGORIES.length) {
249
+ category = CATEGORIES[idx - 1];
250
+ }
251
+ }
252
+
253
+ // Prompt for visibility
254
+ if (!hasVisFlag) {
255
+ const vis = await ask(" Public? (Y/n): ");
256
+ isPublic = vis.toLowerCase() !== "n";
257
+ }
258
+
259
+ // Submit
260
+ console.log("\n Submitting...");
261
+ const baseUrl = config.baseUrl || DEFAULT_URL;
262
+ const res = await fetch(`${baseUrl}/api/apps`, {
263
+ method: "POST",
264
+ headers: {
265
+ "Content-Type": "application/json",
266
+ Cookie: `better-auth.session_token=${config.token}`,
267
+ },
268
+ body: JSON.stringify({ url, category, isPublic }),
269
+ });
270
+
271
+ const data = await res.json();
272
+
273
+ if (!res.ok) {
274
+ console.error(`\n \x1b[31m✗\x1b[0m ${data.error || "Submission failed"}\n`);
275
+ process.exit(1);
276
+ }
277
+
278
+ console.log(
279
+ `\n \x1b[32m✓\x1b[0m Submitted! Install page: ${baseUrl}/${data.app.slug}\n`
280
+ );
281
+ }
282
+
283
+ async function logout() {
284
+ try {
285
+ rmSync(CONFIG_FILE);
286
+ } catch {}
287
+ console.log("\n \x1b[32m✓\x1b[0m Logged out\n");
288
+ }
289
+
290
+ // ── Main ────────────────────────────────────────────────
291
+
292
+ const [, , command, ...args] = process.argv;
293
+ const commands = { login, submit, logout };
294
+
295
+ if (!command || !commands[command]) {
296
+ console.log(`
297
+ \x1b[1mpwahatch\x1b[0m — Submit PWAs from your terminal
298
+
299
+ \x1b[2mUsage:\x1b[0m
300
+ pwahatch login [--url <base>] Log in with email/password
301
+ pwahatch submit <url> Submit a PWA
302
+ --category <name> Set category (skip prompt)
303
+ --private List as private
304
+ pwahatch logout Clear stored session
305
+ `);
306
+ process.exit(command ? 1 : 0);
307
+ }
308
+
309
+ commands[command](args).catch((err) => {
310
+ console.error(`\n \x1b[31m✗\x1b[0m ${err.message}\n`);
311
+ process.exit(1);
312
+ });
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "pwahatch-cli",
3
+ "version": "1.0.0",
4
+ "description": "Submit PWAs to Hatch from your terminal",
5
+ "type": "module",
6
+ "repository": {
7
+ "url": "https://github.com/codellyson/pwahatch-cli.git"
8
+ },
9
+ "bin": {
10
+ "pwahatch": "./bin/pwahatch.mjs"
11
+ },
12
+ "engines": {
13
+ "node": ">=18.15.0"
14
+ },
15
+ "files": [
16
+ "bin/"
17
+ ],
18
+ "license": "MIT"
19
+ }