@nevios/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/bin/nevios.mjs ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * nevios — the storefront developer CLI.
4
+ *
5
+ * nevios start [--dir .] # dev server against Nevios
6
+ * nevios push [--dir ./build/server] [--build] [--store <h>] [--account <h>]
7
+ * nevios undeploy [--store <h>] [--account <h>]
8
+ *
9
+ * `start` runs your store's dev server (react-router dev) wired to the Nevios API.
10
+ * `push` deploys your built React store to Nevios hosting (Cloudflare Workers-
11
+ * for-Platforms). With --build it runs `react-router build` first, then uploads
12
+ * `build/server/*` to Nevios, which hosts it at your store's domain.
13
+ *
14
+ * Config: CLI flag → env → ./nevios.json. Auth token in NEVIOS_PAT (server-only).
15
+ * `.env.local` / `.env` are auto-loaded so local runs work without exporting.
16
+ */
17
+
18
+ import { execSync, spawn } from "node:child_process";
19
+ import { readdir, readFile } from "node:fs/promises";
20
+ import { resolve } from "node:path";
21
+
22
+ import { collectAssets, collectModules, deployStore, detectGit, undeployStore } from "../src/index.mjs";
23
+
24
+ function parseArgs(argv) {
25
+ const a = { _: [] };
26
+ for (let i = 0; i < argv.length; i++) {
27
+ const t = argv[i];
28
+ if (t.startsWith("--")) {
29
+ const k = t.slice(2);
30
+ if (k === "build" || k === "dry-run") a[k] = true;
31
+ else a[k] = argv[++i];
32
+ } else a._.push(t);
33
+ }
34
+ return a;
35
+ }
36
+
37
+ async function loadDotenv(cwd) {
38
+ for (const name of [".env.local", ".env"]) {
39
+ let raw;
40
+ try {
41
+ raw = await readFile(resolve(cwd, name), "utf8");
42
+ } catch {
43
+ continue;
44
+ }
45
+ for (const line of raw.split("\n")) {
46
+ const m = /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
47
+ if (!m || process.env[m[1]] !== undefined) continue;
48
+ let v = m[2];
49
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
50
+ process.env[m[1]] = v;
51
+ }
52
+ }
53
+ }
54
+
55
+ async function loadConfig(cwd) {
56
+ try {
57
+ return JSON.parse(await readFile(resolve(cwd, "nevios.json"), "utf8"));
58
+ } catch {
59
+ return {};
60
+ }
61
+ }
62
+
63
+ function run(cmd, args, cwd) {
64
+ return new Promise((res, rej) => {
65
+ const p = spawn(cmd, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
66
+ p.on("close", (code) => (code === 0 ? res() : rej(new Error(`${cmd} exited ${code}`))));
67
+ p.on("error", rej);
68
+ });
69
+ }
70
+
71
+ const fail = (m) => {
72
+ console.error(`\n✗ ${m}\n`);
73
+ process.exit(1);
74
+ };
75
+
76
+ async function main() {
77
+ const args = parseArgs(process.argv.slice(2));
78
+ const cmd = args._[0];
79
+ if (!cmd || !["start", "push", "preview", "undeploy"].includes(cmd)) {
80
+ fail(`usage: nevios <start|push|preview|undeploy> [--build] [--dir <path>] [--store h] [--account h]`);
81
+ }
82
+ const cwd = process.cwd();
83
+ await loadDotenv(cwd);
84
+ const cfg = await loadConfig(cwd);
85
+
86
+ if (cmd === "start") {
87
+ // Dev server against Nevios. The store reads NEXT_PUBLIC_/VITE_ Nevios vars
88
+ // from its own .env.local; we just launch its dev server in one command.
89
+ const apiUrl = process.env.NEVIOS_API_URL ?? process.env.NEVIOS_COMMERCE_URL ?? cfg.apiUrl ?? "(store .env)";
90
+ console.log(`\nnevios start — dev server against Nevios (${apiUrl})`);
91
+ if (args["dry-run"]) {
92
+ console.log(` would run: react-router dev\n`);
93
+ return;
94
+ }
95
+ await run("npx", ["react-router", "dev"], resolve(cwd, args.dir ?? ".")).catch((e) =>
96
+ fail(`dev server failed: ${e.message} (is this a React Router store dir?)`),
97
+ );
98
+ return;
99
+ }
100
+
101
+ const account = args.account ?? process.env.NEVIOS_ACCOUNT ?? cfg.account;
102
+ const store = args.store ?? process.env.NEVIOS_STORE ?? cfg.store;
103
+ const apiUrl =
104
+ args.api ?? process.env.NEVIOS_API_URL ?? process.env.NEVIOS_COMMERCE_URL ?? cfg.apiUrl ?? "https://c.nevios.io";
105
+ const token = args.token ?? process.env.NEVIOS_PAT ?? cfg.token;
106
+ if (!account) fail("account is required (--account / NEVIOS_ACCOUNT / nevios.json)");
107
+ if (!store) fail("store is required (--store / NEVIOS_STORE / nevios.json)");
108
+ if (!token) fail("token is required (--token / NEVIOS_PAT)");
109
+
110
+ if (cmd === "undeploy") {
111
+ console.log(`\nnevios undeploy — ${store} @ ${account}`);
112
+ const sf = await undeployStore({ apiUrl, token, account, store }).catch((e) => fail(e.message));
113
+ console.log(`✓ isolate removed — back to the shared default worker\n`);
114
+ return sf;
115
+ }
116
+
117
+ // push / preview — same build + upload, different slot
118
+ const isPreview = cmd === "preview";
119
+ const buildDir = resolve(cwd, args.dir ?? cfg.dir ?? "./build/server");
120
+ if (args.build) {
121
+ console.log(`\nnevios ${cmd} — building (react-router build)…`);
122
+ await run("npx", ["react-router", "build"], cwd).catch((e) => fail(`build failed: ${e.message}`));
123
+ }
124
+ let modules;
125
+ try {
126
+ modules = await collectModules(buildDir, { readdir, readFile });
127
+ } catch (e) {
128
+ fail(`could not read build dir "${buildDir}": ${e.message} (did you build first? try --build)`);
129
+ }
130
+ const names = Object.keys(modules);
131
+ if (!names.length) fail(`no built modules in ${buildDir} — run \`react-router build\` (or pass --build)`);
132
+
133
+ // client bundle (build/client, sibling of build/server) → Workers Static Assets
134
+ const assetsDir = resolve(cwd, args.assets ?? cfg.assets ?? resolve(buildDir, "../client"));
135
+ const assets = await collectAssets(assetsDir, { readdir, readFileBuffer: (p) => readFile(p) });
136
+ // compatibility flags from the RR build (nodejs_compat etc.)
137
+ let compatibilityFlags;
138
+ try {
139
+ compatibilityFlags = JSON.parse(await readFile(resolve(buildDir, "wrangler.json"), "utf8")).compatibility_flags;
140
+ } catch {
141
+ /* no wrangler.json — backend defaults to nodejs_compat */
142
+ }
143
+
144
+ console.log(`\nnevios ${cmd} — ${store} @ ${account} → ${apiUrl}`);
145
+ console.log(
146
+ ` ${names.length} module(s), ${Object.keys(assets).length} asset(s)${compatibilityFlags ? ` · flags: ${compatibilityFlags.join(",")}` : ""}`,
147
+ );
148
+ if (args["dry-run"]) {
149
+ console.log(`\n(dry run — nothing pushed)\n`);
150
+ return;
151
+ }
152
+ // Git provenance — logged to the deploy history. CI sets NEVIOS_TRIGGERED_BY.
153
+ const git = detectGit((c) => execSync(c, { cwd, stdio: ["ignore", "pipe", "ignore"] }));
154
+ // GitHub Actions checks out in DETACHED HEAD, so `rev-parse --abbrev-ref HEAD`
155
+ // returns the literal "HEAD" — prefer the CI-provided branch/tag name.
156
+ const sourceRef = process.env.GITHUB_REF_NAME || (git.ref && git.ref !== "HEAD" ? git.ref : null);
157
+ if (git.commit) console.log(` commit ${git.commit.slice(0, 7)}${sourceRef ? ` (${sourceRef})` : ""}`);
158
+ const sf = await deployStore({
159
+ apiUrl,
160
+ token,
161
+ account,
162
+ store,
163
+ modules,
164
+ assets,
165
+ compatibilityFlags,
166
+ preview: isPreview,
167
+ sourceCommit: git.commit,
168
+ sourceRef,
169
+ triggeredBy: process.env.NEVIOS_TRIGGERED_BY || "cli",
170
+ }).catch((e) => fail(e.message));
171
+ if (isPreview) {
172
+ console.log(`\n✓ preview deployed — ${sf.preview?.url}\n (live store untouched)\n`);
173
+ } else {
174
+ console.log(
175
+ `\n✓ deployed — worker_script=${sf.worker_script} version=${sf.worker_version}\n live at https://${sf.host}\n`,
176
+ );
177
+ }
178
+ return sf;
179
+ }
180
+
181
+ main().catch((e) => fail(e.stack || e.message));
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@nevios/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "nevios \u2014 the storefront developer CLI: push a React store to Nevios hosting (Cloudflare), preview, and scaffold.",
6
+ "bin": {
7
+ "nevios": "bin/nevios.mjs"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.mjs"
11
+ },
12
+ "scripts": {
13
+ "test": "node --test test/*.test.mjs"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "src"
21
+ ]
22
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @nevios/cli — core ops for the storefront developer CLI.
3
+ *
4
+ * `push` deploys a built React store to Nevios hosting: it collects the built
5
+ * Cloudflare Worker ESM modules (e.g. `build/server/*`) and POSTs them to the
6
+ * Nevios admin deploy endpoint, which uploads the isolate to Workers-for-
7
+ * Platforms and points the store's host at it (dispatcher → isolate). The store
8
+ * never holds Cloudflare credentials — the platform does the CF upload.
9
+ *
10
+ * Pure functions (fs/fetch injected) so they unit-test without a real project.
11
+ */
12
+
13
+ const MODULE_RE = /\.(js|mjs)$/;
14
+ const DEFAULT_VERSION = "2026-01";
15
+
16
+ /**
17
+ * Best-effort git provenance for the current working tree → { commit, ref }.
18
+ * `exec` is injected (a sync `(cmd) => string`) so this unit-tests without git.
19
+ * Returns nulls when not a git repo / git unavailable — deploy still proceeds.
20
+ */
21
+ export function detectGit(exec) {
22
+ const run = (cmd) => {
23
+ try {
24
+ return String(exec(cmd)).trim() || null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ };
29
+ return {
30
+ commit: run("git rev-parse HEAD"),
31
+ ref: run("git rev-parse --abbrev-ref HEAD"),
32
+ };
33
+ }
34
+
35
+ /** Recursively collect built Worker modules from a dir → { relPath: source }. */
36
+ export async function collectModules(dir, fs) {
37
+ const out = {};
38
+ async function walk(absDir, relDir) {
39
+ const entries = await fs.readdir(absDir, { withFileTypes: true });
40
+ for (const e of entries) {
41
+ const rel = relDir ? `${relDir}/${e.name}` : e.name;
42
+ if (e.isDirectory()) {
43
+ await walk(`${absDir}/${e.name}`, rel);
44
+ } else if (MODULE_RE.test(e.name)) {
45
+ out[rel] = await fs.readFile(`${absDir}/${e.name}`, "utf8");
46
+ }
47
+ }
48
+ }
49
+ await walk(dir, "");
50
+ return out;
51
+ }
52
+
53
+ /** Collect the built client bundle (build/client) → { site-path: base64 }. */
54
+ export async function collectAssets(dir, fs) {
55
+ const out = {};
56
+ async function walk(absDir, relDir) {
57
+ let entries;
58
+ try {
59
+ entries = await fs.readdir(absDir, { withFileTypes: true });
60
+ } catch {
61
+ return;
62
+ }
63
+ for (const e of entries) {
64
+ if (e.name === ".assetsignore") continue;
65
+ const rel = relDir ? `${relDir}/${e.name}` : e.name;
66
+ if (e.isDirectory()) {
67
+ await walk(`${absDir}/${e.name}`, rel);
68
+ } else {
69
+ const buf = await fs.readFileBuffer(`${absDir}/${e.name}`);
70
+ out[`/${rel}`] = buf.toString("base64");
71
+ }
72
+ }
73
+ }
74
+ await walk(dir, "");
75
+ return out;
76
+ }
77
+
78
+ /** Pick the entry module: `index.js` if present, else the shallowest .js. */
79
+ export function pickMainModule(modules) {
80
+ const names = Object.keys(modules);
81
+ if (names.includes("index.js")) return "index.js";
82
+ const sorted = names
83
+ .filter((n) => n.endsWith(".js"))
84
+ .sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
85
+ return sorted[0] ?? names[0];
86
+ }
87
+
88
+ /**
89
+ * POST a built isolate to the Nevios admin deploy endpoint. Auth is a platform
90
+ * token (PAT or JWT) — the account owner. Returns the updated storefront.
91
+ */
92
+ export async function deployStore({
93
+ apiUrl,
94
+ token,
95
+ account,
96
+ store,
97
+ modules,
98
+ mainModule,
99
+ version,
100
+ assets,
101
+ compatibilityFlags,
102
+ preview = false,
103
+ sourceCommit,
104
+ sourceRef,
105
+ triggeredBy,
106
+ apiVersion = DEFAULT_VERSION,
107
+ fetch = globalThis.fetch,
108
+ }) {
109
+ if (!apiUrl) throw new Error("push: apiUrl is required");
110
+ if (!token) throw new Error("push: token (NEVIOS_PAT) is required");
111
+ if (!account) throw new Error("push: account handle is required");
112
+ if (!store) throw new Error("push: store handle is required");
113
+ if (!modules || !Object.keys(modules).length) throw new Error("push: no built modules found");
114
+ const main = mainModule || pickMainModule(modules);
115
+ const base = apiUrl.replace(/\/+$/, "");
116
+ const url = `${base}/admin/${apiVersion}/${encodeURIComponent(account)}/storefronts/${encodeURIComponent(store)}/deploy.json`;
117
+
118
+ const body = { modules, main_module: main, version, preview };
119
+ if (assets && Object.keys(assets).length) body.assets = assets;
120
+ if (compatibilityFlags && compatibilityFlags.length) body.compatibility_flags = compatibilityFlags;
121
+ if (sourceCommit) body.source_commit = sourceCommit;
122
+ if (sourceRef) body.source_ref = sourceRef;
123
+ if (triggeredBy) body.triggered_by = triggeredBy;
124
+
125
+ const res = await fetch(url, {
126
+ method: "POST",
127
+ headers: {
128
+ Authorization: `Bearer ${token}`,
129
+ "Content-Type": "application/json",
130
+ Accept: "application/json",
131
+ },
132
+ body: JSON.stringify(body),
133
+ });
134
+ const text = await res.text();
135
+ let json = null;
136
+ try {
137
+ json = text ? JSON.parse(text) : null;
138
+ } catch {
139
+ json = null;
140
+ }
141
+ if (!res.ok) {
142
+ const msg = json?.error?.message || `deploy failed (HTTP ${res.status})`;
143
+ throw new Error(msg);
144
+ }
145
+ return json?.storefront ?? json;
146
+ }
147
+
148
+ /** Undeploy a store's isolate → back to the shared default. */
149
+ export async function undeployStore({
150
+ apiUrl,
151
+ token,
152
+ account,
153
+ store,
154
+ apiVersion = DEFAULT_VERSION,
155
+ fetch = globalThis.fetch,
156
+ }) {
157
+ const base = apiUrl.replace(/\/+$/, "");
158
+ const url = `${base}/admin/${apiVersion}/${encodeURIComponent(account)}/storefronts/${encodeURIComponent(store)}/undeploy.json`;
159
+ const res = await fetch(url, {
160
+ method: "POST",
161
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
162
+ });
163
+ const text = await res.text();
164
+ let json = null;
165
+ try {
166
+ json = text ? JSON.parse(text) : null;
167
+ } catch {
168
+ json = null;
169
+ }
170
+ if (!res.ok) throw new Error(json?.error?.message || `undeploy failed (HTTP ${res.status})`);
171
+ return json?.storefront ?? json;
172
+ }