hostedge 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +47 -2
  2. package/dist/hostedge.js +3215 -0
  3. package/package.json +46 -3
@@ -0,0 +1,3215 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/main.ts
4
+ import { spawn } from "node:child_process";
5
+
6
+ // ../../packages/core/src/plans.ts
7
+ var MB = 1024 * 1024;
8
+ var GB = 1024 * MB;
9
+ var PLANS = {
10
+ free: {
11
+ id: "free",
12
+ name: "Free",
13
+ priceMonthlyUsd: 0,
14
+ priceYearlyUsd: 0,
15
+ limits: {
16
+ projects: 3,
17
+ customDomains: 1,
18
+ requestsPerMonth: 1e5,
19
+ bandwidthBytesPerMonth: 10 * GB,
20
+ members: 1,
21
+ cpuMs: 10,
22
+ subrequests: 50,
23
+ maxDeploymentBytes: 50 * MB,
24
+ maxFiles: 2e3,
25
+ runtimeLogsPerDay: 2e4,
26
+ analyticsEventsPerMonth: 2e4,
27
+ analyticsRetentionDays: 30,
28
+ storageResourcesPerProject: 3,
29
+ storageBytes: 1 * GB,
30
+ kvOperationsPerMonth: 1e6,
31
+ cronJobsPerProject: 2,
32
+ cronMinIntervalMinutes: 60,
33
+ formSubmissionsPerMonth: 100,
34
+ formRetentionDays: 90,
35
+ monitorsPerProject: 2,
36
+ monitorMinIntervalSeconds: 300
37
+ }
38
+ },
39
+ starter: {
40
+ id: "starter",
41
+ name: "Starter",
42
+ priceMonthlyUsd: 9,
43
+ priceYearlyUsd: 90,
44
+ limits: {
45
+ projects: 10,
46
+ customDomains: 5,
47
+ requestsPerMonth: 1e6,
48
+ bandwidthBytesPerMonth: 100 * GB,
49
+ members: 3,
50
+ cpuMs: 30,
51
+ subrequests: 100,
52
+ maxDeploymentBytes: 250 * MB,
53
+ maxFiles: 2e4,
54
+ runtimeLogsPerDay: 2e5,
55
+ analyticsEventsPerMonth: 2e5,
56
+ analyticsRetentionDays: 365,
57
+ storageResourcesPerProject: 10,
58
+ storageBytes: 10 * GB,
59
+ kvOperationsPerMonth: 1e7,
60
+ cronJobsPerProject: 10,
61
+ cronMinIntervalMinutes: 5,
62
+ formSubmissionsPerMonth: 2e3,
63
+ formRetentionDays: 365,
64
+ monitorsPerProject: 10,
65
+ monitorMinIntervalSeconds: 60
66
+ }
67
+ },
68
+ pro: {
69
+ id: "pro",
70
+ name: "Pro",
71
+ priceMonthlyUsd: 29,
72
+ priceYearlyUsd: 290,
73
+ limits: {
74
+ projects: 50,
75
+ customDomains: 25,
76
+ requestsPerMonth: 1e7,
77
+ bandwidthBytesPerMonth: 500 * GB,
78
+ members: 10,
79
+ cpuMs: 50,
80
+ subrequests: 500,
81
+ maxDeploymentBytes: 250 * MB,
82
+ maxFiles: 2e4,
83
+ runtimeLogsPerDay: 2e6,
84
+ analyticsEventsPerMonth: 2e6,
85
+ analyticsRetentionDays: 730,
86
+ storageResourcesPerProject: 25,
87
+ storageBytes: 100 * GB,
88
+ kvOperationsPerMonth: 5e7,
89
+ cronJobsPerProject: 50,
90
+ cronMinIntervalMinutes: 1,
91
+ formSubmissionsPerMonth: 2e4,
92
+ formRetentionDays: 730,
93
+ monitorsPerProject: 50,
94
+ monitorMinIntervalSeconds: 60
95
+ }
96
+ },
97
+ agency: {
98
+ id: "agency",
99
+ name: "Agency",
100
+ priceMonthlyUsd: 79,
101
+ priceYearlyUsd: 790,
102
+ limits: {
103
+ projects: null,
104
+ customDomains: 100,
105
+ requestsPerMonth: 5e7,
106
+ bandwidthBytesPerMonth: 2e3 * GB,
107
+ members: 25,
108
+ cpuMs: 50,
109
+ subrequests: 1e3,
110
+ maxDeploymentBytes: 250 * MB,
111
+ maxFiles: 2e4,
112
+ runtimeLogsPerDay: 1e7,
113
+ analyticsEventsPerMonth: 1e7,
114
+ analyticsRetentionDays: 1095,
115
+ storageResourcesPerProject: 50,
116
+ storageBytes: 500 * GB,
117
+ kvOperationsPerMonth: 2e8,
118
+ cronJobsPerProject: 100,
119
+ cronMinIntervalMinutes: 1,
120
+ formSubmissionsPerMonth: 1e5,
121
+ formRetentionDays: 1095,
122
+ monitorsPerProject: 100,
123
+ monitorMinIntervalSeconds: 60
124
+ }
125
+ }
126
+ };
127
+ var PLAN_ORDER = ["free", "starter", "pro", "agency"];
128
+ var MAX_FILE_BYTES = 25 * MB;
129
+
130
+ // ../../packages/core/src/projects/upload.ts
131
+ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
132
+ "html",
133
+ "htm",
134
+ "css",
135
+ "js",
136
+ "mjs",
137
+ "cjs",
138
+ "jsx",
139
+ "ts",
140
+ "tsx",
141
+ "json",
142
+ "map",
143
+ "webmanifest",
144
+ "txt",
145
+ "md",
146
+ "mdx",
147
+ "xml",
148
+ "csv",
149
+ "svg",
150
+ "png",
151
+ "jpg",
152
+ "jpeg",
153
+ "gif",
154
+ "webp",
155
+ "avif",
156
+ "ico",
157
+ "bmp",
158
+ "woff",
159
+ "woff2",
160
+ "ttf",
161
+ "otf",
162
+ "eot",
163
+ "wasm",
164
+ "pdf",
165
+ "mp4",
166
+ "webm",
167
+ "ogg",
168
+ "mp3",
169
+ "wav",
170
+ "m4a",
171
+ "vtt",
172
+ "astro",
173
+ "vue",
174
+ "svelte",
175
+ // Template languages that static site generators build from (Eleventy and friends).
176
+ "njk",
177
+ "liquid",
178
+ "hbs",
179
+ "handlebars",
180
+ "mustache",
181
+ "ejs",
182
+ "pug",
183
+ "webc",
184
+ "scss",
185
+ "sass",
186
+ "less",
187
+ "yaml",
188
+ "yml",
189
+ "toml",
190
+ "lock",
191
+ "gitignore",
192
+ "npmrc",
193
+ "nvmrc",
194
+ "editorconfig",
195
+ "prettierrc",
196
+ "eslintrc",
197
+ "browserslistrc",
198
+ "gltf",
199
+ "glb",
200
+ "zip"
201
+ ]);
202
+ var ALLOWED_BARE_NAMES = /* @__PURE__ */ new Set([
203
+ "license",
204
+ "readme",
205
+ "cname",
206
+ "_headers",
207
+ "_redirects",
208
+ "dockerfile",
209
+ "procfile",
210
+ "makefile",
211
+ "notice",
212
+ "authors",
213
+ "changelog"
214
+ ]);
215
+ var BLOCKED_SEGMENTS = /* @__PURE__ */ new Set([
216
+ ".git",
217
+ ".svn",
218
+ ".hg",
219
+ "node_modules",
220
+ ".DS_Store",
221
+ "__MACOSX",
222
+ ".wrangler",
223
+ ".vercel",
224
+ ".netlify",
225
+ // Assistant instruction folders (`hostedge init --ai`): not part of the site.
226
+ ".cursor",
227
+ ".claude"
228
+ ]);
229
+ var SECRET_NAMES = /^(\.env(\..*)?|.*\.pem|.*\.key|id_rsa.*|\.npmrc\.local|\.dev\.vars.*)$/i;
230
+ var HASH_PATTERN = /^[0-9a-f]{32}$/;
231
+ function normalisePath(raw) {
232
+ if (typeof raw !== "string" || raw.length === 0) return { ok: false, reason: "Empty file name." };
233
+ if (raw.length > 512) return { ok: false, reason: "Path is longer than 512 characters." };
234
+ if (/[\u0000-\u001f\u007f]/.test(raw))
235
+ return { ok: false, reason: "Path contains control characters." };
236
+ const path = raw.replace(/\\/g, "/").replace(/^\.\//, "");
237
+ if (path.startsWith("/") || /^[a-zA-Z]:/.test(path))
238
+ return { ok: false, reason: "Absolute paths are not allowed." };
239
+ const segments = path.split("/");
240
+ if (segments.some((s) => s === "..")) return { ok: false, reason: "Paths may not contain \u201C..\u201D." };
241
+ if (segments.some((s) => s === "" || s === "."))
242
+ return { ok: false, reason: "Path has an empty or \u201C.\u201D segment." };
243
+ return { ok: true, path };
244
+ }
245
+ function isIgnoredPath(path) {
246
+ return path.split("/").some((segment) => BLOCKED_SEGMENTS.has(segment));
247
+ }
248
+ function fileTypeProblem(path) {
249
+ const name = path.slice(path.lastIndexOf("/") + 1);
250
+ if (SECRET_NAMES.test(name))
251
+ return "Looks like a secret (env file or key). Remove it \u2014 set secrets as environment variables instead.";
252
+ const dot = name.lastIndexOf(".");
253
+ const ext = dot > 0 ? name.slice(dot + 1).toLowerCase() : name.startsWith(".") ? name.slice(1).toLowerCase() : "";
254
+ if (!ext || dot <= 0) {
255
+ if (name.startsWith(".") && ALLOWED_EXTENSIONS.has(ext)) return null;
256
+ return ALLOWED_BARE_NAMES.has(name.toLowerCase()) ? null : `\u201C${name}\u201D has no file extension HostEdge can serve.`;
257
+ }
258
+ return ALLOWED_EXTENSIONS.has(ext) ? null : `.${ext} files can\u2019t be deployed.`;
259
+ }
260
+ function checkManifest(entries, limits) {
261
+ const problems = [];
262
+ const ignored = [];
263
+ const files = [];
264
+ const seen = /* @__PURE__ */ new Set();
265
+ if (!Array.isArray(entries))
266
+ return {
267
+ files,
268
+ ignored,
269
+ problems: [{ path: "", reason: "Manifest must be a list of files." }],
270
+ totalBytes: 0
271
+ };
272
+ for (const raw of entries) {
273
+ const entry = raw;
274
+ const rawPath = typeof entry?.path === "string" ? entry.path : "";
275
+ const normal = normalisePath(rawPath);
276
+ if (!normal.ok) {
277
+ problems.push({ path: rawPath, reason: normal.reason });
278
+ continue;
279
+ }
280
+ const { path } = normal;
281
+ if (isIgnoredPath(path)) {
282
+ ignored.push(path);
283
+ continue;
284
+ }
285
+ const lower = path.toLowerCase();
286
+ if (seen.has(lower)) {
287
+ problems.push({ path, reason: "Two files share this path (names differ only in case)." });
288
+ continue;
289
+ }
290
+ seen.add(lower);
291
+ const size = entry?.size;
292
+ const hash = entry?.hash;
293
+ if (typeof size !== "number" || !Number.isInteger(size) || size < 0) {
294
+ problems.push({ path, reason: "Invalid file size." });
295
+ continue;
296
+ }
297
+ if (size > MAX_FILE_BYTES) {
298
+ problems.push({
299
+ path,
300
+ reason: `File is ${(size / 1024 / 1024).toFixed(1)} MB; the limit per file is 25 MB.`
301
+ });
302
+ continue;
303
+ }
304
+ if (typeof hash !== "string" || !HASH_PATTERN.test(hash)) {
305
+ problems.push({ path, reason: "Invalid content hash." });
306
+ continue;
307
+ }
308
+ const typeProblem = fileTypeProblem(path);
309
+ if (typeProblem) {
310
+ problems.push({ path, reason: typeProblem });
311
+ continue;
312
+ }
313
+ files.push({ path, size, hash });
314
+ }
315
+ const totalBytes = files.reduce((sum, f) => sum + f.size, 0);
316
+ if (files.length === 0 && problems.length === 0)
317
+ problems.push({ path: "", reason: "No files to deploy." });
318
+ if (files.length > limits.maxFiles)
319
+ problems.push({
320
+ path: "",
321
+ reason: `This deploy has ${files.length.toLocaleString("en-US")} files; your plan allows ${limits.maxFiles.toLocaleString("en-US")}.`
322
+ });
323
+ if (totalBytes > limits.maxDeploymentBytes) {
324
+ problems.push({
325
+ path: "",
326
+ reason: `This deploy is ${(totalBytes / 1024 / 1024).toFixed(1)} MB; your plan allows ${Math.round(limits.maxDeploymentBytes / 1024 / 1024)} MB.`
327
+ });
328
+ }
329
+ return { files, ignored, problems, totalBytes };
330
+ }
331
+
332
+ // ../../packages/core/src/projects/frameworks.ts
333
+ var FRAMEWORKS = [
334
+ "static",
335
+ "astro",
336
+ "next",
337
+ "nuxt",
338
+ "sveltekit",
339
+ "react-router",
340
+ "next-server",
341
+ "astro-server",
342
+ "sveltekit-server",
343
+ "nuxt-server",
344
+ "react-router-server",
345
+ "vite-react",
346
+ "vite-vue",
347
+ "vite-svelte",
348
+ "vite",
349
+ "angular",
350
+ "create-react-app",
351
+ "gatsby",
352
+ "docusaurus",
353
+ "vitepress",
354
+ "eleventy",
355
+ "hono",
356
+ "other"
357
+ ];
358
+ var NEXT_CACHE_OUTPUT = "_hostedge/next-cache";
359
+ var OPEN_NEXT_DEFAULT_CONFIG = `// Written by HostEdge because this project had no open-next.config.ts.
360
+ // Commit your own to change it: https://opennext.js.org/cloudflare/caching
361
+ import { defineCloudflareConfig, getCloudflareContext } from '@opennextjs/cloudflare'
362
+ import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache'
363
+ import d1NextTagCache from '@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache'
364
+
365
+ const pending = new Set<string>()
366
+
367
+ /** Stale-while-revalidate: answer now, regenerate the page after the response. */
368
+ const queue = {
369
+ name: 'hostedge-queue',
370
+ async send(message: {
371
+ MessageBody: { host: string; url: string }
372
+ MessageDeduplicationId: string
373
+ }) {
374
+ const { host, url } = message.MessageBody
375
+ const id = message.MessageDeduplicationId
376
+ if (pending.has(id)) return
377
+ pending.add(id)
378
+ const protocol = host.includes('localhost') ? 'http' : 'https'
379
+ const work = fetch(\`\${protocol}://\${host}\${url}\`, {
380
+ method: 'HEAD',
381
+ headers: {
382
+ 'x-prerender-revalidate': process.env.NEXT_PREVIEW_MODE_ID ?? '',
383
+ 'x-isr': '1',
384
+ },
385
+ signal: AbortSignal.timeout(10_000),
386
+ })
387
+ .then((response) => response.body?.cancel())
388
+ .catch(() => undefined)
389
+ .finally(() => pending.delete(id))
390
+ getCloudflareContext().ctx.waitUntil(work)
391
+ },
392
+ }
393
+
394
+ export default defineCloudflareConfig({
395
+ incrementalCache: r2IncrementalCache,
396
+ tagCache: d1NextTagCache,
397
+ queue: () => Promise.resolve(queue),
398
+ })
399
+ `;
400
+ var OPEN_NEXT_DEFAULT_WRANGLER = `// Written by HostEdge because this project had no wrangler config.
401
+ {
402
+ "name": "hostedge-app",
403
+ "main": ".open-next/worker.js",
404
+ "compatibility_date": "{{compatibilityDate}}",
405
+ "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
406
+ "assets": { "directory": ".open-next/assets", "binding": "ASSETS" },
407
+ "r2_buckets": [{ "binding": "NEXT_INC_CACHE_R2_BUCKET", "bucket_name": "hostedge-next-cache" }],
408
+ "d1_databases": [
409
+ {
410
+ "binding": "NEXT_TAG_CACHE_D1",
411
+ "database_name": "hostedge-next-tags",
412
+ "database_id": "00000000-0000-0000-0000-000000000000"
413
+ }
414
+ ]
415
+ }
416
+ `;
417
+ var npm = (outputDirectory, buildCommand = "npm run build") => ({
418
+ installCommand: "npm install",
419
+ buildCommand,
420
+ outputDirectory
421
+ });
422
+ var FRAMEWORK_INFO = {
423
+ static: {
424
+ id: "static",
425
+ name: "Static HTML",
426
+ description: "No build step. Your files are deployed as they are.",
427
+ group: "static",
428
+ defaults: { installCommand: null, buildCommand: null, outputDirectory: null },
429
+ notFound: "404-page"
430
+ },
431
+ astro: {
432
+ id: "astro",
433
+ name: "Astro",
434
+ description: "Static build to dist/. Your 404 page answers unknown paths.",
435
+ group: "static",
436
+ defaults: npm("dist"),
437
+ notFound: "404-page",
438
+ outputHint: "Astro writes dist/ for static output. With @astrojs/cloudflare, pick Astro (server)."
439
+ },
440
+ next: {
441
+ id: "next",
442
+ name: "Next.js (static export)",
443
+ description: 'Static export to out/. Needs output: "export" in next.config.',
444
+ group: "app",
445
+ defaults: npm("out"),
446
+ notFound: "404-page",
447
+ outputHint: 'Next.js writes out/ only when next.config sets output: "export". For server features, pick Next.js (full, OpenNext).'
448
+ },
449
+ nuxt: {
450
+ id: "nuxt",
451
+ name: "Nuxt (static)",
452
+ description: "Prerendered with nuxi generate to .output/public.",
453
+ group: "app",
454
+ defaults: npm(".output/public", "npx nuxi generate"),
455
+ notFound: "404-page",
456
+ outputHint: "Nuxt writes .output/public with `nuxi generate`. For a server, pick Nuxt (server)."
457
+ },
458
+ sveltekit: {
459
+ id: "sveltekit",
460
+ name: "SvelteKit (static)",
461
+ description: "adapter-static build to build/.",
462
+ group: "app",
463
+ defaults: npm("build"),
464
+ notFound: "404-page",
465
+ outputHint: "SvelteKit writes build/ with @sveltejs/adapter-static. With adapter-cloudflare, pick SvelteKit (server)."
466
+ },
467
+ "react-router": {
468
+ id: "react-router",
469
+ name: "React Router (SPA mode)",
470
+ description: "ssr: false build to build/client, with client-side routing.",
471
+ group: "app",
472
+ defaults: npm("build/client"),
473
+ notFound: "single-page-application",
474
+ outputHint: "React Router writes build/client when react-router.config sets ssr: false."
475
+ },
476
+ "next-server": {
477
+ id: "next-server",
478
+ name: "Next.js (full, OpenNext)",
479
+ description: "Server rendering, API routes, server actions and middleware, through @opennextjs/cloudflare.",
480
+ group: "server",
481
+ defaults: npm(".open-next/assets", "npx opennextjs-cloudflare build"),
482
+ notFound: "none",
483
+ server: {
484
+ entry: ".open-next/worker.js",
485
+ packages: ["@opennextjs/cloudflare@^1", "wrangler@^4"],
486
+ files: [
487
+ {
488
+ path: "open-next.config.ts",
489
+ content: OPEN_NEXT_DEFAULT_CONFIG,
490
+ unless: ["open-next.config.ts"]
491
+ },
492
+ {
493
+ path: "wrangler.jsonc",
494
+ content: OPEN_NEXT_DEFAULT_WRANGLER,
495
+ unless: ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
496
+ }
497
+ ],
498
+ extraOutput: { from: ".open-next/cache", to: NEXT_CACHE_OUTPUT }
499
+ },
500
+ outputHint: "OpenNext writes .open-next/. HostEdge adds @opennextjs/cloudflare and its config when they are missing."
501
+ },
502
+ "astro-server": {
503
+ id: "astro-server",
504
+ name: "Astro (server)",
505
+ description: "On-demand rendering and endpoints with the @astrojs/cloudflare adapter.",
506
+ group: "server",
507
+ defaults: npm("dist/client"),
508
+ notFound: "none",
509
+ server: { entry: null },
510
+ outputHint: "Astro with @astrojs/cloudflare writes dist/client and dist/server."
511
+ },
512
+ "sveltekit-server": {
513
+ id: "sveltekit-server",
514
+ name: "SvelteKit (server)",
515
+ description: "Server load functions and form actions with @sveltejs/adapter-cloudflare.",
516
+ group: "server",
517
+ defaults: npm(".svelte-kit/cloudflare"),
518
+ notFound: "none",
519
+ server: { entry: ".svelte-kit/cloudflare/_worker.js" },
520
+ outputHint: "SvelteKit with @sveltejs/adapter-cloudflare writes .svelte-kit/cloudflare."
521
+ },
522
+ "nuxt-server": {
523
+ id: "nuxt-server",
524
+ name: "Nuxt (server)",
525
+ description: "Server routes and SSR with the Nitro cloudflare_module preset.",
526
+ group: "server",
527
+ defaults: npm(".output/public", "npx nuxi build --preset=cloudflare_module"),
528
+ notFound: "none",
529
+ server: { entry: ".output/server/index.mjs" },
530
+ outputHint: "Nuxt writes .output/public and .output/server with the cloudflare_module preset."
531
+ },
532
+ "react-router-server": {
533
+ id: "react-router-server",
534
+ name: "React Router (server)",
535
+ description: "Loaders, actions and SSR with @cloudflare/vite-plugin.",
536
+ group: "server",
537
+ defaults: npm("build/client"),
538
+ notFound: "none",
539
+ server: { entry: null },
540
+ outputHint: "React Router with @cloudflare/vite-plugin writes build/client and build/server."
541
+ },
542
+ "vite-react": {
543
+ id: "vite-react",
544
+ name: "Vite + React",
545
+ description: "Single-page app in dist/. Unknown paths serve index.html.",
546
+ group: "app",
547
+ defaults: npm("dist"),
548
+ notFound: "single-page-application"
549
+ },
550
+ "vite-vue": {
551
+ id: "vite-vue",
552
+ name: "Vite + Vue",
553
+ description: "Single-page app in dist/. Unknown paths serve index.html.",
554
+ group: "app",
555
+ defaults: npm("dist"),
556
+ notFound: "single-page-application"
557
+ },
558
+ "vite-svelte": {
559
+ id: "vite-svelte",
560
+ name: "Vite + Svelte",
561
+ description: "Single-page app in dist/. Unknown paths serve index.html.",
562
+ group: "app",
563
+ defaults: npm("dist"),
564
+ notFound: "single-page-application"
565
+ },
566
+ vite: {
567
+ id: "vite",
568
+ name: "Vite",
569
+ description: "Solid, Preact, Lit or plain Vite in dist/, with client-side routing.",
570
+ group: "app",
571
+ defaults: npm("dist"),
572
+ notFound: "single-page-application"
573
+ },
574
+ angular: {
575
+ id: "angular",
576
+ name: "Angular",
577
+ description: "Browser build in dist/<project>/browser, with client-side routing.",
578
+ group: "app",
579
+ defaults: npm("dist/app/browser"),
580
+ notFound: "single-page-application",
581
+ outputHint: "Angular writes dist/<project name>/browser. Set the output directory to match angular.json."
582
+ },
583
+ "create-react-app": {
584
+ id: "create-react-app",
585
+ name: "Create React App",
586
+ description: "Single-page app in build/. Unknown paths serve index.html.",
587
+ group: "app",
588
+ defaults: npm("build"),
589
+ notFound: "single-page-application"
590
+ },
591
+ gatsby: {
592
+ id: "gatsby",
593
+ name: "Gatsby",
594
+ description: "Static build to public/.",
595
+ group: "static",
596
+ defaults: npm("public"),
597
+ notFound: "404-page"
598
+ },
599
+ docusaurus: {
600
+ id: "docusaurus",
601
+ name: "Docusaurus",
602
+ description: "Documentation site built to build/.",
603
+ group: "docs",
604
+ defaults: npm("build"),
605
+ notFound: "404-page"
606
+ },
607
+ vitepress: {
608
+ id: "vitepress",
609
+ name: "VitePress",
610
+ description: "Documentation site built to .vitepress/dist.",
611
+ group: "docs",
612
+ defaults: npm("docs/.vitepress/dist", "npm run docs:build"),
613
+ notFound: "404-page",
614
+ outputHint: "VitePress writes <docs folder>/.vitepress/dist."
615
+ },
616
+ eleventy: {
617
+ id: "eleventy",
618
+ name: "Eleventy",
619
+ description: "Static build to _site/.",
620
+ group: "static",
621
+ defaults: npm("_site", "npx @11ty/eleventy"),
622
+ notFound: "404-page"
623
+ },
624
+ hono: {
625
+ id: "hono",
626
+ name: "Hono",
627
+ description: "A Worker (dist/_worker.js) with static files served alongside.",
628
+ group: "worker",
629
+ defaults: npm("dist"),
630
+ notFound: "none",
631
+ outputHint: "The build must write dist/_worker.js (a single bundled file)."
632
+ },
633
+ other: {
634
+ id: "other",
635
+ name: "Other",
636
+ description: "Any build that writes static files, or a _worker.js, to a folder.",
637
+ group: "static",
638
+ defaults: npm("dist"),
639
+ notFound: "404-page"
640
+ }
641
+ };
642
+ var LOCKFILES = [
643
+ ["pnpm-lock.yaml", "pnpm"],
644
+ ["yarn.lock", "yarn"],
645
+ ["package-lock.json", "npm"],
646
+ ["npm-shrinkwrap.json", "npm"]
647
+ ];
648
+ function installCommandFor(pm, locked, yarnBerry) {
649
+ if (pm === "pnpm") return locked ? "pnpm install --frozen-lockfile" : "pnpm install";
650
+ if (pm === "yarn")
651
+ return locked ? yarnBerry ? "yarn install --immutable" : "yarn install --frozen-lockfile" : "yarn install";
652
+ return locked ? "npm ci" : "npm install";
653
+ }
654
+ function withPackageManager(defaults, pm, install) {
655
+ const run2 = (cmd) => {
656
+ if (!cmd || pm === "npm") return cmd;
657
+ const match = /^npm run (\S+)(.*)$/.exec(cmd);
658
+ if (match)
659
+ return pm === "yarn" ? `yarn ${match[1]}${match[2]}` : `pnpm run ${match[1]}${match[2]}`;
660
+ if (cmd.startsWith("npx "))
661
+ return pm === "yarn" ? `yarn dlx ${cmd.slice(4)}` : `pnpm exec ${cmd.slice(4)}`;
662
+ return cmd;
663
+ };
664
+ return {
665
+ installCommand: defaults.installCommand === null ? null : install ?? installCommandFor(pm, false, false),
666
+ buildCommand: run2(defaults.buildCommand),
667
+ outputDirectory: defaults.outputDirectory
668
+ };
669
+ }
670
+ function parsePackageJson(text) {
671
+ if (!text) return null;
672
+ try {
673
+ const parsed = JSON.parse(text);
674
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
675
+ } catch {
676
+ return null;
677
+ }
678
+ }
679
+ function scoped(paths, root) {
680
+ if (!root) return paths;
681
+ const prefix = `${root.replace(/\/+$/, "")}/`;
682
+ return paths.filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length));
683
+ }
684
+ var has = (...names) => (deps) => names.some((n) => n in deps);
685
+ var RULES = [
686
+ {
687
+ framework: "next-server",
688
+ when: (d) => "next" in d && "@opennextjs/cloudflare" in d,
689
+ found: "next and @opennextjs/cloudflare"
690
+ },
691
+ {
692
+ framework: "astro-server",
693
+ when: (d) => "astro" in d && "@astrojs/cloudflare" in d,
694
+ found: "astro and @astrojs/cloudflare"
695
+ },
696
+ {
697
+ framework: "sveltekit-server",
698
+ when: (d) => "@sveltejs/kit" in d && "@sveltejs/adapter-cloudflare" in d,
699
+ found: "@sveltejs/kit and @sveltejs/adapter-cloudflare"
700
+ },
701
+ {
702
+ framework: "nuxt-server",
703
+ when: (d) => ("nuxt" in d || "nuxt3" in d) && ("nitro-cloudflare-dev" in d || "wrangler" in d),
704
+ found: "nuxt with Cloudflare tooling"
705
+ },
706
+ {
707
+ framework: "react-router-server",
708
+ when: (d) => "@react-router/dev" in d && "@cloudflare/vite-plugin" in d,
709
+ found: "@react-router/dev and @cloudflare/vite-plugin"
710
+ },
711
+ {
712
+ // HostEdge adds OpenNext during the build, so plain Next.js apps get the full runtime.
713
+ framework: "next-server",
714
+ when: has("next"),
715
+ found: "next",
716
+ problem: () => 'HostEdge adds @opennextjs/cloudflare during the build. If next.config sets output: "export", pick Next.js (static export) instead.'
717
+ },
718
+ {
719
+ framework: "nuxt",
720
+ when: has("nuxt", "nuxt3"),
721
+ found: "nuxt",
722
+ tune: (pkg) => pkg.scripts?.generate ? { buildCommand: "npm run generate" } : {}
723
+ },
724
+ {
725
+ framework: "sveltekit",
726
+ when: has("@sveltejs/kit"),
727
+ found: "@sveltejs/kit",
728
+ problem: (deps) => "@sveltejs/adapter-static" in deps ? null : "Use @sveltejs/adapter-static for a static site, or @sveltejs/adapter-cloudflare and pick SvelteKit (server)."
729
+ },
730
+ {
731
+ framework: "react-router",
732
+ when: has("@react-router/dev"),
733
+ found: "@react-router/dev",
734
+ problem: () => "Deployed in SPA mode: set ssr: false in react-router.config. For server rendering, add @cloudflare/vite-plugin and pick React Router (server)."
735
+ },
736
+ { framework: "astro", when: has("astro"), found: "astro" },
737
+ { framework: "gatsby", when: has("gatsby"), found: "gatsby" },
738
+ { framework: "docusaurus", when: has("@docusaurus/core"), found: "@docusaurus/core" },
739
+ {
740
+ framework: "vitepress",
741
+ when: has("vitepress"),
742
+ found: "vitepress",
743
+ tune: (pkg) => {
744
+ const entry = Object.entries(pkg.scripts ?? {}).find(
745
+ ([, cmd]) => /\bvitepress build\b/.test(cmd)
746
+ );
747
+ if (!entry) return {};
748
+ const dir = /\bvitepress build\s+([^\s-][^\s]*)/.exec(entry[1])?.[1]?.replace(/^\.\/|\/$/g, "");
749
+ return {
750
+ buildCommand: `npm run ${entry[0]}`,
751
+ outputDirectory: dir && dir !== "." ? `${dir}/.vitepress/dist` : ".vitepress/dist"
752
+ };
753
+ }
754
+ },
755
+ {
756
+ framework: "eleventy",
757
+ when: has("@11ty/eleventy"),
758
+ found: "@11ty/eleventy",
759
+ tune: (pkg) => pkg.scripts?.build ? { buildCommand: "npm run build" } : {}
760
+ },
761
+ {
762
+ framework: "angular",
763
+ when: has("@angular/core"),
764
+ found: "@angular/core",
765
+ tune: (pkg) => {
766
+ const name = pkg.name?.replace(/^@[^/]+\//, "");
767
+ return name && /^[\w.-]+$/.test(name) ? { outputDirectory: `dist/${name}/browser` } : {};
768
+ }
769
+ },
770
+ { framework: "create-react-app", when: has("react-scripts"), found: "react-scripts" },
771
+ { framework: "hono", when: has("hono"), found: "hono" },
772
+ {
773
+ framework: "vite-react",
774
+ when: (d) => "vite" in d && ("react" in d || "react-dom" in d),
775
+ found: "vite and react"
776
+ },
777
+ { framework: "vite-vue", when: (d) => "vite" in d && "vue" in d, found: "vite and vue" },
778
+ { framework: "vite-svelte", when: (d) => "vite" in d && "svelte" in d, found: "vite and svelte" },
779
+ { framework: "vite", when: has("vite"), found: "vite" }
780
+ ];
781
+ function packageManagerFrom(paths, allPaths, pkg) {
782
+ const lock = LOCKFILES.find(([file]) => paths.includes(file) || allPaths.includes(file));
783
+ const declared = /^(npm|pnpm|yarn)@(\d+)/.exec(pkg?.packageManager ?? "");
784
+ const pm = lock?.[1] ?? declared?.[1] ?? "npm";
785
+ const berry = pm === "yarn" && (paths.includes(".yarnrc.yml") || allPaths.includes(".yarnrc.yml") || Number(declared?.[2] ?? 1) >= 2);
786
+ const bun = ["bun.lockb", "bun.lock"].some((f) => paths.includes(f) || allPaths.includes(f));
787
+ return {
788
+ pm,
789
+ locked: Boolean(lock),
790
+ berry,
791
+ note: bun && !lock ? "Found a Bun lockfile; builds use npm, so bun-only APIs won\u2019t run." : null
792
+ };
793
+ }
794
+ function detectFramework(allPaths, packageJsonText, rootDirectory = "") {
795
+ const normalised = allPaths.map((p) => p.replace(/^\/+/, ""));
796
+ const paths = scoped(normalised, rootDirectory);
797
+ const pkg = parsePackageJson(packageJsonText);
798
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
799
+ const manager = packageManagerFrom(paths, normalised, pkg);
800
+ const install = installCommandFor(manager.pm, manager.locked, manager.berry);
801
+ const settingsFor = (framework, overrides = {}) => ({
802
+ framework,
803
+ rootDirectory,
804
+ ...withPackageManager(
805
+ { ...FRAMEWORK_INFO[framework].defaults, ...overrides },
806
+ manager.pm,
807
+ install
808
+ )
809
+ });
810
+ const managerNote = [
811
+ manager.locked ? `Installs with ${manager.pm} from the lockfile.` : null,
812
+ manager.note
813
+ ].filter(Boolean).join(" ");
814
+ const withNote = (reason) => managerNote ? `${reason} ${managerNote}` : reason;
815
+ if (pkg) {
816
+ const rule = RULES.find((r) => r.when(deps, pkg));
817
+ if (rule)
818
+ return {
819
+ settings: settingsFor(rule.framework, rule.tune?.(pkg, deps)),
820
+ reason: withNote(`Found ${rule.found} in package.json.`),
821
+ problem: rule.problem?.(deps) ?? null
822
+ };
823
+ if (pkg.scripts?.build)
824
+ return {
825
+ settings: settingsFor("other"),
826
+ reason: withNote("Found a build script in package.json."),
827
+ problem: null
828
+ };
829
+ if (paths.includes("index.html"))
830
+ return {
831
+ settings: settingsFor("static"),
832
+ reason: "Found index.html and no build script.",
833
+ problem: null
834
+ };
835
+ return {
836
+ settings: settingsFor("other"),
837
+ reason: "Found package.json without a build script.",
838
+ problem: 'Add a "build" script to package.json, or deploy the folder that contains index.html.'
839
+ };
840
+ }
841
+ if (paths.includes("index.html"))
842
+ return { settings: settingsFor("static"), reason: "Found index.html.", problem: null };
843
+ const nested = paths.find((p) => /^[^/]+\/index\.html$/.test(p));
844
+ if (nested) {
845
+ const dir = nested.split("/")[0] ?? "";
846
+ return {
847
+ settings: settingsFor("static", { outputDirectory: dir }),
848
+ reason: `Found ${nested}.`,
849
+ problem: null
850
+ };
851
+ }
852
+ const anyHtml = paths.some((p) => p.endsWith(".html"));
853
+ return {
854
+ settings: settingsFor("static"),
855
+ reason: anyHtml ? "Found HTML files but no index.html." : "No HTML files found.",
856
+ problem: anyHtml ? "Add an index.html so the site has a home page." : "This folder has no index.html. Drop the folder that contains your site\u2019s index.html."
857
+ };
858
+ }
859
+
860
+ // ../../packages/core/src/ai/starter.ts
861
+ var HOSTEDGE_MCP_URL = "https://hostedge.app/api/mcp";
862
+ var CONNECT_WITHOUT_TOKEN = [
863
+ "I have not connected you to HostEdge yet. Before deploying, ask me to do this once:",
864
+ '1. In HostEdge open Settings \u2192 API tokens \u2192 AI assistants and press "Create a token for AI assistants" (shown once).',
865
+ `2. Add the MCP server ${HOSTEDGE_MCP_URL} to your assistant with an Authorization: Bearer header holding that token (Claude Code: claude mcp add --transport http hostedge ${HOSTEDGE_MCP_URL} --header ...; Cursor and others: mcp.json). Most assistants can also connect with just the URL and a sign-in page.`,
866
+ "3. Never paste the token into this chat, a project file or a commit. Keep it in the assistant's MCP settings or in HOSTEDGE_TOKEN."
867
+ ];
868
+
869
+ // ../../packages/core/src/emails/marketing.ts
870
+ var SITE_URL = "https://hostedge.app";
871
+ var LOGO_URL = `${SITE_URL}/brand/hostedge-mark-email.png`;
872
+
873
+ // ../../packages/core/src/ai/kit.ts
874
+ var AI_KIT_DEFAULT_URL = "https://hostedge.app";
875
+ var CLI_EXIT_CODES = [
876
+ { code: 0, name: "ok", meaning: "Success." },
877
+ { code: 1, name: "failure", meaning: "Generic failure (network error, bad input, unexpected)." },
878
+ { code: 2, name: "usage", meaning: "Usage error: unknown flag or command, missing input." },
879
+ {
880
+ code: 3,
881
+ name: "auth",
882
+ meaning: "Not signed in, token invalid, expired, revoked or missing a scope."
883
+ },
884
+ { code: 4, name: "not_found", meaning: "Project, deployment or linked folder not found." },
885
+ { code: 5, name: "deploy_failed", meaning: "The deployment failed or was canceled." },
886
+ { code: 6, name: "limit", meaning: "A plan limit was reached (projects, size, storage \u2026)." }
887
+ ];
888
+ var MB2 = 1024 * 1024;
889
+ var trimUrl = (url) => url.replace(/\/+$/, "");
890
+ var runsOn = (id) => {
891
+ const info = FRAMEWORK_INFO[id];
892
+ if (info.group === "worker") return "Worker";
893
+ return info.server ? "server (Worker)" : "static";
894
+ };
895
+ function frameworkTable() {
896
+ const rows = FRAMEWORKS.map((id) => {
897
+ const info = FRAMEWORK_INFO[id];
898
+ const build = info.defaults.buildCommand ? `\`${info.defaults.buildCommand}\`` : "none";
899
+ const output = info.defaults.outputDirectory ? `\`${info.defaults.outputDirectory}\`` : "the folder";
900
+ return `| \`${id}\` | ${info.name} | ${runsOn(id)} | ${build} | ${output} |`;
901
+ });
902
+ return [
903
+ "| id | Framework | Runs as | Build command | Output |",
904
+ "| --- | --- | --- | --- | --- |",
905
+ ...rows
906
+ ].join("\n");
907
+ }
908
+ var count = (value) => value === null ? "unlimited" : value.toLocaleString("en-US");
909
+ function limitsTable() {
910
+ const header = ["Limit", ...PLAN_ORDER.map((p) => PLANS[p].name)];
911
+ const row = (label, pick) => [
912
+ label,
913
+ ...PLAN_ORDER.map((p) => pick(PLANS[p]))
914
+ ];
915
+ const rows = [
916
+ row("Projects", (p) => count(p.limits.projects)),
917
+ row("Deployment size", (p) => `${p.limits.maxDeploymentBytes / MB2} MB`),
918
+ row("Files per deployment", (p) => count(p.limits.maxFiles)),
919
+ row("Worker CPU per request", (p) => `${p.limits.cpuMs} ms`),
920
+ row("Subrequests per request", (p) => count(p.limits.subrequests)),
921
+ row("Storage resources per project", (p) => count(p.limits.storageResourcesPerProject)),
922
+ row("Requests per month", (p) => count(p.limits.requestsPerMonth))
923
+ ];
924
+ return [header, header.map(() => "---"), ...rows].map((r) => `| ${r.join(" | ")} |`).join("\n");
925
+ }
926
+ var exitCodeTable = () => [
927
+ "| Code | Meaning |",
928
+ "| --- | --- |",
929
+ ...CLI_EXIT_CODES.map((c) => `| ${c.code} | ${c.name}: ${c.meaning} |`)
930
+ ].join("\n");
931
+ function body(appUrl) {
932
+ const mcp = `${appUrl}/api/mcp`;
933
+ return `# HostEdge: how to build and ship here
934
+
935
+ This project is hosted on HostEdge (${appUrl}): static sites and Workers apps on a global edge network. Every deployment gets its own URL; production is \`https://<project>.hostedge.app\`. Follow this file when you write, deploy or debug code for it. Full docs: ${appUrl}/docs (plain-text index: ${appUrl}/llms.txt).
936
+
937
+ ## Quick path
938
+
939
+ 1. Write the app so it builds to a folder of static files, or to a Worker (see "Static vs server").
940
+ 2. Deploy it with the CLI or the MCP server (see "Deploy").
941
+ 3. Wait until the deployment is \`ready\`; if it is \`failed\`, read \`failureReason\` and the build log, fix the code and deploy again.
942
+ 4. Give the person the URL from the result. Never guess a URL.
943
+
944
+ ## Static vs server
945
+
946
+ - **Static**: a folder of HTML, CSS, JavaScript and assets. No server code runs, so there are no bindings and environment variables exist only at build time. Any framework that builds to static files works.
947
+ - **Server (Workers)**: server rendering, API routes and Workers run as Workers. A \`_worker.js\` file at the top of the output folder becomes the Worker; for several modules write a \`_worker.js/\` folder with an \`index.js\` entry. Static files are served next to it through the \`ASSETS\` binding. Frameworks with a Cloudflare adapter (Next.js with OpenNext, Astro, SvelteKit, Nuxt, React Router) write that Worker during the build.
948
+ - Server code has Node.js APIs through \`nodejs_compat\` (\`Buffer\`, \`AsyncLocalStorage\`, \`crypto\`, most \`node:*\`), but no native modules and no child processes. Outbound requests to private networks are blocked. The server bundle must stay under 10 MB gzipped.
949
+ - \`wrangler.jsonc\` bindings are not used. Storage is created on HostEdge (below) and bound by name.
950
+ - Pick static output when the app does not need a server; it is faster and uses no CPU budget.
951
+
952
+ ## Supported frameworks
953
+
954
+ HostEdge detects the framework from \`package.json\` and the lockfile and fills in the build command and output folder (override them in Project > Settings). Anything else: use \`other\` and set the build command and output folder.
955
+
956
+ ${frameworkTable()}
957
+
958
+ Notes: Next.js static export needs \`output: "export"\` (\`next\`); for server features use \`next-server\`. Astro, SvelteKit, Nuxt and React Router become server presets when their Cloudflare adapter is installed. Single-page apps (Vite, Angular, Create React App, React Router SPA) get SPA fallback; others answer unknown paths with \`404.html\`. Builds run on Node.js 22 with the package manager your lockfile names.
959
+
960
+ ## Bindings (databases, key-value, files)
961
+
962
+ Server code reaches storage through \`env\`. Create the storage first, then redeploy so the code is bound to it.
963
+
964
+ | Storage | Built on | Default binding | Code |
965
+ | --- | --- | --- | --- |
966
+ | SQL database | SQLite | \`env.DB\` | \`await env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(email).all()\` |
967
+ | Key-value store | KV | \`env.KV\` | \`await env.KV.put('flag', 'on', { expirationTtl: 3600 })\` |
968
+ | File storage | Object storage | \`env.FILES\` | \`await env.FILES.put(key, request.body)\` |
969
+
970
+ - Create a database with the MCP tool \`create_database\` (binding defaults to \`DB\`), or in the dashboard under Project > Storage (databases, key-value stores and file storage). Run SQL against it with \`run_sql\`.
971
+ - Binding names use capital letters, digits and underscores, and must not clash with an environment variable of the same project.
972
+ - A static site has no server code, so it cannot use bindings.
973
+ - Previews share production storage unless the person gives previews their own copy in the dashboard.
974
+
975
+ ## Sign-in for the app (HostEdge Auth)
976
+
977
+ When the app needs user accounts, use HostEdge Auth instead of a third-party service. Users are stored in the project's own SQL database.
978
+
979
+ 1. Create a database (binding \`DB\`) and, in the dashboard under Project > Storage > the database > Authentication, choose **Set up authentication** (creates the \`auth_*\` tables; the library also creates them on first use).
980
+ 2. Download \`https://hostedge.app/auth-kit/hostedge-auth.ts\` into the project. One file, no dependencies.
981
+ 3. In the Worker: \`const auth = createAuth(env.DB, { env })\`, then \`const response = await auth.handle(request); if (response) return response\`, and \`await auth.getUser(request)\` for the signed-in \`{ id, email }\` or null. Redeploy.
982
+
983
+ - \`auth.handle\` serves \`POST /auth/sign-up\`, \`/auth/sign-in\`, \`/auth/sign-out\`, \`/auth/magic-link\`, \`/auth/reset-request\`, \`/auth/reset\` (JSON bodies, \`content-type: application/json\`) and \`GET /auth/me\`, \`/auth/verify\`, \`/auth/oauth/github\`, \`/auth/oauth/google\`.
984
+ - Passing \`env\` lets HostEdge send magic-link, reset and confirmation emails (limited per plan per day). To use another sender pass \`sendEmail: async ({ to, kind, url }) => {\u2026}\`. The reset email links to \`/reset-password?token=\u2026\` on the site: build that page and post \`{ token, password }\` to \`/auth/reset\`.
985
+ - Options: \`confirmEmail: true\` to require a confirmed email before password sign-in; \`oauth: { github: { clientId, clientSecret }, google: {\u2026} }\` with the secrets as environment variables; redirect URI \`https://<site>/auth/oauth/<provider>/callback\`.
986
+ - Never write your own password hashing or session handling; do not store passwords yourself.
987
+
988
+ ## Using Postgres instead
989
+
990
+ For heavy apps (thousands of concurrent users, complex queries) the built-in SQLite-based database is not the right fit. The person can use **Connect Postgres** under Project > Storage (Neon, Supabase or any host): it saves \`DATABASE_URL\` as an environment variable in every environment. Read it as \`env.DATABASE_URL\` (for example \`postgres(env.DATABASE_URL, { prepare: false })\`), prefer the host's pooled connection string, and redeploy after it is set.
991
+
992
+ ## Environment variables
993
+
994
+ - Set them with the MCP tool \`set_env_vars\` (\`project\`, \`vars\`, optional \`environments\`: \`production\`, \`preview\`) or in the dashboard. The CLI cannot set them; \`hostedge env pull\` writes them to a git-ignored \`.env.local\` for local work.
995
+ - Workers read them as \`env.NAME\` at runtime; static sites only see them at build time.
996
+ - Values are encrypted and never read back; \`list_env_vars\` returns names only.
997
+ - Deployments keep the values they were built with: **redeploy after changing variables or bindings**.
998
+ - At most 63 variables per project and 5 KB per value. Names starting with \`HOSTEDGE_\` or \`CF_\` are reserved.
999
+
1000
+ ## Deploy
1001
+
1002
+ Check who you are first: \`npx hostedge whoami --json\`.
1003
+
1004
+ ### With the CLI
1005
+
1006
+ Authenticate with an API token from Settings > API tokens: set \`HOSTEDGE_TOKEN\` (preferred; nothing is written to disk), or save it once with \`printf %s "$TOKEN" | npx hostedge login --with-token\`. Interactive \`hostedge login\` opens a browser and is for people.
1007
+
1008
+ \`\`\`sh
1009
+ npx hostedge deploy --wait --json --yes # production; prints {deploymentId,status,url,...}
1010
+ npx hostedge deploy --preview --wait --json # a preview that does not touch production
1011
+ npx hostedge logs --json --since 1h --level error
1012
+ npx hostedge env pull # variables to .env.local
1013
+ \`\`\`
1014
+
1015
+ Always pass \`--json\`: stdout is then one JSON document (NDJSON for \`logs\`) and all human text goes to stderr. \`--yes\` or \`CI=1\` makes it non-interactive; it never waits for input and fails with a clear message instead. \`--quiet\` hides progress. \`--api-url <url>\` or \`HOSTEDGE_API_URL\` points at another HostEdge. The first \`deploy\` in an unlinked folder creates the project and writes \`.hostedge/project.json\` (git-ignored; do not delete it, it links the folder to the project). Files that look like secrets (\`.env*\`, keys) and symbolic links are never uploaded; \`.git\` and \`node_modules\` are skipped.
1016
+
1017
+ Exit codes:
1018
+
1019
+ ${exitCodeTable()}
1020
+
1021
+ \`deploy --wait --json\` on failure still prints JSON with \`"status":"failed"\` and \`"failureReason"\`, and exits 5.
1022
+
1023
+ ### With the MCP server
1024
+
1025
+ Endpoint: \`${mcp}\` (Streamable HTTP, \`Authorization: Bearer <API token>\`, or OAuth when the client supports it). In Claude Code: \`claude mcp add --transport http hostedge ${mcp}\`.
1026
+
1027
+ | Tool | Use |
1028
+ | --- | --- |
1029
+ | \`list_projects\` | Projects with URL and latest deployment |
1030
+ | \`deploy_files\` | Deploy files you provide (\`project\`, \`files[{path,content,encoding}]\`, \`message\`); creates the project if needed; up to 2,000 files and 20 MB per call |
1031
+ | \`deploy_github_repo\` | Deploy a public GitHub repository into a new project |
1032
+ | \`get_deployment\` | Status, URL, \`failureReason\` and the last build log lines |
1033
+ | \`list_deployments\`, \`rollback\` | History; make an earlier ready deployment production |
1034
+ | \`get_logs\`, \`list_errors\` | Runtime logs (\`level\`, \`search\`); open errors grouped by message |
1035
+ | \`set_env_vars\`, \`list_env_vars\` | Write variables (values never read back); list names |
1036
+ | \`create_database\`, \`run_sql\` | A SQLite database bound as \`env.DB\`; run SQL on it |
1037
+ | \`get_analytics\` | Visitors, top pages and sources |
1038
+
1039
+ For sites over 20 MB use the CLI.
1040
+
1041
+ ## Wait until ready
1042
+
1043
+ A deployment goes \`queued\` > \`building\` > \`deploying\` > \`ready\`, or ends \`failed\` or \`canceled\`. \`deploy_files\` returns right away; poll \`get_deployment\` every 2 to 5 seconds until the status is \`ready\`, \`failed\` or \`canceled\` (stop after about 5 minutes and tell the person). With the CLI, \`deploy --wait\` does the polling for you. Only report success when the status is \`ready\`.
1044
+
1045
+ ## Read logs and debug
1046
+
1047
+ - Build problems: the \`failureReason\` and the build log in \`get_deployment\` (CLI: the deploy output).
1048
+ - Runtime problems: \`get_logs\` / \`list_errors\`, or \`hostedge logs --json --level error --since 1h\` (add \`--follow\` to tail). Ranges are \`1h\`, \`24h\` and \`7d\`.
1049
+ - Typical causes: wrong output folder (set it in Project > Settings or pick the right framework), missing environment variable, a binding that was created but not redeployed, a native module in server code.
1050
+
1051
+ ## Roll back
1052
+
1053
+ \`list_deployments\` for the project, pick an earlier deployment whose status is \`ready\`, then \`rollback\` with its \`deploymentId\`. It is instant (no rebuild) and can be undone the same way. The CLI has no rollback command; the dashboard has one too.
1054
+
1055
+ ## Limits
1056
+
1057
+ Plan limits (per workspace unless noted):
1058
+
1059
+ ${limitsTable()}
1060
+
1061
+ A single file may be at most ${MAX_FILE_BYTES / MB2} MB. At the limit, calls fail with a "limit reached" error (CLI exit code 6); tell the person which limit and that upgrading the plan (Settings > Billing) raises it. Do not retry in a loop. Custom domains, cron jobs, forms and uptime monitors are set up in the dashboard.
1062
+
1063
+ ## Rules
1064
+
1065
+ - Never commit secrets (API tokens, \`.env\` files, private keys) and never print or echo \`HOSTEDGE_TOKEN\`. Put secrets in environment variables with \`set_env_vars\`, not in code.
1066
+ - Redeploy after changing environment variables or bindings; they only apply to new deployments.
1067
+ - Prefer the smallest correct setup: static output unless the app needs a server.
1068
+ - Do not invent HostEdge features, flags or endpoints. If this file does not cover it, read ${appUrl}/docs or ${appUrl}/llms.txt.
1069
+ - Do not delete \`.hostedge/project.json\`. Do not deploy from a folder you did not build the app in.
1070
+ - Verify before telling the person it works: status \`ready\`, then fetch the URL.
1071
+ `;
1072
+ }
1073
+ var DESCRIPTION = "Build, deploy, debug and roll back apps on HostEdge (static sites and Workers apps). Use when the task involves hosting, deploying, logs, environment variables, databases or the hostedge CLI/MCP.";
1074
+ function renderAgentsMd(options = {}) {
1075
+ return body(trimUrl(options.appUrl ?? AI_KIT_DEFAULT_URL));
1076
+ }
1077
+ function renderClaudeMd(options = {}) {
1078
+ const appUrl = trimUrl(options.appUrl ?? AI_KIT_DEFAULT_URL);
1079
+ return `# Claude Code instructions
1080
+
1081
+ @AGENTS.md
1082
+
1083
+ This project deploys to HostEdge (${appUrl}). The instructions above explain how to build, deploy, read logs and roll back; the \`hostedge\` skill in \`.claude/skills/hostedge/\` has the same content for on-demand use.
1084
+ `;
1085
+ }
1086
+ function renderCursorRule(options = {}) {
1087
+ return `---
1088
+ description: ${DESCRIPTION}
1089
+ globs:
1090
+ alwaysApply: false
1091
+ ---
1092
+
1093
+ ${body(trimUrl(options.appUrl ?? AI_KIT_DEFAULT_URL))}`;
1094
+ }
1095
+ function renderSkill(options = {}) {
1096
+ return `---
1097
+ name: hostedge
1098
+ description: ${DESCRIPTION}
1099
+ ---
1100
+
1101
+ ${body(trimUrl(options.appUrl ?? AI_KIT_DEFAULT_URL))}`;
1102
+ }
1103
+ function aiKitFiles(options = {}) {
1104
+ return [
1105
+ {
1106
+ id: "agents",
1107
+ path: "AGENTS.md",
1108
+ description: "Instructions for any coding assistant (the shared source)",
1109
+ content: renderAgentsMd(options)
1110
+ },
1111
+ {
1112
+ id: "claude",
1113
+ path: "CLAUDE.md",
1114
+ description: "Claude Code: imports AGENTS.md",
1115
+ content: renderClaudeMd(options)
1116
+ },
1117
+ {
1118
+ id: "cursor",
1119
+ path: ".cursor/rules/hostedge.mdc",
1120
+ description: "Cursor rule",
1121
+ content: renderCursorRule(options)
1122
+ },
1123
+ {
1124
+ id: "skill",
1125
+ path: ".claude/skills/hostedge/SKILL.md",
1126
+ description: "Claude Code skill",
1127
+ content: renderSkill(options)
1128
+ }
1129
+ ];
1130
+ }
1131
+
1132
+ // ../../packages/core/src/client.ts
1133
+ async function contentHash(content) {
1134
+ const buffer = content instanceof Uint8Array ? content : new Uint8Array(content);
1135
+ const digest = new Uint8Array(
1136
+ await crypto.subtle.digest("SHA-256", buffer)
1137
+ );
1138
+ let hex = "";
1139
+ for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
1140
+ return hex.slice(0, 32);
1141
+ }
1142
+
1143
+ // src/cli.ts
1144
+ import { parseArgs } from "node:util";
1145
+
1146
+ // src/commands/ai-kit.ts
1147
+ import { lstat, mkdir, readFile, stat, writeFile } from "node:fs/promises";
1148
+ import { dirname, join, resolve } from "node:path";
1149
+
1150
+ // src/io.ts
1151
+ var EXIT = {
1152
+ ok: 0,
1153
+ failure: 1,
1154
+ usage: 2,
1155
+ auth: 3,
1156
+ notFound: 4,
1157
+ deployFailed: 5,
1158
+ limit: 6
1159
+ };
1160
+ function exitName(code) {
1161
+ return CLI_EXIT_CODES.find((c) => c.code === code)?.name ?? "failure";
1162
+ }
1163
+ var CliError = class extends Error {
1164
+ constructor(message, exitCode = EXIT.failure) {
1165
+ super(message);
1166
+ this.exitCode = exitCode;
1167
+ }
1168
+ exitCode;
1169
+ };
1170
+
1171
+ // src/commands/ai-kit.ts
1172
+ function lineDelta(previous, next) {
1173
+ const counts = /* @__PURE__ */ new Map();
1174
+ for (const line of previous.split("\n")) counts.set(line, (counts.get(line) ?? 0) + 1);
1175
+ let added = 0;
1176
+ for (const line of next.split("\n")) {
1177
+ const left = counts.get(line) ?? 0;
1178
+ if (left > 0) counts.set(line, left - 1);
1179
+ else added += 1;
1180
+ }
1181
+ let removed = 0;
1182
+ for (const left of counts.values()) removed += left;
1183
+ return { added, removed };
1184
+ }
1185
+ async function writeOne(dir, file, force) {
1186
+ const target = join(dir, ...file.path.split("/"));
1187
+ const base = { id: file.id, path: file.path };
1188
+ const info = await lstat(target).catch(() => null);
1189
+ if (info?.isSymbolicLink())
1190
+ return { ...base, action: "skipped", added: 0, removed: 0, reason: "it is a symbolic link" };
1191
+ if (info && !info.isFile())
1192
+ return { ...base, action: "skipped", added: 0, removed: 0, reason: "it is not a file" };
1193
+ const previous = info ? await readFile(target, "utf8") : null;
1194
+ if (previous === file.content) return { ...base, action: "unchanged", added: 0, removed: 0 };
1195
+ const delta = previous === null ? { added: file.content.split("\n").length, removed: 0 } : lineDelta(previous, file.content);
1196
+ if (previous !== null && !force)
1197
+ return {
1198
+ ...base,
1199
+ action: "skipped",
1200
+ ...delta,
1201
+ reason: "it already exists with different content (use --force to overwrite)"
1202
+ };
1203
+ try {
1204
+ await mkdir(dirname(target), { recursive: true });
1205
+ await writeFile(target, file.content);
1206
+ } catch (error) {
1207
+ throw new CliError(`Could not write ${file.path}: ${error.message}`, EXIT.failure);
1208
+ }
1209
+ return { ...base, action: previous === null ? "created" : "overwritten", ...delta };
1210
+ }
1211
+ async function writeKit(dir, options) {
1212
+ const info = await stat(dir).catch(() => null);
1213
+ if (!info?.isDirectory()) throw new CliError(`${dir} is not a folder.`, EXIT.usage);
1214
+ const results = [];
1215
+ for (const file of aiKitFiles()) results.push(await writeOne(dir, file, options.force === true));
1216
+ return results;
1217
+ }
1218
+ function formatKit(r, results) {
1219
+ const s = r.s;
1220
+ const lines = results.map((f) => {
1221
+ switch (f.action) {
1222
+ case "created":
1223
+ return `${s.green("+")} ${f.path} ${s.dim(`(created, ${f.added} lines)`)}`;
1224
+ case "overwritten":
1225
+ return `${s.yellow("~")} ${f.path} ${s.dim(`(overwritten: +${f.added} -${f.removed} lines)`)}`;
1226
+ case "unchanged":
1227
+ return `${s.dim("=")} ${f.path} ${s.dim("(already up to date)")}`;
1228
+ default:
1229
+ return `${s.yellow("!")} ${f.path} ${s.dim(`(kept: ${f.reason ?? "skipped"}${f.added || f.removed ? `; HostEdge's version is +${f.added} -${f.removed} lines` : ""})`)}`;
1230
+ }
1231
+ });
1232
+ const kept = results.filter((f) => f.action === "skipped").length;
1233
+ const wrote = results.filter((f) => f.action === "created" || f.action === "overwritten").length;
1234
+ const note = kept > 0 ? `${wrote} written, ${kept} kept. Run again with --force to overwrite the kept files.` : wrote > 0 ? `${wrote} written. Commit them so every assistant on the project reads them.` : "Nothing to change.";
1235
+ return `${lines.join("\n")}
1236
+ ${note}
1237
+ `;
1238
+ }
1239
+ async function aiKit(r, options) {
1240
+ const dir = resolve(r.io.cwd, options.dir ?? ".");
1241
+ const files = await writeKit(dir, { force: options.force });
1242
+ r.result({ dir, files }, formatKit(r, files));
1243
+ return EXIT.ok;
1244
+ }
1245
+
1246
+ // src/commands/auth.ts
1247
+ import { randomBytes } from "node:crypto";
1248
+ import { createServer } from "node:http";
1249
+ import { hostname } from "node:os";
1250
+
1251
+ // src/config.ts
1252
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod, rm } from "node:fs/promises";
1253
+ import { homedir } from "node:os";
1254
+ import { dirname as dirname2, join as join2 } from "node:path";
1255
+ var DEFAULT_API_URL = "https://hostedge.app";
1256
+ function configPath(io2) {
1257
+ const base = io2.env.HOSTEDGE_CONFIG_DIR ?? join2(io2.env.XDG_CONFIG_HOME ?? join2(io2.env.HOME ?? homedir(), ".config"), "hostedge");
1258
+ return join2(base, "config.json");
1259
+ }
1260
+ async function readJson(path) {
1261
+ try {
1262
+ return JSON.parse(await readFile2(path, "utf8"));
1263
+ } catch {
1264
+ return null;
1265
+ }
1266
+ }
1267
+ async function readConfig(io2) {
1268
+ return await readJson(configPath(io2)) ?? {};
1269
+ }
1270
+ async function writeConfig(io2, config) {
1271
+ const path = configPath(io2);
1272
+ await mkdir2(dirname2(path), { recursive: true, mode: 448 });
1273
+ await writeFile2(path, `${JSON.stringify(config, null, 2)}
1274
+ `, { mode: 384 });
1275
+ await chmod(path, 384);
1276
+ }
1277
+ async function clearConfig(io2) {
1278
+ await rm(configPath(io2), { force: true });
1279
+ }
1280
+ async function credentials(io2) {
1281
+ const saved = await readConfig(io2);
1282
+ const fromEnv = io2.env.HOSTEDGE_TOKEN?.trim();
1283
+ const token = fromEnv || saved.token || null;
1284
+ return {
1285
+ apiUrl: (io2.env.HOSTEDGE_API_URL || saved.apiUrl || DEFAULT_API_URL).replace(/\/+$/, ""),
1286
+ token,
1287
+ source: fromEnv ? "env" : saved.token ? "saved" : null
1288
+ };
1289
+ }
1290
+ var linkPath = (dir) => join2(dir, ".hostedge", "project.json");
1291
+ async function readLink(dir) {
1292
+ const link = await readJson(linkPath(dir));
1293
+ return link && typeof link.projectId === "string" ? link : null;
1294
+ }
1295
+ async function writeLink(dir, link) {
1296
+ await mkdir2(join2(dir, ".hostedge"), { recursive: true });
1297
+ await writeFile2(linkPath(dir), `${JSON.stringify(link, null, 2)}
1298
+ `);
1299
+ }
1300
+ async function ensureGitignored(dir, entry) {
1301
+ const path = join2(dir, ".gitignore");
1302
+ let text = "";
1303
+ try {
1304
+ text = await readFile2(path, "utf8");
1305
+ } catch {
1306
+ }
1307
+ const lines = text.split(/\r?\n/).map((l) => l.trim());
1308
+ if (lines.includes(entry) || lines.includes(`/${entry}`)) return false;
1309
+ await writeFile2(path, `${text}${text && !text.endsWith("\n") ? "\n" : ""}${entry}
1310
+ `);
1311
+ return true;
1312
+ }
1313
+
1314
+ // src/api.ts
1315
+ function exitCodeFor(status, code) {
1316
+ if (status === 401 || status === 403 || code === "unauthorized" || code === "forbidden")
1317
+ return EXIT.auth;
1318
+ if (status === 404 || code === "not_found") return EXIT.notFound;
1319
+ if (status === 402 || code === "limit_reached") return EXIT.limit;
1320
+ return EXIT.failure;
1321
+ }
1322
+ var ApiError = class extends CliError {
1323
+ constructor(message, status, code) {
1324
+ super(message, exitCodeFor(status, code));
1325
+ this.status = status;
1326
+ this.code = code;
1327
+ }
1328
+ status;
1329
+ code;
1330
+ };
1331
+ function createApi(io2, apiUrl, token) {
1332
+ async function raw(path, init2 = {}) {
1333
+ const headers = new Headers(init2.headers);
1334
+ headers.set("authorization", `Bearer ${token}`);
1335
+ headers.set("user-agent", "hostedge-cli/0.1.1");
1336
+ let response;
1337
+ try {
1338
+ response = await io2.fetch(`${apiUrl}${path}`, { ...init2, headers });
1339
+ } catch {
1340
+ throw new CliError(`Could not reach ${apiUrl}. Check your connection, or HOSTEDGE_API_URL.`);
1341
+ }
1342
+ if (!response.ok) {
1343
+ const body2 = await response.json().catch(() => ({}));
1344
+ const fields = body2.error?.fields ? ` ${Object.entries(body2.error.fields).map(([k, v]) => `${k}: ${v}`).join("; ")}` : "";
1345
+ const message = response.status === 401 ? `${body2.error?.message ?? "Not signed in."} Run \`hostedge login\`.` : `${body2.error?.message ?? `HostEdge answered ${response.status}.`}${fields}`;
1346
+ throw new ApiError(message, response.status, body2.error?.code ?? null);
1347
+ }
1348
+ return response;
1349
+ }
1350
+ return {
1351
+ apiUrl,
1352
+ raw,
1353
+ async request(path, init2 = {}) {
1354
+ const { json, ...rest } = init2;
1355
+ let request = rest;
1356
+ if (json !== void 0) {
1357
+ const headers = new Headers(rest.headers);
1358
+ headers.set("content-type", "application/json");
1359
+ request = { ...rest, method: rest.method ?? "POST", headers, body: JSON.stringify(json) };
1360
+ }
1361
+ const response = await raw(path, request);
1362
+ return await response.json();
1363
+ }
1364
+ };
1365
+ }
1366
+ async function connect(io2) {
1367
+ const { apiUrl, token } = await credentials(io2);
1368
+ if (!token)
1369
+ throw new CliError("Not signed in. Run `hostedge login` (or set HOSTEDGE_TOKEN).", EXIT.auth);
1370
+ return createApi(io2, apiUrl, token);
1371
+ }
1372
+
1373
+ // src/commands/auth.ts
1374
+ var LOGIN_TIMEOUT_MS = 5 * 6e4;
1375
+ var DONE_PAGE = `<!doctype html><meta charset="utf-8"><title>hostedge CLI</title>
1376
+ <body style="font:16px system-ui;margin:4rem auto;max-width:28rem;color:#16161a;background:#f7f6f2">
1377
+ <p><strong>The hostedge CLI is signed in.</strong></p><p>You can close this tab and return to your terminal.</p>`;
1378
+ async function browserLogin(r, apiUrl) {
1379
+ const { io: io2 } = r;
1380
+ const state = randomBytes(24).toString("base64url");
1381
+ let resolveToken = () => void 0;
1382
+ const received = new Promise((resolve6) => {
1383
+ resolveToken = resolve6;
1384
+ });
1385
+ const server = createServer((req, res) => {
1386
+ const url2 = new URL(req.url ?? "/", "http://127.0.0.1");
1387
+ const token = url2.searchParams.get("token");
1388
+ if (url2.pathname !== "/callback" || url2.searchParams.get("state") !== state || !token) {
1389
+ res.writeHead(400, { "content-type": "text/plain" }).end("Unexpected request.");
1390
+ return;
1391
+ }
1392
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(DONE_PAGE);
1393
+ resolveToken(token);
1394
+ });
1395
+ await new Promise((resolve6) => server.listen(0, "127.0.0.1", resolve6));
1396
+ const address = server.address();
1397
+ const port = typeof address === "object" && address ? address.port : 0;
1398
+ const url = `${apiUrl}/cli/authorize?${new URLSearchParams({ port: String(port), state, host: hostname() })}`;
1399
+ const s = r.s;
1400
+ const opened = await io2.openUrl(url);
1401
+ r.always(
1402
+ opened ? `Opened your browser to authorize the CLI. If nothing happened, visit:
1403
+ ${s.bold(url)}
1404
+ ` : `Open this URL in a browser to authorize the CLI:
1405
+ ${s.bold(url)}
1406
+ `
1407
+ );
1408
+ let timer;
1409
+ const timeout = new Promise((_, reject) => {
1410
+ timer = setTimeout(
1411
+ () => reject(
1412
+ new CliError("Timed out waiting for the browser. Run `hostedge login` again.", EXIT.auth)
1413
+ ),
1414
+ LOGIN_TIMEOUT_MS
1415
+ );
1416
+ });
1417
+ try {
1418
+ return await Promise.race([received, timeout]);
1419
+ } finally {
1420
+ clearTimeout(timer);
1421
+ server.close();
1422
+ }
1423
+ }
1424
+ async function obtainToken(r, options, apiUrl) {
1425
+ const { io: io2 } = r;
1426
+ if (options.token !== void 0 && options.withToken)
1427
+ throw new CliError("Use either --token or --with-token, not both.", EXIT.usage);
1428
+ if (options.token !== void 0) {
1429
+ const token = options.token.trim();
1430
+ if (!token)
1431
+ throw new CliError("--token needs a value. Prefer --with-token (reads stdin).", EXIT.usage);
1432
+ r.warn(
1433
+ `${r.s.dim("Tip: --token shows in your shell history and process list; prefer --with-token or HOSTEDGE_TOKEN.")}
1434
+ `
1435
+ );
1436
+ return { token, source: "flag" };
1437
+ }
1438
+ if (options.withToken) {
1439
+ if (io2.isStdinTty)
1440
+ throw new CliError(
1441
+ '--with-token reads the token from stdin, but stdin is a terminal. Pipe it in, e.g. `printf %s "$TOKEN" | hostedge login --with-token`.',
1442
+ EXIT.usage
1443
+ );
1444
+ const token = (await io2.readStdin()).trim();
1445
+ if (!token) throw new CliError("No token received on stdin.", EXIT.usage);
1446
+ return { token, source: "stdin" };
1447
+ }
1448
+ const fromEnv = io2.env.HOSTEDGE_TOKEN?.trim();
1449
+ if (fromEnv) return { token: fromEnv, source: "env" };
1450
+ if (!r.interactive)
1451
+ throw new CliError(
1452
+ "Not signed in, and a browser login needs a person. Set HOSTEDGE_TOKEN, or pass --with-token (token on stdin) or --token <token>.",
1453
+ EXIT.auth
1454
+ );
1455
+ return { token: await browserLogin(r, apiUrl), source: "browser" };
1456
+ }
1457
+ async function login(r, options) {
1458
+ const { io: io2 } = r;
1459
+ const saved = await readConfig(io2);
1460
+ const apiUrl = (options.api || io2.env.HOSTEDGE_API_URL || saved.apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
1461
+ const { token, source } = await obtainToken(r, options, apiUrl);
1462
+ const identity = await createApi(io2, apiUrl, token).request("/api/v1/me");
1463
+ const saveIt = source !== "env";
1464
+ if (saveIt) await writeConfig(io2, { apiUrl, token, workspace: identity.workspace.slug });
1465
+ const s = r.s;
1466
+ r.result(
1467
+ {
1468
+ signedIn: true,
1469
+ user: identity.user,
1470
+ workspace: identity.workspace,
1471
+ scopes: identity.scopes,
1472
+ apiUrl,
1473
+ tokenSource: source,
1474
+ saved: saveIt
1475
+ },
1476
+ `${s.green("\u2713")} Signed in as ${s.bold(identity.user.email)} in ${s.bold(identity.workspace.name)}.${saveIt ? "" : " (token from HOSTEDGE_TOKEN, not saved)"}
1477
+ `
1478
+ );
1479
+ return EXIT.ok;
1480
+ }
1481
+ async function logout(r) {
1482
+ await clearConfig(r.io);
1483
+ r.result(
1484
+ { signedOut: true },
1485
+ "Signed out on this computer. The token still exists until you revoke it in Settings \u2192 API tokens.\n"
1486
+ );
1487
+ return EXIT.ok;
1488
+ }
1489
+ async function whoami(r) {
1490
+ const api = await connect(r.io);
1491
+ const me = await api.request("/api/v1/me");
1492
+ const { apiUrl, source } = await credentials(r.io);
1493
+ const text = `${me.user.email} in ${me.workspace.name} (${me.workspace.slug}) on ${apiUrl}
1494
+ Scopes: ${me.scopes.join(", ")}
1495
+ `;
1496
+ r.result(
1497
+ { user: me.user, workspace: me.workspace, scopes: me.scopes, apiUrl, tokenSource: source },
1498
+ text,
1499
+ text
1500
+ );
1501
+ return EXIT.ok;
1502
+ }
1503
+
1504
+ // src/commands/deploy.ts
1505
+ import { readFile as readFile4 } from "node:fs/promises";
1506
+ import { basename, join as join4, resolve as resolve2 } from "node:path";
1507
+
1508
+ // src/files.ts
1509
+ import { lstat as lstat2, readdir, readFile as readFile3 } from "node:fs/promises";
1510
+ import { join as join3, relative, sep } from "node:path";
1511
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".hostedge", ".next", ".turbo", ".wrangler"]);
1512
+ async function collectFiles(root, limits = { maxFiles: 1e5 }) {
1513
+ const files = [];
1514
+ const skipped = [];
1515
+ async function walk(dir) {
1516
+ const entries = await readdir(dir, { withFileTypes: true });
1517
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1518
+ for (const entry of entries) {
1519
+ const absolute = join3(dir, entry.name);
1520
+ const rel = relative(root, absolute).split(sep).join("/");
1521
+ if (entry.isSymbolicLink())
1522
+ throw new CliError(
1523
+ `${rel} is a symbolic link. HostEdge uploads real files only \u2014 replace the link with the file it points to.`
1524
+ );
1525
+ if (entry.isDirectory()) {
1526
+ if (SKIP_DIRS.has(entry.name)) skipped.push(`${rel}/`);
1527
+ else await walk(absolute);
1528
+ continue;
1529
+ }
1530
+ if (!entry.isFile()) continue;
1531
+ const normal = normalisePath(rel);
1532
+ if (!normal.ok) throw new CliError(`${rel}: ${normal.reason}`);
1533
+ if (isIgnoredPath(normal.path)) {
1534
+ skipped.push(normal.path);
1535
+ continue;
1536
+ }
1537
+ if (files.length >= limits.maxFiles)
1538
+ throw new CliError(`More than ${limits.maxFiles.toLocaleString("en-US")} files to upload.`);
1539
+ const stat3 = await lstat2(absolute);
1540
+ files.push({ path: normal.path, size: stat3.size, hash: "", absolute });
1541
+ }
1542
+ }
1543
+ try {
1544
+ await walk(root);
1545
+ } catch (error) {
1546
+ if (error instanceof CliError) throw error;
1547
+ throw new CliError(`Could not read ${root}: ${error.message}`);
1548
+ }
1549
+ const check = checkManifest(
1550
+ files.map((f) => ({ path: f.path, size: f.size, hash: "0".repeat(32) })),
1551
+ { maxDeploymentBytes: Number.MAX_SAFE_INTEGER, maxFiles: Number.MAX_SAFE_INTEGER }
1552
+ );
1553
+ const secrets = /* @__PURE__ */ new Set();
1554
+ for (const problem of check.problems) {
1555
+ if (problem.reason.startsWith("Looks like a secret")) secrets.add(problem.path);
1556
+ else throw new CliError(`${problem.path}: ${problem.reason}`);
1557
+ }
1558
+ const upload = files.filter((f) => !secrets.has(f.path));
1559
+ if (limits.hash !== false)
1560
+ for (const file of upload) file.hash = await contentHash(await readFile3(file.absolute));
1561
+ return { files: upload, skipped, secrets: [...secrets] };
1562
+ }
1563
+ async function withRetries(work, retryable, attempts = 4, delayMs = 500) {
1564
+ for (let attempt = 1; ; attempt++) {
1565
+ try {
1566
+ return await work();
1567
+ } catch (error) {
1568
+ if (attempt >= attempts || !retryable(error)) throw error;
1569
+ await new Promise((resolve6) => setTimeout(resolve6, delayMs * 2 ** (attempt - 1)));
1570
+ }
1571
+ }
1572
+ }
1573
+ async function pool(items, limit, work) {
1574
+ let next = 0;
1575
+ const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
1576
+ while (next < items.length) {
1577
+ const item = items[next++];
1578
+ await work(item);
1579
+ }
1580
+ });
1581
+ await Promise.all(runners);
1582
+ }
1583
+
1584
+ // src/output.ts
1585
+ function styles(io2, stream = "stdout", noColor = false) {
1586
+ const tty = stream === "stdout" ? io2.isTty : io2.isErrTty;
1587
+ const on = tty && !noColor && !io2.env.NO_COLOR && io2.env.TERM !== "dumb";
1588
+ const wrap = (open, close) => (text) => on ? `\x1B[${open}m${text}\x1B[${close}m` : text;
1589
+ return {
1590
+ bold: wrap(1, 22),
1591
+ dim: wrap(2, 22),
1592
+ red: wrap(31, 39),
1593
+ green: wrap(32, 39),
1594
+ yellow: wrap(33, 39),
1595
+ cyan: wrap(36, 39)
1596
+ };
1597
+ }
1598
+ function formatBytes(bytes) {
1599
+ if (bytes < 1024) return `${bytes} B`;
1600
+ const units = ["KB", "MB", "GB"];
1601
+ let value = bytes / 1024;
1602
+ let unit = 0;
1603
+ while (value >= 1024 && unit < units.length - 1) {
1604
+ value /= 1024;
1605
+ unit += 1;
1606
+ }
1607
+ return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unit]}`;
1608
+ }
1609
+
1610
+ // src/sse.ts
1611
+ async function* readSse(body2) {
1612
+ const decoder = new TextDecoder();
1613
+ let buffer = "";
1614
+ let event = "message";
1615
+ let data = [];
1616
+ let id = null;
1617
+ for await (const chunk of body2) {
1618
+ buffer += decoder.decode(chunk, { stream: true });
1619
+ let newline = buffer.search(/\r?\n/);
1620
+ while (newline !== -1) {
1621
+ const line = buffer.slice(0, newline);
1622
+ buffer = buffer.slice(buffer[newline] === "\r" ? newline + 2 : newline + 1);
1623
+ if (line === "") {
1624
+ if (data.length > 0) yield { event, data: data.join("\n"), id };
1625
+ event = "message";
1626
+ data = [];
1627
+ } else if (!line.startsWith(":")) {
1628
+ const colon = line.indexOf(":");
1629
+ const field = colon === -1 ? line : line.slice(0, colon);
1630
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
1631
+ if (field === "event") event = value;
1632
+ else if (field === "data") data.push(value);
1633
+ else if (field === "id") id = value;
1634
+ }
1635
+ newline = buffer.search(/\r?\n/);
1636
+ }
1637
+ }
1638
+ }
1639
+
1640
+ // src/commands/deploy.ts
1641
+ var FINISHED = /* @__PURE__ */ new Set(["ready", "failed", "canceled"]);
1642
+ var UPLOAD_CONCURRENCY = 16;
1643
+ var UPLOAD_TIMEOUT_MS = 3e4;
1644
+ function temporary(error) {
1645
+ if (error instanceof ApiError) return error.status === 429 || error.status >= 500;
1646
+ return error instanceof CliError;
1647
+ }
1648
+ async function follow(r, api, deploymentId) {
1649
+ const s = r.s;
1650
+ let cursor = null;
1651
+ let last = { status: "queued", failureReason: null };
1652
+ for (let attempt = 0; attempt < 20; attempt++) {
1653
+ const response = await api.raw(`/api/v1/deployments/${deploymentId}/events`, {
1654
+ headers: cursor ? { "last-event-id": cursor, accept: "text/event-stream" } : {}
1655
+ });
1656
+ if (!response.body) break;
1657
+ for await (const event of readSse(response.body)) {
1658
+ if (event.id) cursor = event.id;
1659
+ if (event.event === "log") {
1660
+ for (const line of JSON.parse(event.data)) {
1661
+ const text = line.level === "error" ? s.red(line.msg) : line.level === "warn" ? s.yellow(line.msg) : line.level === "success" ? s.green(line.msg) : line.msg;
1662
+ r.say(`${s.dim("\u2502")} ${text}
1663
+ `);
1664
+ }
1665
+ } else if (event.event === "status") {
1666
+ const data = JSON.parse(event.data);
1667
+ last = { status: data.status, failureReason: data.failureReason };
1668
+ } else if (event.event === "end") {
1669
+ return last;
1670
+ }
1671
+ }
1672
+ if (FINISHED.has(last.status)) return last;
1673
+ await r.io.sleep(1e3);
1674
+ }
1675
+ return last;
1676
+ }
1677
+ async function lookup(api, deploymentId) {
1678
+ try {
1679
+ const d = await api.request(`/api/v1/deployments/${deploymentId}`);
1680
+ return typeof d.status === "string" ? { status: d.status, failureReason: d.failureReason ?? null } : null;
1681
+ } catch {
1682
+ return null;
1683
+ }
1684
+ }
1685
+ async function deploy(r, options) {
1686
+ const { io: io2 } = r;
1687
+ const started = Date.now();
1688
+ const dir = resolve2(io2.cwd, options.dir ?? ".");
1689
+ const s = r.s;
1690
+ const api = await connect(io2);
1691
+ const link = await readLink(dir);
1692
+ const { files, skipped, secrets } = await collectFiles(dir);
1693
+ if (files.length === 0) throw new CliError(`No files to deploy in ${dir}.`);
1694
+ for (const secret of secrets)
1695
+ r.say(
1696
+ `${s.dim("\u2013")} Left out ${secret}: it looks like a secret. Set it with environment variables instead.
1697
+ `
1698
+ );
1699
+ if (skipped.length > 0)
1700
+ r.say(
1701
+ s.dim(
1702
+ `Skipped ${skipped.slice(0, 5).join(", ")}${skipped.length > 5 ? ` and ${skipped.length - 5} more` : ""}
1703
+ `
1704
+ )
1705
+ );
1706
+ const total = files.reduce((sum, f) => sum + f.size, 0);
1707
+ const { missing } = await api.request("/api/v1/uploads", {
1708
+ json: { hashes: [...new Set(files.map((f) => f.hash))] }
1709
+ });
1710
+ const needed = new Set(missing);
1711
+ const toUpload = files.filter((f) => {
1712
+ if (!needed.has(f.hash)) return false;
1713
+ needed.delete(f.hash);
1714
+ return true;
1715
+ });
1716
+ const uploadBytes = toUpload.reduce((sum, f) => sum + f.size, 0);
1717
+ r.say(
1718
+ `Uploading ${toUpload.length} of ${files.length} files (${formatBytes(uploadBytes)} of ${formatBytes(total)}; the rest are already on HostEdge)
1719
+ `
1720
+ );
1721
+ let uploaded = 0;
1722
+ let reported = 0;
1723
+ const step = Math.max(1, Math.ceil(toUpload.length / 10));
1724
+ await pool(toUpload, UPLOAD_CONCURRENCY, async (file) => {
1725
+ const bytes = await readFile4(file.absolute);
1726
+ await withRetries(
1727
+ () => api.raw(`/api/v1/uploads/${file.hash}`, {
1728
+ method: "PUT",
1729
+ headers: { "content-type": "application/octet-stream" },
1730
+ body: bytes,
1731
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
1732
+ }),
1733
+ temporary
1734
+ );
1735
+ uploaded++;
1736
+ if (toUpload.length > 20 && uploaded - reported >= step && uploaded < toUpload.length) {
1737
+ reported = uploaded;
1738
+ r.say(`${s.dim(` ${uploaded}/${toUpload.length} uploaded`)}
1739
+ `);
1740
+ }
1741
+ });
1742
+ let project;
1743
+ if (!link) {
1744
+ let pkg = null;
1745
+ try {
1746
+ pkg = await readFile4(join4(dir, "package.json"), "utf8");
1747
+ } catch {
1748
+ }
1749
+ project = {
1750
+ name: basename(dir),
1751
+ settings: detectFramework(
1752
+ files.map((f) => f.path),
1753
+ pkg
1754
+ ).settings
1755
+ };
1756
+ }
1757
+ const environment = options.preview ? "preview" : "production";
1758
+ const created = await api.request("/api/v1/deployments", {
1759
+ json: {
1760
+ projectId: link?.projectId,
1761
+ project,
1762
+ files: files.map((f) => ({ path: f.path, hash: f.hash, size: f.size })),
1763
+ environment,
1764
+ commitMessage: options.message ?? null
1765
+ }
1766
+ });
1767
+ if (!link) {
1768
+ const me = await api.request("/api/v1/me");
1769
+ await writeLink(dir, {
1770
+ projectId: created.projectId,
1771
+ slug: created.projectSlug ?? created.projectId,
1772
+ workspace: me.workspace.slug
1773
+ });
1774
+ await ensureGitignored(dir, ".hostedge");
1775
+ r.say(`${s.green("\u2713")} Created project ${s.bold(created.projectSlug ?? created.projectId)}
1776
+ `);
1777
+ }
1778
+ r.say(`Deployment ${s.bold(created.deploymentId)}
1779
+ `);
1780
+ const describe = (outcome) => ({
1781
+ deploymentId: created.deploymentId,
1782
+ status: outcome.status,
1783
+ url: created.url,
1784
+ projectId: created.projectId,
1785
+ environment,
1786
+ failureReason: outcome.failureReason
1787
+ });
1788
+ if (options.wait === false) {
1789
+ r.result(
1790
+ describe({ status: created.status, failureReason: null }),
1791
+ `Queued. Follow it at ${created.url}
1792
+ `,
1793
+ `${created.url}
1794
+ `
1795
+ );
1796
+ return EXIT.ok;
1797
+ }
1798
+ let result = await follow(r, api, created.deploymentId);
1799
+ if (!FINISHED.has(result.status)) result = await lookup(api, created.deploymentId) ?? result;
1800
+ const seconds = ((Date.now() - started) / 1e3).toFixed(1);
1801
+ if (result.status === "ready") {
1802
+ r.result(
1803
+ describe(result),
1804
+ `${s.green("\u2713")} Live in ${seconds}s: ${s.bold(created.url)}
1805
+ `,
1806
+ `${created.url}
1807
+ `
1808
+ );
1809
+ return EXIT.ok;
1810
+ }
1811
+ const canceled = result.status === "canceled";
1812
+ const failed = result.status === "failed";
1813
+ const reason = result.failureReason ?? (canceled ? "Deployment canceled." : failed ? null : `The log stream ended while the deployment was ${result.status}. Check it at ${created.url}.`);
1814
+ const human = canceled ? "Deployment canceled.\n" : `${s.red("\u2717")} Deployment ${failed ? "failed" : `stopped while ${result.status}`}${reason ? `: ${reason}` : ""}
1815
+ `;
1816
+ if (r.json) r.io.stdout(`${JSON.stringify(describe({ ...result, failureReason: reason }))}
1817
+ `);
1818
+ io2.stderr(human);
1819
+ return failed || canceled ? EXIT.deployFailed : EXIT.failure;
1820
+ }
1821
+
1822
+ // src/commands/env.ts
1823
+ import { writeFile as writeFile3 } from "node:fs/promises";
1824
+ import { resolve as resolve4 } from "node:path";
1825
+
1826
+ // src/commands/logs.ts
1827
+ import { resolve as resolve3 } from "node:path";
1828
+ var POLL_MS = 2e3;
1829
+ var LEVELS = ["debug", "info", "warn", "error"];
1830
+ var UNIT_MS = { m: 6e4, h: 36e5, d: 864e5 };
1831
+ var RANGES = [
1832
+ { name: "1h", ms: UNIT_MS.h },
1833
+ { name: "24h", ms: 24 * UNIT_MS.h },
1834
+ { name: "7d", ms: 7 * UNIT_MS.d }
1835
+ ];
1836
+ async function requireLink(io2, dir = ".") {
1837
+ const link = await readLink(resolve3(io2.cwd, dir));
1838
+ if (!link)
1839
+ throw new CliError(
1840
+ "This folder isn\u2019t linked to a project. Run `hostedge init` or `hostedge deploy`.",
1841
+ EXIT.notFound
1842
+ );
1843
+ return link;
1844
+ }
1845
+ function parseSince(text) {
1846
+ const match = /^(\d+)([mhd])$/.exec(text.trim());
1847
+ const ms = match ? Number(match[1]) * UNIT_MS[match[2]] : 0;
1848
+ if (!match || ms <= 0)
1849
+ throw new CliError(
1850
+ `Unknown --since \u201C${text}\u201D. Use a duration like 30m, 1h, 24h or 7d (at most 7d).`,
1851
+ EXIT.usage
1852
+ );
1853
+ const range = RANGES.find((r) => ms <= r.ms);
1854
+ if (!range) throw new CliError(`--since can look back 7 days at most, not \u201C${text}\u201D.`, EXIT.usage);
1855
+ return { range: range.name, windowMs: ms === range.ms ? null : ms };
1856
+ }
1857
+ var newer = (a, b) => a.ts > b.ts || a.ts === b.ts && a.id > b.id;
1858
+ async function logs(r, options) {
1859
+ const { io: io2 } = r;
1860
+ if (options.level && !LEVELS.includes(options.level))
1861
+ throw new CliError(`Unknown --level \u201C${options.level}\u201D. Use ${LEVELS.join(", ")}.`, EXIT.usage);
1862
+ const since = options.since ? parseSince(options.since) : null;
1863
+ const link = await requireLink(io2);
1864
+ const api = await connect(io2);
1865
+ const s = r.s;
1866
+ const query = new URLSearchParams();
1867
+ if (options.level) query.set("level", options.level);
1868
+ if (options.search) query.set("q", options.search);
1869
+ query.set("range", since?.range ?? (options.follow ? "1h" : "24h"));
1870
+ const cutoff = since?.windowMs ? Date.now() - since.windowMs : 0;
1871
+ const print = (row) => {
1872
+ const time = new Date(row.ts).toISOString();
1873
+ const level = row.level.toUpperCase().padEnd(5);
1874
+ const colored = row.level === "error" ? s.red(level) : row.level === "warn" ? s.yellow(level) : s.dim(level);
1875
+ r.line(
1876
+ { id: row.id, timestamp: time, level: row.level, type: row.type, message: row.message },
1877
+ `${s.dim(time.replace("T", " ").slice(0, 19))} ${colored} ${row.message}
1878
+ `
1879
+ );
1880
+ };
1881
+ const fetchRows = async () => (await api.request(
1882
+ `/api/v1/projects/${link.projectId}/logs?${query.toString()}`
1883
+ )).rows.filter((row) => row.ts >= cutoff);
1884
+ const first = await fetchRows();
1885
+ for (const row of [...first].reverse()) print(row);
1886
+ if (!options.follow) {
1887
+ if (first.length === 0) r.say(s.dim("No logs in this range.\n"));
1888
+ return EXIT.ok;
1889
+ }
1890
+ let cursor = first[0] ? { ts: first[0].ts, id: first[0].id } : { ts: 0, id: "" };
1891
+ while (!options.signal?.aborted) {
1892
+ await io2.sleep(POLL_MS);
1893
+ if (options.signal?.aborted) break;
1894
+ const rows = (await fetchRows()).filter((row) => newer(row, cursor)).reverse();
1895
+ for (const row of rows) print(row);
1896
+ const last = rows.at(-1);
1897
+ if (last) cursor = { ts: last.ts, id: last.id };
1898
+ }
1899
+ return EXIT.ok;
1900
+ }
1901
+
1902
+ // src/commands/env.ts
1903
+ async function envPull(r, options) {
1904
+ const { io: io2 } = r;
1905
+ const link = await requireLink(io2);
1906
+ const api = await connect(io2);
1907
+ const environment = options.environment ?? "development";
1908
+ const file = options.file ?? ".env.local";
1909
+ const response = await api.raw(
1910
+ `/api/v1/projects/${link.projectId}/env?${new URLSearchParams({ environment, format: "dotenv" })}`
1911
+ );
1912
+ const text = await response.text();
1913
+ const path = resolve4(io2.cwd, file);
1914
+ await writeFile3(path, text, { mode: 384 });
1915
+ const count2 = text.split("\n").filter(Boolean).length;
1916
+ const ignored = await ensureGitignored(io2.cwd, file);
1917
+ const s = r.s;
1918
+ r.result(
1919
+ { environment, file, count: count2, gitignored: ignored },
1920
+ `${s.green("\u2713")} Wrote ${count2} ${environment} variable${count2 === 1 ? "" : "s"} to ${file}${ignored ? ` (added to .gitignore)` : ""}.
1921
+ `
1922
+ );
1923
+ return EXIT.ok;
1924
+ }
1925
+
1926
+ // src/commands/init.ts
1927
+ import { readFile as readFile5 } from "node:fs/promises";
1928
+ import { basename as basename2, join as join5, resolve as resolve5 } from "node:path";
1929
+ async function init(r, options) {
1930
+ const { io: io2 } = r;
1931
+ const dir = resolve5(io2.cwd, options.dir ?? ".");
1932
+ const s = r.s;
1933
+ const existing = await readLink(dir);
1934
+ let kit = null;
1935
+ if (options.ai) {
1936
+ kit = await writeKit(dir, { force: options.force });
1937
+ r.say(formatKit(r, kit));
1938
+ }
1939
+ const files = kit ? { dir, files: kit } : void 0;
1940
+ const relink = options.force && (!options.ai || options.project || options.name);
1941
+ if (existing && !relink) {
1942
+ r.result(
1943
+ {
1944
+ dir,
1945
+ linked: true,
1946
+ alreadyLinked: true,
1947
+ project: { id: existing.projectId, slug: existing.slug, workspace: existing.workspace },
1948
+ ai: files
1949
+ },
1950
+ `Already linked to ${s.bold(existing.slug)}. Use --force to link a different project.
1951
+ `,
1952
+ ""
1953
+ );
1954
+ return EXIT.ok;
1955
+ }
1956
+ if (options.ai && !(await credentials(io2)).token) {
1957
+ r.result(
1958
+ { dir, linked: false, linkSkipped: "not signed in", ai: files },
1959
+ `${s.dim("Not signed in, so the folder is not linked yet. Run `hostedge login` then `hostedge init`.")}
1960
+ `
1961
+ );
1962
+ return EXIT.ok;
1963
+ }
1964
+ const api = await connect(io2);
1965
+ const me = await api.request("/api/v1/me");
1966
+ let project;
1967
+ let framework;
1968
+ if (options.project) {
1969
+ const { projects } = await api.request("/api/v1/projects");
1970
+ const found = projects.find((p) => p.id === options.project || p.slug === options.project);
1971
+ if (!found)
1972
+ throw new CliError(
1973
+ `No project \u201C${options.project}\u201D in ${me.workspace.slug}. Run \`hostedge init\` without --project to create one.`,
1974
+ EXIT.notFound
1975
+ );
1976
+ project = found;
1977
+ framework = found.framework ?? null;
1978
+ } else {
1979
+ const { files: found } = await collectFiles(dir, { maxFiles: 1e5, hash: false });
1980
+ let pkg = null;
1981
+ try {
1982
+ pkg = await readFile5(join5(dir, "package.json"), "utf8");
1983
+ } catch {
1984
+ }
1985
+ const detection = detectFramework(
1986
+ found.map((f) => f.path),
1987
+ pkg
1988
+ );
1989
+ const settings = detection.settings;
1990
+ framework = settings.framework;
1991
+ r.say(`Detected ${s.bold(FRAMEWORK_INFO[settings.framework].name)}: ${detection.reason}
1992
+ `);
1993
+ if (detection.problem) r.say(`${s.yellow("!")} ${detection.problem}
1994
+ `);
1995
+ project = await api.request("/api/v1/projects", {
1996
+ json: { name: options.name ?? basename2(dir), settings }
1997
+ });
1998
+ }
1999
+ await writeLink(dir, { projectId: project.id, slug: project.slug, workspace: me.workspace.slug });
2000
+ await ensureGitignored(dir, ".hostedge");
2001
+ r.result(
2002
+ {
2003
+ dir,
2004
+ linked: true,
2005
+ alreadyLinked: false,
2006
+ project: { id: project.id, slug: project.slug, workspace: me.workspace.slug },
2007
+ framework,
2008
+ ai: files
2009
+ },
2010
+ `${s.green("\u2713")} Linked ${s.bold(project.slug)}. Run ${s.bold("hostedge deploy")} to publish it.
2011
+ `
2012
+ );
2013
+ return EXIT.ok;
2014
+ }
2015
+
2016
+ // src/commands/share.ts
2017
+ import { spawn as nodeSpawn } from "node:child_process";
2018
+ import { chmod as chmod2, mkdir as mkdir3, rename, rm as rm2, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
2019
+ import { connect as netConnect } from "node:net";
2020
+ import { homedir as homedir2 } from "node:os";
2021
+ import { delimiter, join as join6 } from "node:path";
2022
+
2023
+ // ../../node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
2024
+ var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
2025
+ QrCodeDataType2[QrCodeDataType2["Border"] = -1] = "Border";
2026
+ QrCodeDataType2[QrCodeDataType2["Data"] = 0] = "Data";
2027
+ QrCodeDataType2[QrCodeDataType2["Function"] = 1] = "Function";
2028
+ QrCodeDataType2[QrCodeDataType2["Position"] = 2] = "Position";
2029
+ QrCodeDataType2[QrCodeDataType2["Timing"] = 3] = "Timing";
2030
+ QrCodeDataType2[QrCodeDataType2["Alignment"] = 4] = "Alignment";
2031
+ return QrCodeDataType2;
2032
+ })(QrCodeDataType || {});
2033
+ var LOW = [0, 1];
2034
+ var MEDIUM = [1, 0];
2035
+ var QUARTILE = [2, 3];
2036
+ var HIGH = [3, 2];
2037
+ var EccMap = {
2038
+ L: LOW,
2039
+ M: MEDIUM,
2040
+ Q: QUARTILE,
2041
+ H: HIGH
2042
+ };
2043
+ var NUMERIC_REGEX = /^\d*$/;
2044
+ var ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+./:-]*$/;
2045
+ var ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
2046
+ var MIN_VERSION = 1;
2047
+ var MAX_VERSION = 40;
2048
+ var PENALTY_N1 = 3;
2049
+ var PENALTY_N2 = 3;
2050
+ var PENALTY_N3 = 40;
2051
+ var PENALTY_N4 = 10;
2052
+ var ECC_CODEWORDS_PER_BLOCK = [
2053
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
2054
+ // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
2055
+ [-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
2056
+ // Low
2057
+ [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
2058
+ // Medium
2059
+ [-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
2060
+ // Quartile
2061
+ [-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
2062
+ // High
2063
+ ];
2064
+ var NUM_ERROR_CORRECTION_BLOCKS = [
2065
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
2066
+ // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
2067
+ [-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
2068
+ // Low
2069
+ [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
2070
+ // Medium
2071
+ [-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
2072
+ // Quartile
2073
+ [-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]
2074
+ // High
2075
+ ];
2076
+ var QrCode = class {
2077
+ /* -- Constructor (low level) and fields -- */
2078
+ // Creates a new QR Code with the given version number,
2079
+ // error correction level, data codeword bytes, and mask number.
2080
+ // This is a low-level API that most users should not use directly.
2081
+ // A mid-level API is the encodeSegments() function.
2082
+ constructor(version, ecc, dataCodewords, msk) {
2083
+ this.version = version;
2084
+ this.ecc = ecc;
2085
+ if (version < MIN_VERSION || version > MAX_VERSION)
2086
+ throw new RangeError("Version value out of range");
2087
+ if (msk < -1 || msk > 7)
2088
+ throw new RangeError("Mask value out of range");
2089
+ this.size = version * 4 + 17;
2090
+ const row = Array.from({ length: this.size }).fill(false);
2091
+ for (let i = 0; i < this.size; i++) {
2092
+ this.modules.push(row.slice());
2093
+ this.types.push(row.map(() => 0));
2094
+ }
2095
+ this.drawFunctionPatterns();
2096
+ const allCodewords = this.addEccAndInterleave(dataCodewords);
2097
+ this.drawCodewords(allCodewords);
2098
+ if (msk === -1) {
2099
+ let minPenalty = 1e9;
2100
+ for (let i = 0; i < 8; i++) {
2101
+ this.applyMask(i);
2102
+ this.drawFormatBits(i);
2103
+ const penalty = this.getPenaltyScore();
2104
+ if (penalty < minPenalty) {
2105
+ msk = i;
2106
+ minPenalty = penalty;
2107
+ }
2108
+ this.applyMask(i);
2109
+ }
2110
+ }
2111
+ this.mask = msk;
2112
+ this.applyMask(msk);
2113
+ this.drawFormatBits(msk);
2114
+ }
2115
+ /* -- Fields -- */
2116
+ // The width and height of this QR Code, measured in modules, between
2117
+ // 21 and 177 (inclusive). This is equal to version * 4 + 17.
2118
+ size;
2119
+ // The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
2120
+ // Even if a QR Code is created with automatic masking requested (mask = -1),
2121
+ // the resulting object still has a mask value between 0 and 7.
2122
+ mask;
2123
+ // The modules of this QR Code (false = light, true = dark).
2124
+ // Immutable after constructor finishes. Accessed through getModule().
2125
+ modules = [];
2126
+ types = [];
2127
+ /* -- Accessor methods -- */
2128
+ // Returns the color of the module (pixel) at the given coordinates, which is false
2129
+ // for light or true for dark. The top left corner has the coordinates (x=0, y=0).
2130
+ // If the given coordinates are out of bounds, then false (light) is returned.
2131
+ getModule(x, y) {
2132
+ return x >= 0 && x < this.size && y >= 0 && y < this.size && this.modules[y][x];
2133
+ }
2134
+ /* -- Private helper methods for constructor: Drawing function modules -- */
2135
+ // Reads this object's version field, and draws and marks all function modules.
2136
+ drawFunctionPatterns() {
2137
+ for (let i = 0; i < this.size; i++) {
2138
+ this.setFunctionModule(6, i, i % 2 === 0, QrCodeDataType.Timing);
2139
+ this.setFunctionModule(i, 6, i % 2 === 0, QrCodeDataType.Timing);
2140
+ }
2141
+ this.drawFinderPattern(3, 3);
2142
+ this.drawFinderPattern(this.size - 4, 3);
2143
+ this.drawFinderPattern(3, this.size - 4);
2144
+ const alignPatPos = this.getAlignmentPatternPositions();
2145
+ const numAlign = alignPatPos.length;
2146
+ for (let i = 0; i < numAlign; i++) {
2147
+ for (let j = 0; j < numAlign; j++) {
2148
+ if (!(i === 0 && j === 0 || i === 0 && j === numAlign - 1 || i === numAlign - 1 && j === 0))
2149
+ this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
2150
+ }
2151
+ }
2152
+ this.drawFormatBits(0);
2153
+ this.drawVersion();
2154
+ }
2155
+ // Draws two copies of the format bits (with its own error correction code)
2156
+ // based on the given mask and this object's error correction level field.
2157
+ drawFormatBits(mask) {
2158
+ const data = this.ecc[1] << 3 | mask;
2159
+ let rem = data;
2160
+ for (let i = 0; i < 10; i++)
2161
+ rem = rem << 1 ^ (rem >>> 9) * 1335;
2162
+ const bits = (data << 10 | rem) ^ 21522;
2163
+ for (let i = 0; i <= 5; i++)
2164
+ this.setFunctionModule(8, i, getBit(bits, i));
2165
+ this.setFunctionModule(8, 7, getBit(bits, 6));
2166
+ this.setFunctionModule(8, 8, getBit(bits, 7));
2167
+ this.setFunctionModule(7, 8, getBit(bits, 8));
2168
+ for (let i = 9; i < 15; i++)
2169
+ this.setFunctionModule(14 - i, 8, getBit(bits, i));
2170
+ for (let i = 0; i < 8; i++)
2171
+ this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
2172
+ for (let i = 8; i < 15; i++)
2173
+ this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
2174
+ this.setFunctionModule(8, this.size - 8, true);
2175
+ }
2176
+ // Draws two copies of the version bits (with its own error correction code),
2177
+ // based on this object's version field, iff 7 <= version <= 40.
2178
+ drawVersion() {
2179
+ if (this.version < 7)
2180
+ return;
2181
+ let rem = this.version;
2182
+ for (let i = 0; i < 12; i++)
2183
+ rem = rem << 1 ^ (rem >>> 11) * 7973;
2184
+ const bits = this.version << 12 | rem;
2185
+ for (let i = 0; i < 18; i++) {
2186
+ const color = getBit(bits, i);
2187
+ const a = this.size - 11 + i % 3;
2188
+ const b = Math.floor(i / 3);
2189
+ this.setFunctionModule(a, b, color);
2190
+ this.setFunctionModule(b, a, color);
2191
+ }
2192
+ }
2193
+ // Draws a 9*9 finder pattern including the border separator,
2194
+ // with the center module at (x, y). Modules can be out of bounds.
2195
+ drawFinderPattern(x, y) {
2196
+ for (let dy = -4; dy <= 4; dy++) {
2197
+ for (let dx = -4; dx <= 4; dx++) {
2198
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
2199
+ const xx = x + dx;
2200
+ const yy = y + dy;
2201
+ if (xx >= 0 && xx < this.size && yy >= 0 && yy < this.size)
2202
+ this.setFunctionModule(xx, yy, dist !== 2 && dist !== 4, QrCodeDataType.Position);
2203
+ }
2204
+ }
2205
+ }
2206
+ // Draws a 5*5 alignment pattern, with the center module
2207
+ // at (x, y). All modules must be in bounds.
2208
+ drawAlignmentPattern(x, y) {
2209
+ for (let dy = -2; dy <= 2; dy++) {
2210
+ for (let dx = -2; dx <= 2; dx++) {
2211
+ this.setFunctionModule(
2212
+ x + dx,
2213
+ y + dy,
2214
+ Math.max(Math.abs(dx), Math.abs(dy)) !== 1,
2215
+ QrCodeDataType.Alignment
2216
+ );
2217
+ }
2218
+ }
2219
+ }
2220
+ // Sets the color of a module and marks it as a function module.
2221
+ // Only used by the constructor. Coordinates must be in bounds.
2222
+ setFunctionModule(x, y, isDark, type = QrCodeDataType.Function) {
2223
+ this.modules[y][x] = isDark;
2224
+ this.types[y][x] = type;
2225
+ }
2226
+ /* -- Private helper methods for constructor: Codewords and masking -- */
2227
+ // Returns a new byte string representing the given data with the appropriate error correction
2228
+ // codewords appended to it, based on this object's version and error correction level.
2229
+ addEccAndInterleave(data) {
2230
+ const ver = this.version;
2231
+ const ecl = this.ecc;
2232
+ if (data.length !== getNumDataCodewords(ver, ecl))
2233
+ throw new RangeError("Invalid argument");
2234
+ const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
2235
+ const blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver];
2236
+ const rawCodewords = Math.floor(getNumRawDataModules(ver) / 8);
2237
+ const numShortBlocks = numBlocks - rawCodewords % numBlocks;
2238
+ const shortBlockLen = Math.floor(rawCodewords / numBlocks);
2239
+ const blocks = [];
2240
+ const rsDiv = reedSolomonComputeDivisor(blockEccLen);
2241
+ for (let i = 0, k = 0; i < numBlocks; i++) {
2242
+ const dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
2243
+ k += dat.length;
2244
+ const ecc = reedSolomonComputeRemainder(dat, rsDiv);
2245
+ if (i < numShortBlocks)
2246
+ dat.push(0);
2247
+ blocks.push(dat.concat(ecc));
2248
+ }
2249
+ const result = [];
2250
+ for (let i = 0; i < blocks[0].length; i++) {
2251
+ blocks.forEach((block, j) => {
2252
+ if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks)
2253
+ result.push(block[i]);
2254
+ });
2255
+ }
2256
+ return result;
2257
+ }
2258
+ // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
2259
+ // data area of this QR Code. Function modules need to be marked off before this is called.
2260
+ drawCodewords(data) {
2261
+ if (data.length !== Math.floor(getNumRawDataModules(this.version) / 8))
2262
+ throw new RangeError("Invalid argument");
2263
+ let i = 0;
2264
+ for (let right = this.size - 1; right >= 1; right -= 2) {
2265
+ if (right === 6)
2266
+ right = 5;
2267
+ for (let vert = 0; vert < this.size; vert++) {
2268
+ for (let j = 0; j < 2; j++) {
2269
+ const x = right - j;
2270
+ const upward = (right + 1 & 2) === 0;
2271
+ const y = upward ? this.size - 1 - vert : vert;
2272
+ if (!this.types[y][x] && i < data.length * 8) {
2273
+ this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
2274
+ i++;
2275
+ }
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2280
+ // XORs the codeword modules in this QR Code with the given mask pattern.
2281
+ // The function modules must be marked and the codeword bits must be drawn
2282
+ // before masking. Due to the arithmetic of XOR, calling applyMask() with
2283
+ // the same mask value a second time will undo the mask. A final well-formed
2284
+ // QR Code needs exactly one (not zero, two, etc.) mask applied.
2285
+ applyMask(mask) {
2286
+ if (mask < 0 || mask > 7)
2287
+ throw new RangeError("Mask value out of range");
2288
+ for (let y = 0; y < this.size; y++) {
2289
+ for (let x = 0; x < this.size; x++) {
2290
+ let invert;
2291
+ switch (mask) {
2292
+ case 0:
2293
+ invert = (x + y) % 2 === 0;
2294
+ break;
2295
+ case 1:
2296
+ invert = y % 2 === 0;
2297
+ break;
2298
+ case 2:
2299
+ invert = x % 3 === 0;
2300
+ break;
2301
+ case 3:
2302
+ invert = (x + y) % 3 === 0;
2303
+ break;
2304
+ case 4:
2305
+ invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
2306
+ break;
2307
+ case 5:
2308
+ invert = x * y % 2 + x * y % 3 === 0;
2309
+ break;
2310
+ case 6:
2311
+ invert = (x * y % 2 + x * y % 3) % 2 === 0;
2312
+ break;
2313
+ case 7:
2314
+ invert = ((x + y) % 2 + x * y % 3) % 2 === 0;
2315
+ break;
2316
+ default:
2317
+ throw new Error("Unreachable");
2318
+ }
2319
+ if (!this.types[y][x] && invert)
2320
+ this.modules[y][x] = !this.modules[y][x];
2321
+ }
2322
+ }
2323
+ }
2324
+ // Calculates and returns the penalty score based on state of this QR Code's current modules.
2325
+ // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
2326
+ getPenaltyScore() {
2327
+ let result = 0;
2328
+ for (let y = 0; y < this.size; y++) {
2329
+ let runColor = false;
2330
+ let runX = 0;
2331
+ const runHistory = [0, 0, 0, 0, 0, 0, 0];
2332
+ for (let x = 0; x < this.size; x++) {
2333
+ if (this.modules[y][x] === runColor) {
2334
+ runX++;
2335
+ if (runX === 5)
2336
+ result += PENALTY_N1;
2337
+ else if (runX > 5)
2338
+ result++;
2339
+ } else {
2340
+ this.finderPenaltyAddHistory(runX, runHistory);
2341
+ if (!runColor)
2342
+ result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
2343
+ runColor = this.modules[y][x];
2344
+ runX = 1;
2345
+ }
2346
+ }
2347
+ result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3;
2348
+ }
2349
+ for (let x = 0; x < this.size; x++) {
2350
+ let runColor = false;
2351
+ let runY = 0;
2352
+ const runHistory = [0, 0, 0, 0, 0, 0, 0];
2353
+ for (let y = 0; y < this.size; y++) {
2354
+ if (this.modules[y][x] === runColor) {
2355
+ runY++;
2356
+ if (runY === 5)
2357
+ result += PENALTY_N1;
2358
+ else if (runY > 5)
2359
+ result++;
2360
+ } else {
2361
+ this.finderPenaltyAddHistory(runY, runHistory);
2362
+ if (!runColor)
2363
+ result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
2364
+ runColor = this.modules[y][x];
2365
+ runY = 1;
2366
+ }
2367
+ }
2368
+ result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3;
2369
+ }
2370
+ for (let y = 0; y < this.size - 1; y++) {
2371
+ for (let x = 0; x < this.size - 1; x++) {
2372
+ const color = this.modules[y][x];
2373
+ if (color === this.modules[y][x + 1] && color === this.modules[y + 1][x] && color === this.modules[y + 1][x + 1]) {
2374
+ result += PENALTY_N2;
2375
+ }
2376
+ }
2377
+ }
2378
+ let dark = 0;
2379
+ for (const row of this.modules)
2380
+ dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
2381
+ const total = this.size * this.size;
2382
+ const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
2383
+ result += k * PENALTY_N4;
2384
+ return result;
2385
+ }
2386
+ /* -- Private helper functions -- */
2387
+ // Returns an ascending list of positions of alignment patterns for this version number.
2388
+ // Each position is in the range [0,177), and are used on both the x and y axes.
2389
+ // This could be implemented as lookup table of 40 variable-length lists of integers.
2390
+ getAlignmentPatternPositions() {
2391
+ if (this.version === 1) {
2392
+ return [];
2393
+ } else {
2394
+ const numAlign = Math.floor(this.version / 7) + 2;
2395
+ const step = this.version === 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
2396
+ const result = [6];
2397
+ for (let pos = this.size - 7; result.length < numAlign; pos -= step)
2398
+ result.splice(1, 0, pos);
2399
+ return result;
2400
+ }
2401
+ }
2402
+ // Can only be called immediately after a light run is added, and
2403
+ // returns either 0, 1, or 2. A helper function for getPenaltyScore().
2404
+ finderPenaltyCountPatterns(runHistory) {
2405
+ const n = runHistory[1];
2406
+ const core = n > 0 && runHistory[2] === n && runHistory[3] === n * 3 && runHistory[4] === n && runHistory[5] === n;
2407
+ return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
2408
+ }
2409
+ // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
2410
+ finderPenaltyTerminateAndCount(currentRunColor, currentRunLength, runHistory) {
2411
+ if (currentRunColor) {
2412
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
2413
+ currentRunLength = 0;
2414
+ }
2415
+ currentRunLength += this.size;
2416
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
2417
+ return this.finderPenaltyCountPatterns(runHistory);
2418
+ }
2419
+ // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
2420
+ finderPenaltyAddHistory(currentRunLength, runHistory) {
2421
+ if (runHistory[0] === 0)
2422
+ currentRunLength += this.size;
2423
+ runHistory.pop();
2424
+ runHistory.unshift(currentRunLength);
2425
+ }
2426
+ };
2427
+ function appendBits(val, len, bb) {
2428
+ if (len < 0 || len > 31 || val >>> len !== 0)
2429
+ throw new RangeError("Value out of range");
2430
+ for (let i = len - 1; i >= 0; i--)
2431
+ bb.push(val >>> i & 1);
2432
+ }
2433
+ function getBit(x, i) {
2434
+ return (x >>> i & 1) !== 0;
2435
+ }
2436
+ var QrSegment = class {
2437
+ // Creates a new QR Code segment with the given attributes and data.
2438
+ // The character count (numChars) must agree with the mode and the bit buffer length,
2439
+ // but the constraint isn't checked. The given bit buffer is cloned and stored.
2440
+ constructor(mode, numChars, bitData) {
2441
+ this.mode = mode;
2442
+ this.numChars = numChars;
2443
+ this.bitData = bitData;
2444
+ if (numChars < 0)
2445
+ throw new RangeError("Invalid argument");
2446
+ this.bitData = bitData.slice();
2447
+ }
2448
+ /* -- Methods -- */
2449
+ // Returns a new copy of the data bits of this segment.
2450
+ getData() {
2451
+ return this.bitData.slice();
2452
+ }
2453
+ };
2454
+ var MODE_NUMERIC = [1, 10, 12, 14];
2455
+ var MODE_ALPHANUMERIC = [2, 9, 11, 13];
2456
+ var MODE_BYTE = [4, 8, 16, 16];
2457
+ function numCharCountBits(mode, ver) {
2458
+ return mode[Math.floor((ver + 7) / 17) + 1];
2459
+ }
2460
+ function makeBytes(data) {
2461
+ const bb = [];
2462
+ for (const b of data)
2463
+ appendBits(b, 8, bb);
2464
+ return new QrSegment(MODE_BYTE, data.length, bb);
2465
+ }
2466
+ function makeNumeric(digits) {
2467
+ if (!isNumeric(digits))
2468
+ throw new RangeError("String contains non-numeric characters");
2469
+ const bb = [];
2470
+ for (let i = 0; i < digits.length; ) {
2471
+ const n = Math.min(digits.length - i, 3);
2472
+ appendBits(Number.parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
2473
+ i += n;
2474
+ }
2475
+ return new QrSegment(MODE_NUMERIC, digits.length, bb);
2476
+ }
2477
+ function makeAlphanumeric(text) {
2478
+ if (!isAlphanumeric(text))
2479
+ throw new RangeError("String contains unencodable characters in alphanumeric mode");
2480
+ const bb = [];
2481
+ let i;
2482
+ for (i = 0; i + 2 <= text.length; i += 2) {
2483
+ let temp = ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
2484
+ temp += ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
2485
+ appendBits(temp, 11, bb);
2486
+ }
2487
+ if (i < text.length)
2488
+ appendBits(ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
2489
+ return new QrSegment(MODE_ALPHANUMERIC, text.length, bb);
2490
+ }
2491
+ function makeSegments(text) {
2492
+ if (text === "")
2493
+ return [];
2494
+ else if (isNumeric(text))
2495
+ return [makeNumeric(text)];
2496
+ else if (isAlphanumeric(text))
2497
+ return [makeAlphanumeric(text)];
2498
+ else
2499
+ return [makeBytes(toUtf8ByteArray(text))];
2500
+ }
2501
+ function isNumeric(text) {
2502
+ return NUMERIC_REGEX.test(text);
2503
+ }
2504
+ function isAlphanumeric(text) {
2505
+ return ALPHANUMERIC_REGEX.test(text);
2506
+ }
2507
+ function getTotalBits(segs, version) {
2508
+ let result = 0;
2509
+ for (const seg of segs) {
2510
+ const ccbits = numCharCountBits(seg.mode, version);
2511
+ if (seg.numChars >= 1 << ccbits)
2512
+ return Number.POSITIVE_INFINITY;
2513
+ result += 4 + ccbits + seg.bitData.length;
2514
+ }
2515
+ return result;
2516
+ }
2517
+ function toUtf8ByteArray(str2) {
2518
+ str2 = encodeURI(str2);
2519
+ const result = [];
2520
+ for (let i = 0; i < str2.length; i++) {
2521
+ if (str2.charAt(i) !== "%") {
2522
+ result.push(str2.charCodeAt(i));
2523
+ } else {
2524
+ result.push(Number.parseInt(str2.substring(i + 1, i + 3), 16));
2525
+ i += 2;
2526
+ }
2527
+ }
2528
+ return result;
2529
+ }
2530
+ function getNumRawDataModules(ver) {
2531
+ if (ver < MIN_VERSION || ver > MAX_VERSION)
2532
+ throw new RangeError("Version number out of range");
2533
+ let result = (16 * ver + 128) * ver + 64;
2534
+ if (ver >= 2) {
2535
+ const numAlign = Math.floor(ver / 7) + 2;
2536
+ result -= (25 * numAlign - 10) * numAlign - 55;
2537
+ if (ver >= 7)
2538
+ result -= 36;
2539
+ }
2540
+ return result;
2541
+ }
2542
+ function getNumDataCodewords(ver, ecl) {
2543
+ return Math.floor(getNumRawDataModules(ver) / 8) - ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver] * NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
2544
+ }
2545
+ function reedSolomonComputeDivisor(degree) {
2546
+ if (degree < 1 || degree > 255)
2547
+ throw new RangeError("Degree out of range");
2548
+ const result = [];
2549
+ for (let i = 0; i < degree - 1; i++)
2550
+ result.push(0);
2551
+ result.push(1);
2552
+ let root = 1;
2553
+ for (let i = 0; i < degree; i++) {
2554
+ for (let j = 0; j < result.length; j++) {
2555
+ result[j] = reedSolomonMultiply(result[j], root);
2556
+ if (j + 1 < result.length)
2557
+ result[j] ^= result[j + 1];
2558
+ }
2559
+ root = reedSolomonMultiply(root, 2);
2560
+ }
2561
+ return result;
2562
+ }
2563
+ function reedSolomonComputeRemainder(data, divisor) {
2564
+ const result = divisor.map((_) => 0);
2565
+ for (const b of data) {
2566
+ const factor = b ^ result.shift();
2567
+ result.push(0);
2568
+ divisor.forEach((coef, i) => result[i] ^= reedSolomonMultiply(coef, factor));
2569
+ }
2570
+ return result;
2571
+ }
2572
+ function reedSolomonMultiply(x, y) {
2573
+ if (x >>> 8 !== 0 || y >>> 8 !== 0)
2574
+ throw new RangeError("Byte out of range");
2575
+ let z = 0;
2576
+ for (let i = 7; i >= 0; i--) {
2577
+ z = z << 1 ^ (z >>> 7) * 285;
2578
+ z ^= (y >>> i & 1) * x;
2579
+ }
2580
+ return z;
2581
+ }
2582
+ function encodeSegments(segs, ecl, minVersion = 1, maxVersion = 40, mask = -1, boostEcl = true) {
2583
+ if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) {
2584
+ throw new RangeError("Invalid value");
2585
+ }
2586
+ let version;
2587
+ let dataUsedBits;
2588
+ for (version = minVersion; ; version++) {
2589
+ const dataCapacityBits2 = getNumDataCodewords(version, ecl) * 8;
2590
+ const usedBits = getTotalBits(segs, version);
2591
+ if (usedBits <= dataCapacityBits2) {
2592
+ dataUsedBits = usedBits;
2593
+ break;
2594
+ }
2595
+ if (version >= maxVersion)
2596
+ throw new RangeError("Data too long");
2597
+ }
2598
+ for (const newEcl of [MEDIUM, QUARTILE, HIGH]) {
2599
+ if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8)
2600
+ ecl = newEcl;
2601
+ }
2602
+ const bb = [];
2603
+ for (const seg of segs) {
2604
+ appendBits(seg.mode[0], 4, bb);
2605
+ appendBits(seg.numChars, numCharCountBits(seg.mode, version), bb);
2606
+ for (const b of seg.getData())
2607
+ bb.push(b);
2608
+ }
2609
+ const dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
2610
+ appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
2611
+ appendBits(0, (8 - bb.length % 8) % 8, bb);
2612
+ for (let padByte = 236; bb.length < dataCapacityBits; padByte ^= 236 ^ 17)
2613
+ appendBits(padByte, 8, bb);
2614
+ const dataCodewords = Array.from({ length: Math.ceil(bb.length / 8) }, () => 0);
2615
+ bb.forEach((b, i) => dataCodewords[i >>> 3] |= b << 7 - (i & 7));
2616
+ return new QrCode(version, ecl, dataCodewords, mask);
2617
+ }
2618
+ function encode(data, options) {
2619
+ const {
2620
+ ecc = "L",
2621
+ boostEcc = false,
2622
+ minVersion = 1,
2623
+ maxVersion = 40,
2624
+ maskPattern = -1,
2625
+ border = 1
2626
+ } = options || {};
2627
+ const segment = typeof data === "string" ? makeSegments(data) : Array.isArray(data) ? [makeBytes(data)] : void 0;
2628
+ if (!segment)
2629
+ throw new Error(`uqr only supports encoding string and binary data, but got: ${typeof data}`);
2630
+ const qr = encodeSegments(
2631
+ segment,
2632
+ EccMap[ecc],
2633
+ minVersion,
2634
+ maxVersion,
2635
+ maskPattern,
2636
+ boostEcc
2637
+ );
2638
+ const result = addBorder({
2639
+ version: qr.version,
2640
+ maskPattern: qr.mask,
2641
+ size: qr.size,
2642
+ data: qr.modules,
2643
+ types: qr.types
2644
+ }, border);
2645
+ if (options?.invert)
2646
+ result.data = result.data.map((row) => row.map((mod) => !mod));
2647
+ options?.onEncoded?.(result);
2648
+ return result;
2649
+ }
2650
+ function addBorder(input, border = 1) {
2651
+ if (!border)
2652
+ return input;
2653
+ const { size } = input;
2654
+ const newSize = size + border * 2;
2655
+ input.size = newSize;
2656
+ input.data.forEach((row) => {
2657
+ for (let i = 0; i < border; i++) {
2658
+ row.unshift(false);
2659
+ row.push(false);
2660
+ }
2661
+ });
2662
+ for (let i = 0; i < border; i++) {
2663
+ input.data.unshift(Array.from({ length: newSize }, (_) => false));
2664
+ input.data.push(Array.from({ length: newSize }, (_) => false));
2665
+ }
2666
+ const b = QrCodeDataType.Border;
2667
+ input.types.forEach((row) => {
2668
+ for (let i = 0; i < border; i++) {
2669
+ row.unshift(b);
2670
+ row.push(b);
2671
+ }
2672
+ });
2673
+ for (let i = 0; i < border; i++) {
2674
+ input.types.unshift(Array.from({ length: newSize }, (_) => b));
2675
+ input.types.push(Array.from({ length: newSize }, (_) => b));
2676
+ }
2677
+ return input;
2678
+ }
2679
+ function getDataAt(data, x, y, defaults = false) {
2680
+ if (x < 0 || y < 0 || x >= data.length || y >= data.length)
2681
+ return defaults;
2682
+ return data[y][x];
2683
+ }
2684
+ function renderUnicodeCompact(data, options = {}) {
2685
+ const palette = {
2686
+ WHITE_ALL: "\u2588",
2687
+ WHITE_BLACK: "\u2580",
2688
+ BLACK_WHITE: "\u2584",
2689
+ BLACK_ALL: " "
2690
+ };
2691
+ const result = encode(data, options);
2692
+ const WHITE = false;
2693
+ const BLACK = true;
2694
+ const at = (x, y) => getDataAt(result.data, x, y, true);
2695
+ const lines = [];
2696
+ let line = "";
2697
+ for (let row = 0; row < result.size; row += 2) {
2698
+ for (let col = 0; col < result.size; col++) {
2699
+ if (at(col, row) === WHITE && at(col, row + 1) === WHITE)
2700
+ line += palette.WHITE_ALL;
2701
+ else if (at(col, row) === WHITE && at(col, row + 1) === BLACK)
2702
+ line += palette.WHITE_BLACK;
2703
+ else if (at(col, row) === BLACK && at(col, row + 1) === WHITE)
2704
+ line += palette.BLACK_WHITE;
2705
+ else
2706
+ line += palette.BLACK_ALL;
2707
+ }
2708
+ lines.push(line);
2709
+ line = "";
2710
+ }
2711
+ return lines.join("\n");
2712
+ }
2713
+
2714
+ // src/commands/share.ts
2715
+ var RELEASES = "https://github.com/cloudflare/cloudflared/releases/latest/download";
2716
+ function cloudflaredAsset(platform, arch) {
2717
+ const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : null;
2718
+ if (!cpu) return null;
2719
+ if (platform === "linux") return `cloudflared-linux-${cpu}`;
2720
+ if (platform === "darwin") return `cloudflared-darwin-${cpu}.tgz`;
2721
+ if (platform === "win32" && cpu === "amd64") return "cloudflared-windows-amd64.exe";
2722
+ return null;
2723
+ }
2724
+ var exists = async (path) => (await stat2(path).catch(() => null))?.isFile() ?? false;
2725
+ async function findCloudflared(r) {
2726
+ const { io: io2 } = r;
2727
+ const override = io2.env.HOSTEDGE_TUNNEL_HELPER ?? io2.env.HOSTEDGE_CLOUDFLARED;
2728
+ if (override) return override;
2729
+ const exe = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
2730
+ for (const dir of (io2.env.PATH ?? "").split(delimiter).filter(Boolean))
2731
+ if (await exists(join6(dir, exe))) return join6(dir, exe);
2732
+ const binDir = join6(io2.env.HOME ?? homedir2(), ".hostedge", "bin");
2733
+ const own = join6(binDir, exe);
2734
+ if (await exists(own)) return own;
2735
+ const asset = cloudflaredAsset(process.platform, process.arch);
2736
+ if (!asset)
2737
+ throw new CliError(
2738
+ "Install the open-source tunnel helper (the cloudflared command) and run this again, or set HOSTEDGE_TUNNEL_HELPER to its path."
2739
+ );
2740
+ r.warn("Downloading the tunnel helper (one time)\u2026\n");
2741
+ const response = await io2.fetch(`${RELEASES}/${asset}`);
2742
+ if (!response.ok) throw new CliError(`Could not download the tunnel helper (${response.status}).`);
2743
+ await mkdir3(binDir, { recursive: true });
2744
+ const tmp = join6(binDir, `${asset}.download`);
2745
+ await writeFile4(tmp, new Uint8Array(await response.arrayBuffer()));
2746
+ if (asset.endsWith(".tgz")) {
2747
+ await new Promise((resolve6, reject) => {
2748
+ const tar = nodeSpawn("tar", ["-xzf", tmp, "-C", binDir], { stdio: "ignore" });
2749
+ tar.on("error", reject);
2750
+ tar.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
2751
+ });
2752
+ await rm2(tmp, { force: true });
2753
+ } else {
2754
+ await rename(tmp, own);
2755
+ }
2756
+ await chmod2(own, 493);
2757
+ return own;
2758
+ }
2759
+ function listening(port) {
2760
+ return new Promise((resolve6) => {
2761
+ const socket = netConnect({ host: "127.0.0.1", port });
2762
+ socket.once("connect", () => {
2763
+ socket.destroy();
2764
+ resolve6(true);
2765
+ });
2766
+ socket.once("error", () => resolve6(false));
2767
+ socket.setTimeout(1e3, () => {
2768
+ socket.destroy();
2769
+ resolve6(false);
2770
+ });
2771
+ });
2772
+ }
2773
+ var QUICK_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
2774
+ async function share(r, options) {
2775
+ const { io: io2 } = r;
2776
+ const s = r.s;
2777
+ const port = Number(options.port ?? "3000");
2778
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
2779
+ throw new CliError(
2780
+ "Give the port your dev server runs on, e.g. `hostedge share 5173`.",
2781
+ EXIT.usage
2782
+ );
2783
+ if (!await listening(port))
2784
+ r.warn(
2785
+ `${s.yellow("Note:")} nothing answers on localhost:${port} yet. Start your dev server; the link works as soon as it does.
2786
+ `
2787
+ );
2788
+ const { apiUrl, token } = await credentials(io2);
2789
+ let opened = { mode: "quick" };
2790
+ const api = token && !options.quick ? createApi(io2, apiUrl, token) : null;
2791
+ if (api) opened = await api.request("/api/v1/shares", { json: { port } });
2792
+ const binary = await findCloudflared(r);
2793
+ const hostHeader = options.hostHeader ?? "localhost";
2794
+ const args = opened.mode === "tunnel" && opened.token ? ["tunnel", "--no-autoupdate", "run", "--token", opened.token] : [
2795
+ "tunnel",
2796
+ "--no-autoupdate",
2797
+ "--url",
2798
+ `http://localhost:${port}`,
2799
+ "--http-host-header",
2800
+ hostHeader
2801
+ ];
2802
+ const spawn2 = options.spawn ?? nodeSpawn;
2803
+ const child = spawn2(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
2804
+ const progress = { announced: false };
2805
+ const tail = [];
2806
+ const remember = (chunk) => {
2807
+ for (const line of chunk.toString().split("\n").filter((l) => l.trim())) {
2808
+ tail.push(
2809
+ line.trim().replace(/\bcloudflared\b/gi, "the tunnel helper").replace(/\bCloudflare\b/gi, "the platform")
2810
+ );
2811
+ if (tail.length > 4) tail.shift();
2812
+ }
2813
+ };
2814
+ child.stdout?.on("data", remember);
2815
+ child.stderr?.on("data", remember);
2816
+ const announce = (url) => {
2817
+ if (progress.announced) return;
2818
+ progress.announced = true;
2819
+ if (r.json)
2820
+ io2.stdout(
2821
+ `${JSON.stringify({ mode: opened.mode, url, expiresAt: opened.share?.expiresAt ?? null })}
2822
+ `
2823
+ );
2824
+ if (r.quiet) {
2825
+ if (!r.json) io2.stdout(`${url}
2826
+ `);
2827
+ return;
2828
+ }
2829
+ r.always(`
2830
+ ${s.bold("Your localhost is live:")} ${s.cyan(url)}
2831
+
2832
+ `);
2833
+ r.always(`${renderUnicodeCompact(url, { border: 1 })}
2834
+
2835
+ `);
2836
+ r.always(
2837
+ `Scan with your phone, or share the link.${opened.share ? ` It closes when you press Ctrl+C, or at ${new Date(opened.share.expiresAt).toLocaleTimeString()}.` : " Quick links are public and anonymous; they close when you press Ctrl+C."}
2838
+ `
2839
+ );
2840
+ };
2841
+ if (opened.mode === "tunnel" && opened.share) {
2842
+ const url = opened.share.url;
2843
+ const watch = (chunk) => {
2844
+ if (/Registered tunnel connection|Connection [0-9a-f-]+ registered/i.test(chunk.toString()))
2845
+ announce(url);
2846
+ };
2847
+ child.stdout?.on("data", watch);
2848
+ child.stderr?.on("data", watch);
2849
+ } else {
2850
+ if (!api && !options.quick)
2851
+ r.warn(
2852
+ `${s.dim("Not signed in: using a quick tunnel. `hostedge login` for a hostedge.app link.")}
2853
+ `
2854
+ );
2855
+ const watch = (chunk) => {
2856
+ const match = QUICK_URL.exec(chunk.toString());
2857
+ if (match) announce(match[0]);
2858
+ };
2859
+ child.stdout?.on("data", watch);
2860
+ child.stderr?.on("data", watch);
2861
+ }
2862
+ const exitCode = await new Promise((resolve6) => {
2863
+ const stop = () => {
2864
+ child.kill("SIGTERM");
2865
+ };
2866
+ options.signal?.addEventListener("abort", stop, { once: true });
2867
+ child.on("error", () => resolve6(1));
2868
+ child.on("exit", (code) => {
2869
+ options.signal?.removeEventListener("abort", stop);
2870
+ resolve6(options.signal?.aborted ? 0 : code ?? 1);
2871
+ });
2872
+ });
2873
+ if (api && opened.share) {
2874
+ await api.request(`/api/v1/shares/${encodeURIComponent(opened.share.id)}`, { method: "DELETE" }).catch(() => void 0);
2875
+ r.say(`${s.dim("Share closed.")}
2876
+ `);
2877
+ }
2878
+ if (!progress.announced && exitCode !== 0)
2879
+ throw new CliError(
2880
+ `The tunnel helper stopped before the link was ready. Check your connection.${tail.length ? `
2881
+ ${tail.join("\n")}` : ""}`
2882
+ );
2883
+ return exitCode;
2884
+ }
2885
+
2886
+ // src/report.ts
2887
+ function isCi(env) {
2888
+ const value = env.CI;
2889
+ return value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
2890
+ }
2891
+ var TOKEN_LIKE = /hostedge_[A-Za-z0-9_-]{8,}/g;
2892
+ function redact(text) {
2893
+ return text.replace(TOKEN_LIKE, "hostedge_\u2026");
2894
+ }
2895
+ var Reporter = class {
2896
+ constructor(io2, flags) {
2897
+ this.io = io2;
2898
+ this.flags = flags;
2899
+ }
2900
+ io;
2901
+ flags;
2902
+ get json() {
2903
+ return this.flags.json;
2904
+ }
2905
+ get quiet() {
2906
+ return this.flags.quiet;
2907
+ }
2908
+ /** Where human text goes. */
2909
+ get human() {
2910
+ return this.flags.json ? "stderr" : "stdout";
2911
+ }
2912
+ /** Styles for human text (colors only if that stream is a terminal). */
2913
+ get s() {
2914
+ return styles(this.io, this.human, this.flags.noColor);
2915
+ }
2916
+ /** Whether a person could answer a prompt. False for --yes, CI, or when stdin is piped. */
2917
+ get interactive() {
2918
+ return !this.flags.yes && !isCi(this.io.env) && this.io.isStdinTty;
2919
+ }
2920
+ write(stream, text) {
2921
+ if (stream === "stdout") this.io.stdout(text);
2922
+ else this.io.stderr(text);
2923
+ }
2924
+ /** Progress and decoration for people: hidden by --quiet. */
2925
+ say(text) {
2926
+ if (!this.flags.quiet) this.write(this.human, text);
2927
+ }
2928
+ /** Human output that --quiet keeps (the one line a script wants). */
2929
+ always(text) {
2930
+ this.write(this.human, text);
2931
+ }
2932
+ /** A note for people on stderr (never stdout): hidden by --quiet. */
2933
+ warn(text) {
2934
+ if (!this.flags.quiet) this.io.stderr(text);
2935
+ }
2936
+ /**
2937
+ * The command's result. With --json: one JSON document on stdout, and `human` on stderr
2938
+ * for whoever is watching. Otherwise `human` on stdout, or just `quiet` with --quiet
2939
+ * (the one line a script wants; empty by default).
2940
+ */
2941
+ result(data, human, quiet = "") {
2942
+ if (this.flags.json) {
2943
+ this.io.stdout(`${JSON.stringify(data)}
2944
+ `);
2945
+ if (!this.flags.quiet) this.io.stderr(human);
2946
+ return;
2947
+ }
2948
+ const text = this.flags.quiet ? quiet : human;
2949
+ if (text) this.io.stdout(text);
2950
+ }
2951
+ /** One NDJSON line with --json, a human line otherwise: for streaming commands. */
2952
+ line(data, human) {
2953
+ if (this.flags.json) this.io.stdout(`${JSON.stringify(data)}
2954
+ `);
2955
+ else this.io.stdout(human);
2956
+ }
2957
+ /** Reports a failure: human text on stderr, plus a JSON error envelope on stdout. */
2958
+ fail(message, exitCode) {
2959
+ const clean = redact(message);
2960
+ const red = styles(this.io, "stderr", this.flags.noColor).red;
2961
+ this.io.stderr(`${red("Error:")} ${clean}
2962
+ `);
2963
+ if (this.flags.json)
2964
+ this.io.stdout(
2965
+ `${JSON.stringify({ error: { code: exitName(exitCode), message: clean, exitCode } })}
2966
+ `
2967
+ );
2968
+ }
2969
+ };
2970
+
2971
+ // src/cli.ts
2972
+ var VERSION = "0.1.1";
2973
+ var HELP = `hostedge \u2014 deploy to HostEdge from your terminal
2974
+
2975
+ Usage
2976
+ hostedge login [--token <t> | --with-token] [--api-url <url>]
2977
+ Sign in (opens your browser; --with-token reads stdin)
2978
+ hostedge logout Forget the saved token
2979
+ hostedge whoami Show who and where you are signed in
2980
+ hostedge init [dir] [--name <name>] [--project <id|slug>] [--force] [--ai]
2981
+ Link a folder to a project; --ai also writes
2982
+ AGENTS.md, CLAUDE.md, .cursor/rules and a Claude skill
2983
+ hostedge ai-kit [dir] [--force] Write the assistant instruction files only
2984
+ hostedge deploy [dir] [--preview] [--message <text>] [--wait | --no-wait]
2985
+ Upload and deploy a folder (waits by default)
2986
+ hostedge logs [--follow] [--level <level>] [--search <text>] [--since 30m|1h|24h|7d]
2987
+ Show runtime logs (--json: one JSON object per line)
2988
+ hostedge env pull [--environment <name>] [--file <path>]
2989
+ Write variables to a .env file
2990
+ hostedge share [port] [--quick] [--host-header <host>]
2991
+ Public HTTPS link to a local dev server
2992
+ (try it on your phone before deploying)
2993
+
2994
+ Options for every command
2995
+ --json Machine-readable output on stdout; progress and text go to stderr
2996
+ -y, --yes Never prompt; with CI=1 or no TTY it never waits for input either
2997
+ -q, --quiet Only results and errors (deploy prints just the URL)
2998
+ --api-url <url> Use another HostEdge for this run (default https://hostedge.app)
2999
+ --no-color Plain text (NO_COLOR is respected too)
3000
+ -h, --help Show this help -v, --version Show the version
3001
+
3002
+ Environment
3003
+ HOSTEDGE_TOKEN API token (CI); overrides the saved login. Never printed.
3004
+ HOSTEDGE_API_URL HostEdge URL (default https://hostedge.app)
3005
+ HOSTEDGE_TUNNEL_HELPER Path to the tunnel helper (otherwise PATH, then a one-time download)
3006
+ NO_COLOR Disable colors
3007
+ CI Treated like --yes
3008
+
3009
+ Exit codes
3010
+ ${CLI_EXIT_CODES.map((c) => ` ${c.code} ${c.name.padEnd(14)}${c.meaning}`).join("\n")}
3011
+
3012
+ Agents: \`hostedge deploy --wait --json\` prints {"deploymentId","status","url","failureReason"}
3013
+ and exits 5 when the build fails. Docs: https://hostedge.app/docs/agent-kit
3014
+ `;
3015
+ var str = (value) => typeof value === "string" ? value : void 0;
3016
+ function apiUrlOf(value) {
3017
+ let url;
3018
+ try {
3019
+ url = new URL(value);
3020
+ } catch {
3021
+ throw new CliError(
3022
+ `--api-url \u201C${value}\u201D is not a URL. Use e.g. https://hostedge.app.`,
3023
+ EXIT.usage
3024
+ );
3025
+ }
3026
+ if (url.protocol !== "https:" && url.protocol !== "http:")
3027
+ throw new CliError(`--api-url must start with https:// or http://, not \u201C${value}\u201D.`, EXIT.usage);
3028
+ return value.replace(/\/+$/, "");
3029
+ }
3030
+ async function run(argv, baseIo, signal) {
3031
+ const wantsJson = argv.includes("--json");
3032
+ const early = new Reporter(baseIo, { json: wantsJson, quiet: false, yes: false, noColor: false });
3033
+ let parsed;
3034
+ try {
3035
+ parsed = parseArgs({
3036
+ args: argv,
3037
+ allowPositionals: true,
3038
+ strict: true,
3039
+ options: {
3040
+ help: { type: "boolean", short: "h" },
3041
+ version: { type: "boolean", short: "v" },
3042
+ json: { type: "boolean" },
3043
+ yes: { type: "boolean", short: "y" },
3044
+ quiet: { type: "boolean", short: "q" },
3045
+ "no-color": { type: "boolean" },
3046
+ "api-url": { type: "string" },
3047
+ token: { type: "string" },
3048
+ "with-token": { type: "boolean" },
3049
+ // The original spelling of --api-url, still accepted.
3050
+ api: { type: "string" },
3051
+ name: { type: "string" },
3052
+ project: { type: "string" },
3053
+ force: { type: "boolean" },
3054
+ ai: { type: "boolean" },
3055
+ preview: { type: "boolean" },
3056
+ message: { type: "string", short: "m" },
3057
+ wait: { type: "boolean" },
3058
+ "no-wait": { type: "boolean" },
3059
+ follow: { type: "boolean", short: "f" },
3060
+ level: { type: "string" },
3061
+ search: { type: "string" },
3062
+ since: { type: "string" },
3063
+ environment: { type: "string", short: "e" },
3064
+ file: { type: "string" },
3065
+ quick: { type: "boolean" },
3066
+ "host-header": { type: "string" }
3067
+ }
3068
+ });
3069
+ } catch (error) {
3070
+ early.fail(`${error.message}
3071
+ Run \`hostedge --help\` for usage.`, EXIT.usage);
3072
+ return EXIT.usage;
3073
+ }
3074
+ const { values, positionals } = parsed;
3075
+ const [command, ...rest] = positionals;
3076
+ if (values.version) {
3077
+ if (wantsJson) early.result({ version: VERSION }, "");
3078
+ else baseIo.stdout(`${VERSION}
3079
+ `);
3080
+ return EXIT.ok;
3081
+ }
3082
+ if (values.help || !command) {
3083
+ baseIo.stdout(HELP);
3084
+ return command || values.help ? EXIT.ok : EXIT.usage;
3085
+ }
3086
+ let r = early;
3087
+ try {
3088
+ const apiOverride = str(values["api-url"]) ?? str(values.api);
3089
+ const io2 = apiOverride ? { ...baseIo, env: { ...baseIo.env, HOSTEDGE_API_URL: apiUrlOf(apiOverride) } } : baseIo;
3090
+ r = new Reporter(io2, {
3091
+ json: values.json === true,
3092
+ quiet: values.quiet === true,
3093
+ yes: values.yes === true,
3094
+ noColor: values["no-color"] === true
3095
+ });
3096
+ if (values.wait && values["no-wait"])
3097
+ throw new CliError("Use either --wait or --no-wait, not both.", EXIT.usage);
3098
+ switch (command) {
3099
+ case "login":
3100
+ return await login(r, {
3101
+ token: str(values.token),
3102
+ withToken: values["with-token"] === true,
3103
+ api: apiOverride ? apiUrlOf(apiOverride) : void 0
3104
+ });
3105
+ case "logout":
3106
+ return await logout(r);
3107
+ case "whoami":
3108
+ return await whoami(r);
3109
+ case "init":
3110
+ return await init(r, {
3111
+ dir: rest[0],
3112
+ name: str(values.name),
3113
+ project: str(values.project),
3114
+ force: values.force === true,
3115
+ ai: values.ai === true
3116
+ });
3117
+ case "ai-kit":
3118
+ return await aiKit(r, { dir: rest[0], force: values.force === true });
3119
+ case "deploy":
3120
+ return await deploy(r, {
3121
+ dir: rest[0],
3122
+ preview: values.preview === true,
3123
+ message: str(values.message),
3124
+ wait: values["no-wait"] !== true
3125
+ });
3126
+ case "logs":
3127
+ return await logs(r, {
3128
+ follow: values.follow === true,
3129
+ level: str(values.level),
3130
+ search: str(values.search),
3131
+ since: str(values.since),
3132
+ signal
3133
+ });
3134
+ case "env":
3135
+ if (rest[0] !== "pull")
3136
+ throw new CliError(
3137
+ "Usage: hostedge env pull [--environment <name>] [--file <path>]",
3138
+ EXIT.usage
3139
+ );
3140
+ return await envPull(r, { environment: str(values.environment), file: str(values.file) });
3141
+ case "share":
3142
+ return await share(r, {
3143
+ port: rest[0],
3144
+ quick: values.quick === true,
3145
+ hostHeader: str(values["host-header"]),
3146
+ signal
3147
+ });
3148
+ default:
3149
+ throw new CliError(
3150
+ `Unknown command \u201C${command}\u201D. Run \`hostedge --help\` for usage.`,
3151
+ EXIT.usage
3152
+ );
3153
+ }
3154
+ } catch (error) {
3155
+ if (error instanceof CliError) {
3156
+ r.fail(error.message, error.exitCode);
3157
+ return error.exitCode;
3158
+ }
3159
+ r.fail(
3160
+ `Unexpected error: ${error instanceof Error ? error.stack ?? error.message : String(error)}`,
3161
+ EXIT.failure
3162
+ );
3163
+ return EXIT.failure;
3164
+ }
3165
+ }
3166
+
3167
+ // src/main.ts
3168
+ function openUrl(url) {
3169
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
3170
+ return new Promise((resolve6) => {
3171
+ try {
3172
+ const child = spawn(command, [url], { stdio: "ignore", detached: true });
3173
+ child.on("error", () => resolve6(false));
3174
+ child.on("spawn", () => {
3175
+ child.unref();
3176
+ resolve6(true);
3177
+ });
3178
+ } catch {
3179
+ resolve6(false);
3180
+ }
3181
+ });
3182
+ }
3183
+ var io = {
3184
+ cwd: process.cwd(),
3185
+ env: process.env,
3186
+ stdout: (text) => process.stdout.write(text),
3187
+ stderr: (text) => process.stderr.write(text),
3188
+ fetch: globalThis.fetch,
3189
+ openUrl,
3190
+ // Undefined (not false) when stdout is a pipe.
3191
+ isTty: process.stdout.isTTY,
3192
+ isErrTty: process.stderr.isTTY,
3193
+ isStdinTty: process.stdin.isTTY,
3194
+ readStdin: async () => {
3195
+ const chunks = [];
3196
+ for await (const chunk of process.stdin) chunks.push(chunk);
3197
+ return Buffer.concat(chunks).toString("utf8");
3198
+ },
3199
+ sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
3200
+ };
3201
+ var controller = new AbortController();
3202
+ process.on("SIGINT", () => {
3203
+ if (controller.signal.aborted) process.exit(130);
3204
+ controller.abort();
3205
+ });
3206
+ run(process.argv.slice(2), io, controller.signal).then(
3207
+ (code) => process.exit(code),
3208
+ (error) => {
3209
+ process.stderr.write(
3210
+ `Unexpected error: ${error instanceof Error ? error.stack : String(error)}
3211
+ `
3212
+ );
3213
+ process.exit(1);
3214
+ }
3215
+ );