shyim-hosting-test-cli 0.0.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.
- package/.app-hosting/d1/miniflare-D1DatabaseObject/e833abeb83abd38e60de8166ab13bbd4d7a5636dc148a7fd8ec47b7678c87854.sqlite +0 -0
- package/.app-hosting/d1/miniflare-D1DatabaseObject/e833abeb83abd38e60de8166ab13bbd4d7a5636dc148a7fd8ec47b7678c87854.sqlite-shm +0 -0
- package/.app-hosting/d1/miniflare-D1DatabaseObject/e833abeb83abd38e60de8166ab13bbd4d7a5636dc148a7fd8ec47b7678c87854.sqlite-wal +0 -0
- package/.app-hosting/kv/local-cache/blobs/31b6e14132d4fc13bdcea6c476f84d6ae21cccfa838c0e604b09ebe4e2a157810000019d25d8210e +1 -0
- package/.app-hosting/kv/miniflare-KVNamespaceObject/5d62bcd90581395a36432777b0f85ea17c9dc3777f19ed60568ff7698752517c.sqlite +0 -0
- package/.app-hosting/kv/miniflare-KVNamespaceObject/5d62bcd90581395a36432777b0f85ea17c9dc3777f19ed60568ff7698752517c.sqlite-shm +0 -0
- package/.app-hosting/kv/miniflare-KVNamespaceObject/5d62bcd90581395a36432777b0f85ea17c9dc3777f19ed60568ff7698752517c.sqlite-wal +0 -0
- package/dist/index.js +398 -0
- package/package.json +27 -0
- package/src/commands/deploy.ts +85 -0
- package/src/commands/dev.ts +109 -0
- package/src/commands/init.ts +57 -0
- package/src/commands/login.ts +44 -0
- package/src/commands/logout.ts +16 -0
- package/src/commands/projects.ts +38 -0
- package/src/config.ts +29 -0
- package/src/index.ts +26 -0
- package/src/project.ts +22 -0
- package/src/trpc.ts +25 -0
- package/tsconfig.json +13 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
world
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { defineCommand as defineCommand7, runMain } from "citty";
|
|
5
|
+
|
|
6
|
+
// src/commands/login.ts
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import consola from "consola";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
14
|
+
var CONFIG_DIR = join(homedir(), ".config", "app-hosting");
|
|
15
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
16
|
+
async function loadConfig() {
|
|
17
|
+
try {
|
|
18
|
+
const data = await readFile(CONFIG_FILE, "utf-8");
|
|
19
|
+
return JSON.parse(data);
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function saveConfig(config) {
|
|
25
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
26
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
27
|
+
}
|
|
28
|
+
function getApiUrl(config) {
|
|
29
|
+
return "https://sw-test-app-hosting.click";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/commands/login.ts
|
|
33
|
+
var loginCommand = defineCommand({
|
|
34
|
+
meta: {
|
|
35
|
+
name: "login",
|
|
36
|
+
description: "Store a deploy token for authentication"
|
|
37
|
+
},
|
|
38
|
+
args: {
|
|
39
|
+
token: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Deploy token from the web UI",
|
|
42
|
+
required: false
|
|
43
|
+
},
|
|
44
|
+
"api-url": {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "API base URL",
|
|
47
|
+
required: false
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async run({ args }) {
|
|
51
|
+
const config = await loadConfig();
|
|
52
|
+
let token = args.token;
|
|
53
|
+
if (!token) {
|
|
54
|
+
token = await consola.prompt("Enter your deploy token:", {
|
|
55
|
+
type: "text"
|
|
56
|
+
});
|
|
57
|
+
if (!token || typeof token !== "string") {
|
|
58
|
+
consola.error("No token provided.");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
config.token = token;
|
|
63
|
+
if (args["api-url"]) {
|
|
64
|
+
config.apiUrl = args["api-url"];
|
|
65
|
+
}
|
|
66
|
+
await saveConfig(config);
|
|
67
|
+
consola.success("Token saved. You can now use `deploy` and other commands.");
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// src/commands/logout.ts
|
|
72
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
73
|
+
import consola2 from "consola";
|
|
74
|
+
var logoutCommand = defineCommand2({
|
|
75
|
+
meta: {
|
|
76
|
+
name: "logout",
|
|
77
|
+
description: "Remove stored credentials"
|
|
78
|
+
},
|
|
79
|
+
async run() {
|
|
80
|
+
const config = await loadConfig();
|
|
81
|
+
delete config.token;
|
|
82
|
+
await saveConfig(config);
|
|
83
|
+
consola2.success("Logged out.");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// src/commands/init.ts
|
|
88
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
89
|
+
import consola3 from "consola";
|
|
90
|
+
|
|
91
|
+
// src/project.ts
|
|
92
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
93
|
+
import { join as join2 } from "path";
|
|
94
|
+
var PROJECT_FILE = "app-hosting.json";
|
|
95
|
+
async function loadProjectConfig(cwd) {
|
|
96
|
+
try {
|
|
97
|
+
const data = await readFile2(join2(cwd, PROJECT_FILE), "utf-8");
|
|
98
|
+
return JSON.parse(data);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function saveProjectConfig(cwd, config) {
|
|
104
|
+
await writeFile2(join2(cwd, PROJECT_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/trpc.ts
|
|
108
|
+
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
109
|
+
async function createClient() {
|
|
110
|
+
const config = await loadConfig();
|
|
111
|
+
if (!config.token) {
|
|
112
|
+
throw new Error("Not logged in. Run `app-hosting login` first.");
|
|
113
|
+
}
|
|
114
|
+
const apiUrl = getApiUrl(config);
|
|
115
|
+
const client = createTRPCClient({
|
|
116
|
+
links: [
|
|
117
|
+
httpBatchLink({
|
|
118
|
+
url: `${apiUrl}/api`,
|
|
119
|
+
headers: {
|
|
120
|
+
Authorization: `Bearer ${config.token}`
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
]
|
|
124
|
+
});
|
|
125
|
+
return client;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/commands/init.ts
|
|
129
|
+
var initCommand = defineCommand3({
|
|
130
|
+
meta: {
|
|
131
|
+
name: "init",
|
|
132
|
+
description: "Link current directory to a project"
|
|
133
|
+
},
|
|
134
|
+
args: {
|
|
135
|
+
"project-id": {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Project ID to link",
|
|
138
|
+
required: false
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async run({ args }) {
|
|
142
|
+
const client = await createClient();
|
|
143
|
+
let projectId = args["project-id"];
|
|
144
|
+
let projectName;
|
|
145
|
+
if (projectId) {
|
|
146
|
+
const project = await client.projects.get.query({ id: projectId });
|
|
147
|
+
projectName = project.name;
|
|
148
|
+
} else {
|
|
149
|
+
const projects = await client.projects.list.query();
|
|
150
|
+
if (projects.length === 0) {
|
|
151
|
+
consola3.error("No projects found. Create one in the web UI first.");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const selected = await consola3.prompt("Select a project:", {
|
|
155
|
+
type: "select",
|
|
156
|
+
options: projects.map((p) => ({ label: p.name, value: p.id }))
|
|
157
|
+
});
|
|
158
|
+
if (!selected || typeof selected !== "string") {
|
|
159
|
+
consola3.error("No project selected.");
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
projectId = selected;
|
|
163
|
+
projectName = projects.find((p) => p.id === selected).name;
|
|
164
|
+
}
|
|
165
|
+
await saveProjectConfig(process.cwd(), {
|
|
166
|
+
project_id: projectId,
|
|
167
|
+
project_name: projectName
|
|
168
|
+
});
|
|
169
|
+
consola3.success(`Linked to project "${projectName}" (${projectId})`);
|
|
170
|
+
consola3.info("Created app-hosting.json");
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// src/commands/deploy.ts
|
|
175
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
176
|
+
import consola4 from "consola";
|
|
177
|
+
import { resolve } from "path";
|
|
178
|
+
import { build } from "esbuild";
|
|
179
|
+
var deployCommand = defineCommand4({
|
|
180
|
+
meta: {
|
|
181
|
+
name: "deploy",
|
|
182
|
+
description: "Deploy worker code to your project"
|
|
183
|
+
},
|
|
184
|
+
args: {
|
|
185
|
+
file: {
|
|
186
|
+
type: "positional",
|
|
187
|
+
description: "Path to the worker entry point",
|
|
188
|
+
required: true,
|
|
189
|
+
default: "index.js"
|
|
190
|
+
},
|
|
191
|
+
"no-bundle": {
|
|
192
|
+
type: "boolean",
|
|
193
|
+
description: "Skip bundling and upload the file as-is",
|
|
194
|
+
default: false
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
async run({ args }) {
|
|
198
|
+
const client = await createClient();
|
|
199
|
+
const projectConfig = await loadProjectConfig(process.cwd());
|
|
200
|
+
if (!projectConfig) {
|
|
201
|
+
consola4.error("No app-hosting.json found. Run `app-hosting init` first.");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const filePath = resolve(args.file);
|
|
205
|
+
let scriptContent;
|
|
206
|
+
if (args["no-bundle"]) {
|
|
207
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
208
|
+
try {
|
|
209
|
+
scriptContent = await readFile3(filePath, "utf-8");
|
|
210
|
+
} catch {
|
|
211
|
+
consola4.error(`Could not read file: ${filePath}`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
consola4.start("Bundling with esbuild...");
|
|
216
|
+
try {
|
|
217
|
+
const result = await build({
|
|
218
|
+
entryPoints: [filePath],
|
|
219
|
+
bundle: true,
|
|
220
|
+
write: false,
|
|
221
|
+
format: "esm",
|
|
222
|
+
target: "esnext",
|
|
223
|
+
platform: "browser",
|
|
224
|
+
conditions: ["workerd", "worker", "browser"],
|
|
225
|
+
minify: true,
|
|
226
|
+
sourcemap: false
|
|
227
|
+
});
|
|
228
|
+
scriptContent = result.outputFiles[0].text;
|
|
229
|
+
} catch (e) {
|
|
230
|
+
consola4.error(`Bundle failed: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
consola4.success("Bundle complete.");
|
|
234
|
+
}
|
|
235
|
+
const sizeKb = (Buffer.byteLength(scriptContent) / 1024).toFixed(1);
|
|
236
|
+
consola4.start(`Deploying (${sizeKb} KB) to "${projectConfig.project_name}"...`);
|
|
237
|
+
try {
|
|
238
|
+
const result = await client.deployments.deploy.mutate({
|
|
239
|
+
projectId: projectConfig.project_id,
|
|
240
|
+
script: scriptContent
|
|
241
|
+
});
|
|
242
|
+
consola4.success(`Deployed! Deployment ID: ${result.deploymentId}`);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
consola4.error(`Deployment failed: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// src/commands/dev.ts
|
|
251
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
252
|
+
import consola5 from "consola";
|
|
253
|
+
import { resolve as resolve2, dirname, join as join3 } from "path";
|
|
254
|
+
import { watch } from "fs";
|
|
255
|
+
import { build as build2 } from "esbuild";
|
|
256
|
+
import { Miniflare } from "miniflare";
|
|
257
|
+
async function bundleWorker(entryPoint) {
|
|
258
|
+
const result = await build2({
|
|
259
|
+
entryPoints: [entryPoint],
|
|
260
|
+
bundle: true,
|
|
261
|
+
write: false,
|
|
262
|
+
format: "esm",
|
|
263
|
+
target: "esnext",
|
|
264
|
+
platform: "browser",
|
|
265
|
+
conditions: ["workerd", "worker", "browser"],
|
|
266
|
+
minify: false,
|
|
267
|
+
sourcemap: false
|
|
268
|
+
});
|
|
269
|
+
return result.outputFiles[0].text;
|
|
270
|
+
}
|
|
271
|
+
function getMiniflareOptions(script, port, persistDir) {
|
|
272
|
+
return {
|
|
273
|
+
modules: true,
|
|
274
|
+
script,
|
|
275
|
+
port,
|
|
276
|
+
compatibilityDate: "2025-03-25",
|
|
277
|
+
kvNamespaces: { cache: "local-cache" },
|
|
278
|
+
d1Databases: { db: "local-db" },
|
|
279
|
+
kvPersist: join3(persistDir, "kv"),
|
|
280
|
+
d1Persist: join3(persistDir, "d1")
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
var devCommand = defineCommand5({
|
|
284
|
+
meta: {
|
|
285
|
+
name: "dev",
|
|
286
|
+
description: "Start a local dev server with workerd"
|
|
287
|
+
},
|
|
288
|
+
args: {
|
|
289
|
+
file: {
|
|
290
|
+
type: "positional",
|
|
291
|
+
description: "Path to the worker entry point",
|
|
292
|
+
required: true
|
|
293
|
+
},
|
|
294
|
+
port: {
|
|
295
|
+
type: "string",
|
|
296
|
+
description: "Port to listen on",
|
|
297
|
+
default: "8787"
|
|
298
|
+
},
|
|
299
|
+
persist: {
|
|
300
|
+
type: "string",
|
|
301
|
+
description: "Directory for persisting KV and D1 data",
|
|
302
|
+
default: ".app-hosting"
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
async run({ args }) {
|
|
306
|
+
const filePath = resolve2(args.file);
|
|
307
|
+
const port = parseInt(args.port, 10);
|
|
308
|
+
const persistDir = resolve2(args.persist);
|
|
309
|
+
consola5.start("Bundling...");
|
|
310
|
+
let script;
|
|
311
|
+
try {
|
|
312
|
+
script = await bundleWorker(filePath);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
consola5.error(`Bundle failed: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
const mf = new Miniflare(getMiniflareOptions(script, port, persistDir));
|
|
318
|
+
const url = await mf.ready;
|
|
319
|
+
consola5.success(`Listening on ${url}`);
|
|
320
|
+
consola5.info("Bindings available: env.cache (KV), env.db (D1)");
|
|
321
|
+
const watchDir = dirname(filePath);
|
|
322
|
+
let debounce = null;
|
|
323
|
+
consola5.info(`Watching ${watchDir} for changes...`);
|
|
324
|
+
watch(watchDir, { recursive: true }, (_event, filename) => {
|
|
325
|
+
if (!filename) return;
|
|
326
|
+
if (debounce) clearTimeout(debounce);
|
|
327
|
+
debounce = setTimeout(async () => {
|
|
328
|
+
consola5.start("Rebuilding...");
|
|
329
|
+
try {
|
|
330
|
+
const newScript = await bundleWorker(filePath);
|
|
331
|
+
await mf.setOptions(getMiniflareOptions(newScript, port, persistDir));
|
|
332
|
+
consola5.success("Reloaded.");
|
|
333
|
+
} catch (e) {
|
|
334
|
+
consola5.error(`Rebuild failed: ${e instanceof Error ? e.message : e}`);
|
|
335
|
+
}
|
|
336
|
+
}, 100);
|
|
337
|
+
});
|
|
338
|
+
process.on("SIGINT", async () => {
|
|
339
|
+
consola5.info("Shutting down...");
|
|
340
|
+
await mf.dispose();
|
|
341
|
+
process.exit(0);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// src/commands/projects.ts
|
|
347
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
348
|
+
import consola6 from "consola";
|
|
349
|
+
var projectsCommand = defineCommand6({
|
|
350
|
+
meta: {
|
|
351
|
+
name: "projects",
|
|
352
|
+
description: "Manage projects"
|
|
353
|
+
},
|
|
354
|
+
subCommands: {
|
|
355
|
+
list: defineCommand6({
|
|
356
|
+
meta: {
|
|
357
|
+
name: "list",
|
|
358
|
+
description: "List your projects"
|
|
359
|
+
},
|
|
360
|
+
async run() {
|
|
361
|
+
const client = await createClient();
|
|
362
|
+
const projects = await client.projects.list.query();
|
|
363
|
+
if (projects.length === 0) {
|
|
364
|
+
consola6.info("No projects yet.");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
consola6.log("");
|
|
368
|
+
for (const p of projects) {
|
|
369
|
+
consola6.log(` ${p.name}`);
|
|
370
|
+
consola6.log(` ID: ${p.id}`);
|
|
371
|
+
consola6.log(` Subdomain: ${p.subdomain}`);
|
|
372
|
+
if (p.description) {
|
|
373
|
+
consola6.log(` Desc: ${p.description}`);
|
|
374
|
+
}
|
|
375
|
+
consola6.log("");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// src/index.ts
|
|
383
|
+
var main = defineCommand7({
|
|
384
|
+
meta: {
|
|
385
|
+
name: "app-hosting",
|
|
386
|
+
version: "0.0.1",
|
|
387
|
+
description: "CLI for App Hosting platform"
|
|
388
|
+
},
|
|
389
|
+
subCommands: {
|
|
390
|
+
login: loginCommand,
|
|
391
|
+
logout: logoutCommand,
|
|
392
|
+
init: initCommand,
|
|
393
|
+
deploy: deployCommand,
|
|
394
|
+
dev: devCommand,
|
|
395
|
+
projects: projectsCommand
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
runMain(main);
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shyim-hosting-test-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"app-hosting": "./dist/index.mjs"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup src/index.ts --format esm --out-dir dist --clean",
|
|
10
|
+
"dev": "tsx src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@trpc/client": "^11.15.0",
|
|
14
|
+
"@trpc/server": "^11.15.0",
|
|
15
|
+
"citty": "^0.1.6",
|
|
16
|
+
"consola": "^3.4.2",
|
|
17
|
+
"esbuild": "^0.27.4",
|
|
18
|
+
"miniflare": "^4.20260317.2"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@cloudflare/workers-types": "^4.20260317.1",
|
|
22
|
+
"@types/node": "^25.5.0",
|
|
23
|
+
"tsup": "^8.5.0",
|
|
24
|
+
"tsx": "^4.20.0",
|
|
25
|
+
"typescript": "^5.5.2"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { build } from "esbuild";
|
|
5
|
+
import { loadProjectConfig } from "../project";
|
|
6
|
+
import { createClient } from "../trpc";
|
|
7
|
+
|
|
8
|
+
export const deployCommand = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "deploy",
|
|
11
|
+
description: "Deploy worker code to your project",
|
|
12
|
+
},
|
|
13
|
+
args: {
|
|
14
|
+
file: {
|
|
15
|
+
type: "positional",
|
|
16
|
+
description: "Path to the worker entry point",
|
|
17
|
+
required: true,
|
|
18
|
+
default: 'index.js',
|
|
19
|
+
},
|
|
20
|
+
"no-bundle": {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
description: "Skip bundling and upload the file as-is",
|
|
23
|
+
default: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
async run({ args }) {
|
|
27
|
+
const client = await createClient();
|
|
28
|
+
|
|
29
|
+
const projectConfig = await loadProjectConfig(process.cwd());
|
|
30
|
+
if (!projectConfig) {
|
|
31
|
+
consola.error("No app-hosting.json found. Run `app-hosting init` first.");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const filePath = resolve(args.file);
|
|
36
|
+
let scriptContent: string;
|
|
37
|
+
|
|
38
|
+
if (args["no-bundle"]) {
|
|
39
|
+
const { readFile } = await import("node:fs/promises");
|
|
40
|
+
try {
|
|
41
|
+
scriptContent = await readFile(filePath, "utf-8");
|
|
42
|
+
} catch {
|
|
43
|
+
consola.error(`Could not read file: ${filePath}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
consola.start("Bundling with esbuild...");
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const result = await build({
|
|
51
|
+
entryPoints: [filePath],
|
|
52
|
+
bundle: true,
|
|
53
|
+
write: false,
|
|
54
|
+
format: "esm",
|
|
55
|
+
target: "esnext",
|
|
56
|
+
platform: "browser",
|
|
57
|
+
conditions: ["workerd", "worker", "browser"],
|
|
58
|
+
minify: true,
|
|
59
|
+
sourcemap: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
scriptContent = result.outputFiles[0]!.text;
|
|
63
|
+
} catch (e: unknown) {
|
|
64
|
+
consola.error(`Bundle failed: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
consola.success("Bundle complete.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sizeKb = (Buffer.byteLength(scriptContent) / 1024).toFixed(1);
|
|
72
|
+
consola.start(`Deploying (${sizeKb} KB) to "${projectConfig.project_name}"...`);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const result = await client.deployments.deploy.mutate({
|
|
76
|
+
projectId: projectConfig.project_id,
|
|
77
|
+
script: scriptContent,
|
|
78
|
+
});
|
|
79
|
+
consola.success(`Deployed! Deployment ID: ${result.deploymentId}`);
|
|
80
|
+
} catch (e: unknown) {
|
|
81
|
+
consola.error(`Deployment failed: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { resolve, dirname, join } from "node:path";
|
|
4
|
+
import { watch } from "node:fs";
|
|
5
|
+
import { build, type BuildResult } from "esbuild";
|
|
6
|
+
import { Miniflare } from "miniflare";
|
|
7
|
+
|
|
8
|
+
async function bundleWorker(entryPoint: string): Promise<string> {
|
|
9
|
+
const result: BuildResult = await build({
|
|
10
|
+
entryPoints: [entryPoint],
|
|
11
|
+
bundle: true,
|
|
12
|
+
write: false,
|
|
13
|
+
format: "esm",
|
|
14
|
+
target: "esnext",
|
|
15
|
+
platform: "browser",
|
|
16
|
+
conditions: ["workerd", "worker", "browser"],
|
|
17
|
+
minify: false,
|
|
18
|
+
sourcemap: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return result.outputFiles![0]!.text;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getMiniflareOptions(script: string, port: number, persistDir: string) {
|
|
25
|
+
return {
|
|
26
|
+
modules: true,
|
|
27
|
+
script,
|
|
28
|
+
port,
|
|
29
|
+
compatibilityDate: "2025-03-25",
|
|
30
|
+
kvNamespaces: { cache: "local-cache" },
|
|
31
|
+
d1Databases: { db: "local-db" },
|
|
32
|
+
kvPersist: join(persistDir, "kv"),
|
|
33
|
+
d1Persist: join(persistDir, "d1"),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const devCommand = defineCommand({
|
|
38
|
+
meta: {
|
|
39
|
+
name: "dev",
|
|
40
|
+
description: "Start a local dev server with workerd",
|
|
41
|
+
},
|
|
42
|
+
args: {
|
|
43
|
+
file: {
|
|
44
|
+
type: "positional",
|
|
45
|
+
description: "Path to the worker entry point",
|
|
46
|
+
required: true,
|
|
47
|
+
},
|
|
48
|
+
port: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Port to listen on",
|
|
51
|
+
default: "8787",
|
|
52
|
+
},
|
|
53
|
+
persist: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Directory for persisting KV and D1 data",
|
|
56
|
+
default: ".app-hosting",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
async run({ args }) {
|
|
60
|
+
const filePath = resolve(args.file);
|
|
61
|
+
const port = parseInt(args.port, 10);
|
|
62
|
+
const persistDir = resolve(args.persist);
|
|
63
|
+
|
|
64
|
+
consola.start("Bundling...");
|
|
65
|
+
|
|
66
|
+
let script: string;
|
|
67
|
+
try {
|
|
68
|
+
script = await bundleWorker(filePath);
|
|
69
|
+
} catch (e: unknown) {
|
|
70
|
+
consola.error(`Bundle failed: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const mf = new Miniflare(getMiniflareOptions(script, port, persistDir));
|
|
75
|
+
|
|
76
|
+
const url = await mf.ready;
|
|
77
|
+
consola.success(`Listening on ${url}`);
|
|
78
|
+
consola.info("Bindings available: env.cache (KV), env.db (D1)");
|
|
79
|
+
|
|
80
|
+
// Watch the entry point directory for changes
|
|
81
|
+
const watchDir = dirname(filePath);
|
|
82
|
+
let debounce: ReturnType<typeof setTimeout> | null = null;
|
|
83
|
+
|
|
84
|
+
consola.info(`Watching ${watchDir} for changes...`);
|
|
85
|
+
|
|
86
|
+
watch(watchDir, { recursive: true }, (_event, filename) => {
|
|
87
|
+
if (!filename) return;
|
|
88
|
+
|
|
89
|
+
if (debounce) clearTimeout(debounce);
|
|
90
|
+
debounce = setTimeout(async () => {
|
|
91
|
+
consola.start("Rebuilding...");
|
|
92
|
+
try {
|
|
93
|
+
const newScript = await bundleWorker(filePath);
|
|
94
|
+
await mf.setOptions(getMiniflareOptions(newScript, port, persistDir));
|
|
95
|
+
consola.success("Reloaded.");
|
|
96
|
+
} catch (e: unknown) {
|
|
97
|
+
consola.error(`Rebuild failed: ${e instanceof Error ? e.message : e}`);
|
|
98
|
+
}
|
|
99
|
+
}, 100);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Keep process alive, handle shutdown
|
|
103
|
+
process.on("SIGINT", async () => {
|
|
104
|
+
consola.info("Shutting down...");
|
|
105
|
+
await mf.dispose();
|
|
106
|
+
process.exit(0);
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { saveProjectConfig } from "../project";
|
|
4
|
+
import { createClient } from "../trpc";
|
|
5
|
+
|
|
6
|
+
export const initCommand = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "init",
|
|
9
|
+
description: "Link current directory to a project",
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
"project-id": {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Project ID to link",
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
async run({ args }) {
|
|
19
|
+
const client = await createClient();
|
|
20
|
+
|
|
21
|
+
let projectId = args["project-id"];
|
|
22
|
+
let projectName: string;
|
|
23
|
+
|
|
24
|
+
if (projectId) {
|
|
25
|
+
const project = await client.projects.get.query({ id: projectId });
|
|
26
|
+
projectName = project.name;
|
|
27
|
+
} else {
|
|
28
|
+
const projects = await client.projects.list.query();
|
|
29
|
+
|
|
30
|
+
if (projects.length === 0) {
|
|
31
|
+
consola.error("No projects found. Create one in the web UI first.");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const selected = await consola.prompt("Select a project:", {
|
|
36
|
+
type: "select",
|
|
37
|
+
options: projects.map((p) => ({ label: p.name, value: p.id })),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!selected || typeof selected !== "string") {
|
|
41
|
+
consola.error("No project selected.");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
projectId = selected;
|
|
46
|
+
projectName = projects.find((p) => p.id === selected)!.name;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await saveProjectConfig(process.cwd(), {
|
|
50
|
+
project_id: projectId,
|
|
51
|
+
project_name: projectName,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
consola.success(`Linked to project "${projectName}" (${projectId})`);
|
|
55
|
+
consola.info("Created app-hosting.json");
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { loadConfig, saveConfig } from "../config";
|
|
4
|
+
|
|
5
|
+
export const loginCommand = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "login",
|
|
8
|
+
description: "Store a deploy token for authentication",
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
token: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Deploy token from the web UI",
|
|
14
|
+
required: false,
|
|
15
|
+
},
|
|
16
|
+
"api-url": {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "API base URL",
|
|
19
|
+
required: false,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
async run({ args }) {
|
|
23
|
+
const config = await loadConfig();
|
|
24
|
+
|
|
25
|
+
let token = args.token;
|
|
26
|
+
if (!token) {
|
|
27
|
+
token = await consola.prompt("Enter your deploy token:", {
|
|
28
|
+
type: "text",
|
|
29
|
+
});
|
|
30
|
+
if (!token || typeof token !== "string") {
|
|
31
|
+
consola.error("No token provided.");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
config.token = token;
|
|
37
|
+
if (args["api-url"]) {
|
|
38
|
+
config.apiUrl = args["api-url"];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await saveConfig(config);
|
|
42
|
+
consola.success("Token saved. You can now use `deploy` and other commands.");
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { loadConfig, saveConfig } from "../config";
|
|
4
|
+
|
|
5
|
+
export const logoutCommand = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "logout",
|
|
8
|
+
description: "Remove stored credentials",
|
|
9
|
+
},
|
|
10
|
+
async run() {
|
|
11
|
+
const config = await loadConfig();
|
|
12
|
+
delete config.token;
|
|
13
|
+
await saveConfig(config);
|
|
14
|
+
consola.success("Logged out.");
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { createClient } from "../trpc";
|
|
4
|
+
|
|
5
|
+
export const projectsCommand = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "projects",
|
|
8
|
+
description: "Manage projects",
|
|
9
|
+
},
|
|
10
|
+
subCommands: {
|
|
11
|
+
list: defineCommand({
|
|
12
|
+
meta: {
|
|
13
|
+
name: "list",
|
|
14
|
+
description: "List your projects",
|
|
15
|
+
},
|
|
16
|
+
async run() {
|
|
17
|
+
const client = await createClient();
|
|
18
|
+
const projects = await client.projects.list.query();
|
|
19
|
+
|
|
20
|
+
if (projects.length === 0) {
|
|
21
|
+
consola.info("No projects yet.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
consola.log("");
|
|
26
|
+
for (const p of projects) {
|
|
27
|
+
consola.log(` ${p.name}`);
|
|
28
|
+
consola.log(` ID: ${p.id}`);
|
|
29
|
+
consola.log(` Subdomain: ${p.subdomain}`);
|
|
30
|
+
if (p.description) {
|
|
31
|
+
consola.log(` Desc: ${p.description}`);
|
|
32
|
+
}
|
|
33
|
+
consola.log("");
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
interface Config {
|
|
6
|
+
token?: string;
|
|
7
|
+
apiUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIR = join(homedir(), ".config", "app-hosting");
|
|
11
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
12
|
+
|
|
13
|
+
export async function loadConfig(): Promise<Config> {
|
|
14
|
+
try {
|
|
15
|
+
const data = await readFile(CONFIG_FILE, "utf-8");
|
|
16
|
+
return JSON.parse(data) as Config;
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function saveConfig(config: Config): Promise<void> {
|
|
23
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
24
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getApiUrl(config: Config): string {
|
|
28
|
+
return "https://sw-test-app-hosting.click";
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { loginCommand } from "./commands/login";
|
|
4
|
+
import { logoutCommand } from "./commands/logout";
|
|
5
|
+
import { initCommand } from "./commands/init";
|
|
6
|
+
import { deployCommand } from "./commands/deploy";
|
|
7
|
+
import { devCommand } from "./commands/dev";
|
|
8
|
+
import { projectsCommand } from "./commands/projects";
|
|
9
|
+
|
|
10
|
+
const main = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: "app-hosting",
|
|
13
|
+
version: "0.0.1",
|
|
14
|
+
description: "CLI for App Hosting platform",
|
|
15
|
+
},
|
|
16
|
+
subCommands: {
|
|
17
|
+
login: loginCommand,
|
|
18
|
+
logout: logoutCommand,
|
|
19
|
+
init: initCommand,
|
|
20
|
+
deploy: deployCommand,
|
|
21
|
+
dev: devCommand,
|
|
22
|
+
projects: projectsCommand,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
runMain(main);
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
interface ProjectConfig {
|
|
5
|
+
project_id: string;
|
|
6
|
+
project_name: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const PROJECT_FILE = "app-hosting.json";
|
|
10
|
+
|
|
11
|
+
export async function loadProjectConfig(cwd: string): Promise<ProjectConfig | null> {
|
|
12
|
+
try {
|
|
13
|
+
const data = await readFile(join(cwd, PROJECT_FILE), "utf-8");
|
|
14
|
+
return JSON.parse(data) as ProjectConfig;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function saveProjectConfig(cwd: string, config: ProjectConfig): Promise<void> {
|
|
21
|
+
await writeFile(join(cwd, PROJECT_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
22
|
+
}
|
package/src/trpc.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
2
|
+
import type { AppRouter } from "../../src/routers";
|
|
3
|
+
import { loadConfig, getApiUrl } from "./config";
|
|
4
|
+
|
|
5
|
+
export async function createClient() {
|
|
6
|
+
const config = await loadConfig();
|
|
7
|
+
if (!config.token) {
|
|
8
|
+
throw new Error("Not logged in. Run `app-hosting login` first.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const apiUrl = getApiUrl(config);
|
|
12
|
+
|
|
13
|
+
const client = createTRPCClient<AppRouter>({
|
|
14
|
+
links: [
|
|
15
|
+
httpBatchLink({
|
|
16
|
+
url: `${apiUrl}/api`,
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${config.token}`,
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return client;
|
|
25
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"module": "es2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"types": ["node", "@cloudflare/workers-types"],
|
|
10
|
+
"noEmit": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src", "../worker-configuration.d.ts"]
|
|
13
|
+
}
|