@magneticjs/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # @magnetic/cli
2
+
3
+ Build, develop, and deploy Magnetic server-driven UI apps.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @magnetic/cli
9
+ ```
10
+
11
+ This also downloads the `magnetic-v8-server` binary for your platform via postinstall.
12
+
13
+ ## Commands
14
+
15
+ ### `magnetic dev`
16
+
17
+ Start a local development server with hot rebuild on file changes.
18
+
19
+ ```bash
20
+ cd my-app
21
+ magnetic dev # http://localhost:3003
22
+ magnetic dev --port 4000 # custom port
23
+ magnetic dev --dir ./my-app # specify app directory
24
+ ```
25
+
26
+ What happens:
27
+ 1. Scans `pages/` for TSX page components
28
+ 2. Detects `server/state.ts` for business logic
29
+ 3. Auto-generates the V8 bridge (route map + state wiring)
30
+ 4. Bundles with esbuild into a single IIFE
31
+ 5. Starts `magnetic-v8-server` on the specified port
32
+ 6. Watches for changes, rebuilds + restarts on save
33
+
34
+ ### `magnetic build`
35
+
36
+ Generate the production bundle without starting a server.
37
+
38
+ ```bash
39
+ magnetic build --dir ./my-app
40
+ magnetic build --dir ./my-app --minify --verbose
41
+ ```
42
+
43
+ Output: `dist/app.js` (~15KB typical)
44
+
45
+ ### `magnetic push`
46
+
47
+ Build and deploy to a Magnetic platform server.
48
+
49
+ ```bash
50
+ magnetic push --dir ./my-app --server https://platform.magnetic.app --name my-app
51
+ ```
52
+
53
+ Or configure in `magnetic.json`:
54
+ ```json
55
+ {
56
+ "name": "my-app",
57
+ "server": "https://platform.magnetic.app"
58
+ }
59
+ ```
60
+
61
+ Then just: `magnetic push`
62
+
63
+ ## How It Works
64
+
65
+ The CLI is a **build tool**, not a runtime. It:
66
+ - Scans your `pages/` directory and maps filenames to routes
67
+ - Generates a bridge file that wires pages + state + router
68
+ - Bundles everything into a single JS file for the V8 engine
69
+ - The Rust V8 server executes this bundle to render JSON DOM descriptors
70
+
71
+ **You write**: pages, components, state
72
+ **CLI generates**: bridge, bundle
73
+ **Rust server runs**: V8 + HTTP + SSE
74
+
75
+ ## File Conventions
76
+
77
+ | File | Route |
78
+ |------|-------|
79
+ | `pages/IndexPage.tsx` | `/` |
80
+ | `pages/AboutPage.tsx` | `/about` |
81
+ | `pages/SettingsPage.tsx` | `/settings` |
82
+ | `pages/[id].tsx` | `/:id` (dynamic) |
83
+ | `pages/NotFoundPage.tsx` | `*` (catch-all) |
84
+
85
+ ## Server Binary
86
+
87
+ The Rust binary (`magnetic-v8-server`) is automatically downloaded during
88
+ `npm install`. If the download fails, you can build from source:
89
+
90
+ ```bash
91
+ cd rs/crates/magnetic-v8-server
92
+ cargo build --release
93
+ ```
94
+
95
+ Supported platforms: macOS (ARM64, x64), Linux (x64, ARM64)
96
+
97
+ ## License
98
+
99
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,498 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { resolve as resolve3, join as join4 } from "node:path";
5
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
6
+
7
+ // src/generator.ts
8
+ import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
9
+ import { join, relative, extname, basename } from "node:path";
10
+ var PAGE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
11
+ var CATCH_ALL_NAMES = ["notfound", "404", "notfoundpage", "_404", "error"];
12
+ var INDEX_NAMES = ["index", "indexpage", "home", "homepage", "tasks", "taskspage", "main", "mainpage"];
13
+ function scanApp(appDir, monorepoRoot) {
14
+ const pagesDir = join(appDir, "pages");
15
+ const pages = [];
16
+ if (existsSync(pagesDir)) {
17
+ scanDir(pagesDir, "", pages);
18
+ }
19
+ const stateCandidates = [
20
+ "state.ts",
21
+ "state.tsx",
22
+ "server/state.ts",
23
+ "server/state.tsx",
24
+ "store.ts",
25
+ "store.tsx"
26
+ ];
27
+ let statePath = null;
28
+ let hasViewModel = false;
29
+ for (const candidate of stateCandidates) {
30
+ const full = join(appDir, candidate);
31
+ if (existsSync(full)) {
32
+ statePath = "./" + candidate;
33
+ const content = readFileSync(full, "utf-8");
34
+ hasViewModel = /export\s+(function|const)\s+toViewModel/.test(content);
35
+ break;
36
+ }
37
+ }
38
+ let serverPkgPath;
39
+ if (monorepoRoot) {
40
+ const relPath = relative(appDir, join(monorepoRoot, "js/packages/magnetic-server/src"));
41
+ serverPkgPath = relPath.startsWith(".") ? relPath : "./" + relPath;
42
+ } else {
43
+ serverPkgPath = "@magneticjs/server";
44
+ }
45
+ return { pages, statePath, hasViewModel, serverPkgPath };
46
+ }
47
+ function scanDir(dir, pathPrefix, pages, rootPagesDir) {
48
+ const pagesRoot = rootPagesDir || dir;
49
+ const entries = readdirSync(dir).sort();
50
+ for (const entry of entries) {
51
+ const fullPath = join(dir, entry);
52
+ const stat = statSync(fullPath);
53
+ if (stat.isDirectory()) {
54
+ let segment = entry;
55
+ if (entry.startsWith("[") && entry.endsWith("]")) {
56
+ segment = ":" + entry.slice(1, -1);
57
+ }
58
+ scanDir(fullPath, pathPrefix + "/" + segment, pages, pagesRoot);
59
+ continue;
60
+ }
61
+ const ext = extname(entry);
62
+ if (!PAGE_EXTENSIONS.includes(ext)) continue;
63
+ const nameNoExt = basename(entry, ext);
64
+ if (nameNoExt.startsWith("_") && !CATCH_ALL_NAMES.includes(nameNoExt.toLowerCase())) continue;
65
+ if (nameNoExt === "layout") continue;
66
+ const importName = nameNoExt;
67
+ const nameLower = nameNoExt.toLowerCase().replace(/page$/, "");
68
+ let routePath;
69
+ let isCatchAll = false;
70
+ if (CATCH_ALL_NAMES.includes(nameLower)) {
71
+ routePath = "*";
72
+ isCatchAll = true;
73
+ } else if (INDEX_NAMES.includes(nameLower)) {
74
+ routePath = pathPrefix || "/";
75
+ } else if (nameNoExt.startsWith("[") && nameNoExt.endsWith("]")) {
76
+ const param = nameNoExt.slice(1, -1);
77
+ routePath = pathPrefix + "/:" + param;
78
+ } else {
79
+ routePath = pathPrefix + "/" + nameLower;
80
+ }
81
+ const relFromPagesRoot = relative(pagesRoot, fullPath).replace(/\\/g, "/");
82
+ const filePath = "pages/" + relFromPagesRoot;
83
+ pages.push({ filePath, importName, routePath, isCatchAll });
84
+ }
85
+ }
86
+ function generateBridge(scan) {
87
+ const lines = [];
88
+ lines.push("// AUTO-GENERATED by @magnetic/cli \u2014 do not edit");
89
+ lines.push(`import { createRouter } from '${scan.serverPkgPath}/router.ts';`);
90
+ const catchAllPage = scan.pages.find((p) => p.isCatchAll);
91
+ for (const page of scan.pages) {
92
+ lines.push(`import { ${page.importName} } from './${page.filePath}';`);
93
+ }
94
+ if (scan.statePath) {
95
+ lines.push(`import { initialState, reduce as _reduce${scan.hasViewModel ? ", toViewModel" : ""} } from '${scan.statePath}';`);
96
+ } else {
97
+ lines.push("");
98
+ lines.push("// No state.ts found \u2014 using minimal default state");
99
+ lines.push("function initialState() { return {}; }");
100
+ lines.push("function _reduce(state, action, payload) { return state; }");
101
+ }
102
+ if (!scan.hasViewModel && scan.statePath) {
103
+ lines.push("function toViewModel(s) { return s; }");
104
+ } else if (!scan.statePath) {
105
+ lines.push("function toViewModel(s) { return s; }");
106
+ }
107
+ lines.push("");
108
+ lines.push("const router = createRouter([");
109
+ for (const page of scan.pages) {
110
+ if (!page.isCatchAll) {
111
+ lines.push(` { path: '${page.routePath}', page: ${page.importName} },`);
112
+ }
113
+ }
114
+ if (catchAllPage) {
115
+ lines.push(` { path: '*', page: ${catchAllPage.importName} },`);
116
+ }
117
+ lines.push("]);");
118
+ lines.push("");
119
+ lines.push("let state = initialState();");
120
+ lines.push("");
121
+ lines.push("export function render(path) {");
122
+ lines.push(" const vm = toViewModel(state);");
123
+ lines.push(" const result = router.resolve(path, vm);");
124
+ if (catchAllPage) {
125
+ lines.push(` if (!result) return ${catchAllPage.importName}({ params: {} });`);
126
+ } else {
127
+ lines.push(' if (!result) return { tag: "div", text: "Not Found" };');
128
+ }
129
+ lines.push(" if (result.kind === 'redirect') {");
130
+ lines.push(" const r2 = router.resolve(result.to, vm);");
131
+ lines.push(" if (r2 && r2.kind === 'render') return r2.dom;");
132
+ if (catchAllPage) {
133
+ lines.push(` return ${catchAllPage.importName}({ params: {} });`);
134
+ } else {
135
+ lines.push(' return { tag: "div", text: "Not Found" };');
136
+ }
137
+ lines.push(" }");
138
+ lines.push(" return result.dom;");
139
+ lines.push("}");
140
+ lines.push("");
141
+ lines.push("export function reduce(ap) {");
142
+ lines.push(" const { action, payload = {}, path = '/' } = ap;");
143
+ lines.push(" state = _reduce(state, action, payload);");
144
+ lines.push(" return render(path);");
145
+ lines.push("}");
146
+ return lines.join("\n") + "\n";
147
+ }
148
+
149
+ // src/bundler.ts
150
+ import { build } from "esbuild";
151
+ import { join as join2 } from "node:path";
152
+ import { mkdirSync, existsSync as existsSync2, statSync as statSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
153
+ async function bundleApp(opts) {
154
+ const outDir = opts.outDir || join2(opts.appDir, "dist");
155
+ const outFile = opts.outFile || "app.js";
156
+ const outPath = join2(outDir, outFile);
157
+ if (!existsSync2(outDir)) {
158
+ mkdirSync(outDir, { recursive: true });
159
+ }
160
+ const alias = {};
161
+ if (opts.monorepoRoot) {
162
+ const serverPkg = join2(opts.monorepoRoot, "js/packages/magnetic-server/src");
163
+ alias["@magneticjs/server"] = serverPkg;
164
+ alias["@magneticjs/server/jsx-runtime"] = join2(serverPkg, "jsx-runtime.ts");
165
+ }
166
+ const result = await build({
167
+ stdin: {
168
+ contents: opts.bridgeCode,
169
+ resolveDir: opts.appDir,
170
+ loader: "tsx"
171
+ },
172
+ bundle: true,
173
+ format: "iife",
174
+ globalName: "MagneticApp",
175
+ outfile: outPath,
176
+ minify: opts.minify || false,
177
+ sourcemap: false,
178
+ target: "es2020",
179
+ jsx: "automatic",
180
+ jsxImportSource: "@magneticjs/server",
181
+ alias,
182
+ logLevel: "warning"
183
+ });
184
+ const stat = statSync2(outPath);
185
+ return {
186
+ outPath,
187
+ sizeBytes: stat.size
188
+ };
189
+ }
190
+ async function buildForDeploy(opts) {
191
+ const bundle = await bundleApp({ ...opts, minify: true });
192
+ const publicDir = join2(opts.appDir, "public");
193
+ const assets = {};
194
+ if (existsSync2(publicDir)) {
195
+ const entries = readdirSync2(publicDir);
196
+ for (const entry of entries) {
197
+ const fullPath = join2(publicDir, entry);
198
+ if (statSync2(fullPath).isFile()) {
199
+ const ext = entry.split(".").pop() || "";
200
+ const textExts = ["css", "js", "json", "html", "svg", "txt", "xml"];
201
+ if (textExts.includes(ext)) {
202
+ assets[entry] = readFileSync2(fullPath, "utf-8");
203
+ } else {
204
+ assets[entry] = readFileSync2(fullPath).toString("base64");
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return {
210
+ bundlePath: bundle.outPath,
211
+ bundleSize: bundle.sizeBytes,
212
+ assets
213
+ };
214
+ }
215
+
216
+ // src/dev.ts
217
+ import { watch } from "node:fs";
218
+ import { join as join3 } from "node:path";
219
+ import { existsSync as existsSync3 } from "node:fs";
220
+ import { spawn } from "node:child_process";
221
+ async function startDev(opts) {
222
+ const {
223
+ appDir,
224
+ port = 3003,
225
+ monorepoRoot
226
+ } = opts;
227
+ const staticDir = join3(appDir, "public");
228
+ const outDir = join3(appDir, "dist");
229
+ const serverBin = opts.serverBin || findServerBinary(monorepoRoot || appDir);
230
+ if (!serverBin) {
231
+ console.error("[magnetic] Cannot find magnetic-v8-server binary.");
232
+ console.error(" Build it with: cargo build --release -p magnetic-v8-server");
233
+ process.exit(1);
234
+ }
235
+ let serverProcess = null;
236
+ async function rebuild() {
237
+ const start = Date.now();
238
+ try {
239
+ const scan = scanApp(appDir, monorepoRoot);
240
+ console.log(`[magnetic] Scanned ${scan.pages.length} pages, state: ${scan.statePath || "none"}`);
241
+ for (const page of scan.pages) {
242
+ console.log(` ${page.routePath} \u2192 ${page.filePath} (${page.importName})`);
243
+ }
244
+ const bridgeCode = generateBridge(scan);
245
+ const result = await bundleApp({
246
+ appDir,
247
+ bridgeCode,
248
+ outDir,
249
+ monorepoRoot
250
+ });
251
+ const ms = Date.now() - start;
252
+ const kb = (result.sizeBytes / 1024).toFixed(1);
253
+ console.log(`[magnetic] Built ${result.outPath} (${kb}KB) in ${ms}ms`);
254
+ return result.outPath;
255
+ } catch (err) {
256
+ console.error(`[magnetic] Build failed: ${err.message}`);
257
+ return null;
258
+ }
259
+ }
260
+ function startServer(bundlePath2) {
261
+ const args2 = [
262
+ "--bundle",
263
+ bundlePath2,
264
+ "--port",
265
+ String(port),
266
+ "--static",
267
+ staticDir
268
+ ];
269
+ console.log(`[magnetic] Starting V8 server on :${port}`);
270
+ const proc = spawn(serverBin, args2, {
271
+ stdio: ["ignore", "inherit", "inherit"]
272
+ });
273
+ proc.on("exit", (code) => {
274
+ if (code !== null && code !== 0) {
275
+ console.error(`[magnetic] Server exited with code ${code}`);
276
+ }
277
+ });
278
+ return proc;
279
+ }
280
+ function stopServer() {
281
+ if (serverProcess) {
282
+ serverProcess.kill("SIGTERM");
283
+ serverProcess = null;
284
+ }
285
+ }
286
+ const bundlePath = await rebuild();
287
+ if (!bundlePath) {
288
+ console.error("[magnetic] Initial build failed. Fix errors and save to retry.");
289
+ } else {
290
+ serverProcess = startServer(bundlePath);
291
+ }
292
+ const watchDirs = [join3(appDir, "pages"), join3(appDir, "components")];
293
+ const watchFiles = ["state.ts", "state.tsx", "server/state.ts", "server/state.tsx"].map((f) => join3(appDir, f));
294
+ let rebuildTimer = null;
295
+ function scheduleRebuild() {
296
+ if (rebuildTimer) clearTimeout(rebuildTimer);
297
+ rebuildTimer = setTimeout(async () => {
298
+ console.log("\n[magnetic] Change detected, rebuilding...");
299
+ stopServer();
300
+ const path = await rebuild();
301
+ if (path) {
302
+ serverProcess = startServer(path);
303
+ }
304
+ }, 200);
305
+ }
306
+ for (const dir of watchDirs) {
307
+ if (existsSync3(dir)) {
308
+ watch(dir, { recursive: true }, (event, filename) => {
309
+ if (filename && /\.(tsx?|jsx?|css)$/.test(filename)) {
310
+ scheduleRebuild();
311
+ }
312
+ });
313
+ console.log(`[magnetic] Watching ${dir}`);
314
+ }
315
+ }
316
+ for (const file of watchFiles) {
317
+ if (existsSync3(file)) {
318
+ watch(file, () => scheduleRebuild());
319
+ console.log(`[magnetic] Watching ${file}`);
320
+ }
321
+ }
322
+ process.on("SIGINT", () => {
323
+ console.log("\n[magnetic] Shutting down...");
324
+ stopServer();
325
+ process.exit(0);
326
+ });
327
+ process.on("SIGTERM", () => {
328
+ stopServer();
329
+ process.exit(0);
330
+ });
331
+ console.log(`[magnetic] Dev mode ready. Edit pages/ and save to rebuild.`);
332
+ console.log(`[magnetic] http://localhost:${port}
333
+ `);
334
+ }
335
+ function findServerBinary(searchRoot) {
336
+ const cliPkgBin = join3(import.meta.dirname || __dirname, "..", "bin", "magnetic-v8-server");
337
+ const candidates = [
338
+ // npm-installed binary (from postinstall)
339
+ cliPkgBin,
340
+ // Monorepo development paths
341
+ join3(searchRoot, "rs/crates/magnetic-v8-server/target/debug/magnetic-v8-server"),
342
+ join3(searchRoot, "rs/crates/magnetic-v8-server/target/release/magnetic-v8-server"),
343
+ join3(searchRoot, "target/debug/magnetic-v8-server"),
344
+ join3(searchRoot, "target/release/magnetic-v8-server")
345
+ ];
346
+ for (const path of candidates) {
347
+ if (existsSync3(path)) return path;
348
+ }
349
+ return null;
350
+ }
351
+
352
+ // src/cli.ts
353
+ var args = process.argv.slice(2);
354
+ var command = args[0];
355
+ function log(level, msg) {
356
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
357
+ const prefix = level === "error" ? "\u2717" : level === "warn" ? "\u26A0" : level === "debug" ? "\xB7" : "\u2192";
358
+ const stream = level === "error" ? process.stderr : process.stdout;
359
+ stream.write(`[${ts}] ${prefix} ${msg}
360
+ `);
361
+ }
362
+ function findMonorepoRoot(from) {
363
+ let dir = from;
364
+ for (let i = 0; i < 10; i++) {
365
+ if (existsSync4(join4(dir, "js/packages/magnetic-server"))) return dir;
366
+ const parent = resolve3(dir, "..");
367
+ if (parent === dir) break;
368
+ dir = parent;
369
+ }
370
+ return null;
371
+ }
372
+ function getArg(flag) {
373
+ const idx = args.indexOf(flag);
374
+ return idx >= 0 ? args[idx + 1] : void 0;
375
+ }
376
+ function usage() {
377
+ console.log(`
378
+ @magnetic/cli \u2014 Build and deploy server-driven UI apps
379
+
380
+ Usage:
381
+ magnetic dev Start dev mode (watch + rebuild + serve)
382
+ magnetic build Build the app bundle for deployment
383
+ magnetic push Build and deploy to a Magnetic platform server
384
+
385
+ Options:
386
+ --port <n> Dev server port (default: 3003)
387
+ --dir <path> App directory (default: current directory)
388
+ --server <url> Platform server URL for push
389
+ --name <name> App name for push (default: from magnetic.json)
390
+ --minify Minify the output bundle
391
+
392
+ Developer workflow:
393
+ 1. Write pages in pages/*.tsx
394
+ 2. Write business logic in state.ts (optional)
395
+ 3. Run \`magnetic dev\` to develop locally
396
+ 4. Run \`magnetic push\` to deploy
397
+ `);
398
+ }
399
+ async function main() {
400
+ if (!command || command === "--help" || command === "-h") {
401
+ usage();
402
+ process.exit(0);
403
+ }
404
+ const appDir = resolve3(getArg("--dir") || ".");
405
+ const monorepoRoot = findMonorepoRoot(appDir);
406
+ const port = parseInt(getArg("--port") || "3003", 10);
407
+ let config = {};
408
+ const configPath = join4(appDir, "magnetic.json");
409
+ if (existsSync4(configPath)) {
410
+ config = JSON.parse(readFileSync3(configPath, "utf-8"));
411
+ }
412
+ switch (command) {
413
+ case "dev": {
414
+ await startDev({
415
+ appDir,
416
+ port,
417
+ monorepoRoot: monorepoRoot || void 0
418
+ });
419
+ break;
420
+ }
421
+ case "build": {
422
+ log("info", `Building ${appDir}`);
423
+ const buildStart = Date.now();
424
+ const scan = scanApp(appDir, monorepoRoot || void 0);
425
+ log("info", `Scanned: ${scan.pages.length} pages, state: ${scan.statePath || "none (using defaults)"}`);
426
+ for (const page of scan.pages) {
427
+ log("debug", ` route ${page.routePath.padEnd(15)} \u2190 ${page.filePath}`);
428
+ }
429
+ const bridgeCode = generateBridge(scan);
430
+ log("debug", `Bridge generated: ${bridgeCode.split("\n").length} lines`);
431
+ if (args.includes("--verbose")) {
432
+ console.log("\n--- Generated bridge ---");
433
+ console.log(bridgeCode);
434
+ console.log("--- End bridge ---\n");
435
+ }
436
+ const result = await bundleApp({
437
+ appDir,
438
+ bridgeCode,
439
+ minify: args.includes("--minify"),
440
+ monorepoRoot: monorepoRoot || void 0
441
+ });
442
+ const kb = (result.sizeBytes / 1024).toFixed(1);
443
+ const elapsed = Date.now() - buildStart;
444
+ log("info", `\u2713 Built ${result.outPath} (${kb}KB) in ${elapsed}ms`);
445
+ break;
446
+ }
447
+ case "push": {
448
+ const serverUrl = getArg("--server") || config.server;
449
+ const appName = getArg("--name") || config.name;
450
+ if (!serverUrl) {
451
+ console.error('[magnetic] No server URL. Use --server <url> or set "server" in magnetic.json');
452
+ process.exit(1);
453
+ }
454
+ if (!appName) {
455
+ console.error('[magnetic] No app name. Use --name <name> or set "name" in magnetic.json');
456
+ process.exit(1);
457
+ }
458
+ log("info", `Building for deploy...`);
459
+ const scan = scanApp(appDir, monorepoRoot || void 0);
460
+ log("info", `Scanned: ${scan.pages.length} pages, state: ${scan.statePath || "none"}`);
461
+ const bridgeCode = generateBridge(scan);
462
+ const deploy = await buildForDeploy({ appDir, bridgeCode, monorepoRoot: monorepoRoot || void 0 });
463
+ log("info", `Bundle: ${(deploy.bundleSize / 1024).toFixed(1)}KB (minified)`);
464
+ log("info", `Assets: ${Object.keys(deploy.assets).length} files`);
465
+ for (const [name, content] of Object.entries(deploy.assets)) {
466
+ log("debug", ` asset: ${name} (${(content.length / 1024).toFixed(1)}KB)`);
467
+ }
468
+ log("info", `Pushing to ${serverUrl}/api/apps/${appName}/deploy...`);
469
+ const bundleContent = readFileSync3(deploy.bundlePath, "utf-8");
470
+ const resp = await fetch(`${serverUrl}/api/apps/${appName}/deploy`, {
471
+ method: "POST",
472
+ headers: { "Content-Type": "application/json" },
473
+ body: JSON.stringify({
474
+ bundle: bundleContent,
475
+ assets: deploy.assets
476
+ })
477
+ });
478
+ if (resp.ok) {
479
+ const data = await resp.json();
480
+ log("info", `\u2713 Deployed! ${data.url || serverUrl + "/apps/" + appName + "/"}`);
481
+ log("info", ` Live at: ${serverUrl}/apps/${appName}/`);
482
+ } else {
483
+ const text = await resp.text();
484
+ log("error", `Deploy failed (${resp.status}): ${text}`);
485
+ process.exit(1);
486
+ }
487
+ break;
488
+ }
489
+ default:
490
+ console.error(`[magnetic] Unknown command: ${command}`);
491
+ usage();
492
+ process.exit(1);
493
+ }
494
+ }
495
+ main().catch((err) => {
496
+ console.error(`[magnetic] Fatal: ${err.message}`);
497
+ process.exit(1);
498
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@magneticjs/cli",
3
+ "version": "0.1.0",
4
+ "description": "Magnetic CLI — build, dev, and deploy server-driven UI apps",
5
+ "type": "module",
6
+ "bin": {
7
+ "magnetic": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=dist/cli.js --external:esbuild",
11
+ "dev": "tsx src/cli.ts",
12
+ "postinstall": "node scripts/install-server.js || true"
13
+ },
14
+ "files": ["dist", "src", "scripts"],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "dependencies": {
19
+ "@magneticjs/server": "^0.1.0",
20
+ "esbuild": "^0.27.3"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^25.2.3",
24
+ "tsx": "^4.21.0"
25
+ },
26
+ "repository": { "type": "git", "url": "https://github.com/inventhq/magnetic.git", "directory": "js/packages/magnetic-cli" },
27
+ "homepage": "https://github.com/inventhq/magnetic#readme",
28
+ "keywords": ["magnetic", "cli", "server-driven-ui", "build-tool"],
29
+ "license": "MIT"
30
+ }