easyorders 0.1.1 → 0.1.4

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 (37) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/cli.js +185 -0
  3. package/dist/server/api.js +22 -0
  4. package/dist/server/index.js +255 -0
  5. package/dist/server/token-store.js +24 -0
  6. package/dist/server/tunnel.js +124 -0
  7. package/package.json +7 -5
  8. package/cli/bin/cli.ts +0 -240
  9. package/cli/server/api.ts +0 -56
  10. package/cli/server/index.ts +0 -320
  11. package/cli/server/token-store.ts +0 -35
  12. package/cli/server/tunnel.ts +0 -161
  13. /package/{cli → dist}/template/theme/config.json +0 -0
  14. /package/{cli → dist}/template/theme/schema.json +0 -0
  15. /package/{cli → dist}/template/theme/script.js +0 -0
  16. /package/{cli → dist}/template/theme/sections/breadcrumbs.liquid +0 -0
  17. /package/{cli → dist}/template/theme/sections/categories.liquid +0 -0
  18. /package/{cli → dist}/template/theme/sections/fake-counter.liquid +0 -0
  19. /package/{cli → dist}/template/theme/sections/fake-stock.liquid +0 -0
  20. /package/{cli → dist}/template/theme/sections/fake-visitor.liquid +0 -0
  21. /package/{cli → dist}/template/theme/sections/featured-products.liquid +0 -0
  22. /package/{cli → dist}/template/theme/sections/fixed-buy-button.liquid +0 -0
  23. /package/{cli → dist}/template/theme/sections/footer.liquid +0 -0
  24. /package/{cli → dist}/template/theme/sections/gallery.liquid +0 -0
  25. /package/{cli → dist}/template/theme/sections/header.liquid +0 -0
  26. /package/{cli → dist}/template/theme/sections/home-products-grid.liquid +0 -0
  27. /package/{cli → dist}/template/theme/sections/list-products.liquid +0 -0
  28. /package/{cli → dist}/template/theme/sections/order-invoice.liquid +0 -0
  29. /package/{cli → dist}/template/theme/sections/product-description.liquid +0 -0
  30. /package/{cli → dist}/template/theme/sections/product-details.liquid +0 -0
  31. /package/{cli → dist}/template/theme/sections/products-grid.liquid +0 -0
  32. /package/{cli → dist}/template/theme/sections/related-products.liquid +0 -0
  33. /package/{cli → dist}/template/theme/sections/reviews.liquid +0 -0
  34. /package/{cli → dist}/template/theme/sections/slider.liquid +0 -0
  35. /package/{cli → dist}/template/theme/sections/thanks.liquid +0 -0
  36. /package/{cli → dist}/template/theme/style.css +0 -0
  37. /package/{cli → dist}/template/theme/theme-data.json +0 -0
package/README.md CHANGED
@@ -48,7 +48,7 @@ This will:
48
48
 
49
49
  ### Interactive mode
50
50
 
51
- Running `npx easy-orders` without arguments shows an interactive menu where you can choose to create a theme or start the dev server.
51
+ Running `npx easyorders` without arguments shows an interactive menu where you can choose to create a theme or start the dev server.
52
52
 
53
53
  ## Environment Variables
54
54
 
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import readline from "node:readline";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ import { getCliVersion } from "../server/api.js";
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const CLI_ROOT = path.resolve(__dirname, "..");
12
+ const CYAN = "\x1b[36m";
13
+ const GREEN = "\x1b[32m";
14
+ const RED = "\x1b[31m";
15
+ const DIM = "\x1b[2m";
16
+ const BOLD = "\x1b[1m";
17
+ const RESET = "\x1b[0m";
18
+ const YELLOW = "\x1b[33m";
19
+ // ── Helpers ─────────────────────────────────────────────────
20
+ function ask(question) {
21
+ const rl = readline.createInterface({
22
+ input: process.stdin,
23
+ output: process.stdout,
24
+ });
25
+ return new Promise((resolve) => {
26
+ rl.question(question, (answer) => {
27
+ rl.close();
28
+ resolve(answer.trim());
29
+ });
30
+ });
31
+ }
32
+ function copyDirSync(src, dest) {
33
+ fs.mkdirSync(dest, { recursive: true });
34
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
35
+ const srcPath = path.join(src, entry.name);
36
+ const destPath = path.join(dest, entry.name);
37
+ if (entry.isDirectory()) {
38
+ copyDirSync(srcPath, destPath);
39
+ }
40
+ else {
41
+ fs.copyFileSync(srcPath, destPath);
42
+ }
43
+ }
44
+ }
45
+ // ── Commands ────────────────────────────────────────────────
46
+ async function createTheme() {
47
+ let projectName = process.argv[3];
48
+ if (!projectName) {
49
+ projectName = await ask(` ${BOLD}▸ Theme project name: ${RESET}`);
50
+ }
51
+ if (!projectName) {
52
+ console.error(`\n ${RED}${BOLD}✖ Project name is required.${RESET}\n`);
53
+ process.exit(1);
54
+ }
55
+ const dest = path.resolve(process.cwd(), projectName);
56
+ if (fs.existsSync(dest)) {
57
+ console.error(`\n ${RED}${BOLD}✖ Directory "${projectName}" already exists.${RESET}\n`);
58
+ process.exit(1);
59
+ }
60
+ console.log(`\n ${DIM}Creating theme project${RESET} ${BOLD}${projectName}${RESET}…`);
61
+ // Create project directory
62
+ fs.mkdirSync(dest, { recursive: true });
63
+ // Copy template theme folder
64
+ const templateDir = path.join(CLI_ROOT, "template", "theme");
65
+ const themeDir = path.join(dest, "theme");
66
+ copyDirSync(templateDir, themeDir);
67
+ // Create a minimal package.json for the theme project
68
+ const pkg = {
69
+ name: projectName,
70
+ version: "0.1.0",
71
+ private: true,
72
+ scripts: {
73
+ dev: "easyorders start",
74
+ },
75
+ };
76
+ fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify(pkg, null, 2) + "\n", "utf-8");
77
+ // Create .gitignore
78
+ fs.writeFileSync(path.join(dest, ".gitignore"), [
79
+ "node_modules",
80
+ ".tunnel-url",
81
+ ".tunnel-pid",
82
+ ".auth-ready",
83
+ ".cli-tokens.json",
84
+ ".env",
85
+ ].join("\n") + "\n", "utf-8");
86
+ console.log(` ${GREEN}${BOLD}✔${RESET} Theme project created at ${BOLD}${dest}${RESET}`);
87
+ console.log("");
88
+ console.log(` ${DIM}Next steps:${RESET}`);
89
+ console.log(` ${CYAN}cd ${projectName}${RESET}`);
90
+ console.log(` ${CYAN}npx easyorders start${RESET}`);
91
+ console.log("");
92
+ }
93
+ async function startDev() {
94
+ // Resolve the theme directory from the current working directory
95
+ const cwd = process.cwd();
96
+ const themeDir = path.join(cwd, "theme");
97
+ if (!fs.existsSync(themeDir)) {
98
+ console.error(`\n ${RED}${BOLD}✖ No "theme" folder found in the current directory.${RESET}`);
99
+ console.error(` ${DIM}Run ${CYAN}npx easyorders create-theme <name>${DIM} first to create a project.${RESET}\n`);
100
+ process.exit(1);
101
+ }
102
+ // The server files are bundled inside the CLI package
103
+ const serverDir = path.join(CLI_ROOT, "server");
104
+ const indexScript = path.join(serverDir, "index.js");
105
+ const tunnelScript = path.join(serverDir, "tunnel.js");
106
+ const cmd = `node "${tunnelScript}" & node "${indexScript}"`;
107
+ const child = spawn(cmd, {
108
+ stdio: "inherit",
109
+ cwd,
110
+ env: { ...process.env, THEME_DIR: themeDir },
111
+ shell: true,
112
+ });
113
+ child.on("exit", (code) => {
114
+ process.exit(code ?? 0);
115
+ });
116
+ }
117
+ function showMenu() {
118
+ console.log("");
119
+ console.log(` ${CYAN}${BOLD}Easy Orders CLI${RESET}`);
120
+ console.log("");
121
+ console.log(` ${BOLD}Usage:${RESET}`);
122
+ console.log(` ${CYAN}npx easyorders create-theme ${DIM}<name>${RESET} Create a new theme project`);
123
+ console.log(` ${CYAN}npx easyorders start${RESET} Start the dev server`);
124
+ console.log("");
125
+ }
126
+ async function interactiveMenu() {
127
+ console.log("");
128
+ console.log(` ${CYAN}${BOLD}Easy Orders CLI${RESET}`);
129
+ console.log("");
130
+ console.log(` ${BOLD}What would you like to do?${RESET}`);
131
+ console.log(` ${BOLD}1)${RESET} Create a new theme`);
132
+ console.log(` ${BOLD}2)${RESET} Start dev server`);
133
+ console.log("");
134
+ const choice = await ask(` ${BOLD}▸ Choose (1/2): ${RESET}`);
135
+ switch (choice) {
136
+ case "1":
137
+ await createTheme();
138
+ break;
139
+ case "2":
140
+ await startDev();
141
+ break;
142
+ default:
143
+ console.error(`\n ${RED}${BOLD}✖ Invalid choice.${RESET}\n`);
144
+ showMenu();
145
+ process.exit(1);
146
+ }
147
+ }
148
+ // ── Version check ───────────────────────────────────────────
149
+ async function checkForUpdates() {
150
+ const pkgPath = path.resolve(__dirname, "..", "..", "package.json");
151
+ const localVersion = JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
152
+ try {
153
+ const latestVersion = await getCliVersion();
154
+ if (latestVersion !== localVersion) {
155
+ console.log("");
156
+ console.log(` ${YELLOW}${BOLD}⚠ Update available!${RESET} ${DIM}${localVersion}${RESET} → ${GREEN}${BOLD}${latestVersion}${RESET}`);
157
+ console.log(` ${DIM}Run${RESET} ${CYAN}npm install -g easyorders@latest${RESET} ${DIM}to update.${RESET}`);
158
+ console.log("");
159
+ process.exit(1);
160
+ }
161
+ }
162
+ catch {
163
+ // If the version check fails (e.g. no network), skip silently
164
+ }
165
+ }
166
+ // ── Entry point ─────────────────────────────────────────────
167
+ (async () => {
168
+ await checkForUpdates();
169
+ const command = process.argv[2];
170
+ switch (command) {
171
+ case "create-theme":
172
+ await createTheme();
173
+ break;
174
+ case "start":
175
+ await startDev();
176
+ break;
177
+ case "--help":
178
+ case "-h":
179
+ showMenu();
180
+ break;
181
+ default:
182
+ await interactiveMenu();
183
+ break;
184
+ }
185
+ })();
@@ -0,0 +1,22 @@
1
+ import axios from "axios";
2
+ const API_BASE_URL = process.env.API_BASE_URL || "https://api.easy-orders.net";
3
+ function createClient() {
4
+ return axios.create({
5
+ baseURL: API_BASE_URL,
6
+ headers: { "Content-Type": "application/json" },
7
+ timeout: 10_000,
8
+ });
9
+ }
10
+ const client = createClient();
11
+ export async function generateCliToken(storeName) {
12
+ const { data } = await client.post("/api/v1/themes/cli-token", { store_name: storeName });
13
+ return data;
14
+ }
15
+ export async function checkTokenStatus(tokenId) {
16
+ const { data } = await client.get("/api/v1/themes/cli-token/status", { params: { token_id: tokenId } });
17
+ return data;
18
+ }
19
+ export async function getCliVersion() {
20
+ const { data } = await client.get("/api/v1/themes/cli-version");
21
+ return data.version;
22
+ }
@@ -0,0 +1,255 @@
1
+ import "dotenv/config";
2
+ import cors from "cors";
3
+ import express from "express";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import readline from "node:readline";
7
+ import { exec } from "node:child_process";
8
+ import { generateCliToken, checkTokenStatus } from "./api.js";
9
+ import { getToken, saveToken } from "./token-store.js";
10
+ const CYAN = "\x1b[36m";
11
+ const GREEN = "\x1b[32m";
12
+ const RED = "\x1b[31m";
13
+ const DIM = "\x1b[2m";
14
+ const BOLD = "\x1b[1m";
15
+ const RESET = "\x1b[0m";
16
+ const UNDERLINE = "\x1b[4m";
17
+ const YELLOW = "\x1b[33m";
18
+ const FRONTEND_BASE_URL = process.env.FRONTEND_BASE_URL || "https://app.easy-orders.net";
19
+ const STORE_DOMAIN = process.env.STORE_DOMAIN || "myeasyorders.com";
20
+ const app = express();
21
+ app.use(cors());
22
+ const port = Number(process.env.PORT) || 4000;
23
+ const URL_FILE = path.resolve(process.cwd(), ".tunnel-url");
24
+ const THEME_DIR = process.env.THEME_DIR || path.resolve(process.cwd(), "theme");
25
+ const SECTIONS_DIR = path.join(THEME_DIR, "sections");
26
+ const AUTH_READY_FILE = path.resolve(process.cwd(), ".auth-ready");
27
+ app.use(express.json());
28
+ function getBaseUrl(req) {
29
+ try {
30
+ const tunnelUrl = fs.readFileSync(URL_FILE, "utf-8").trim();
31
+ if (tunnelUrl)
32
+ return tunnelUrl;
33
+ }
34
+ catch { }
35
+ return `${req.protocol}://${req.get("host")}`;
36
+ }
37
+ function toSnakeCase(filename) {
38
+ return filename.replace(/\.liquid$/, "").replace(/-/g, "_");
39
+ }
40
+ function readJsonFile(filepath) {
41
+ return JSON.parse(fs.readFileSync(filepath, "utf-8"));
42
+ }
43
+ function buildSections() {
44
+ const files = fs
45
+ .readdirSync(SECTIONS_DIR)
46
+ .filter((f) => f.endsWith(".liquid"));
47
+ return files.map((file) => ({
48
+ key: toSnakeCase(file),
49
+ template: fs.readFileSync(path.join(SECTIONS_DIR, file), "utf-8"),
50
+ }));
51
+ }
52
+ function extractSubdomain(input) {
53
+ const trimmed = input.trim();
54
+ try {
55
+ const url = new URL(trimmed);
56
+ const hostname = url.hostname;
57
+ const parts = hostname.split(".");
58
+ if (parts.length >= 2) {
59
+ return parts[0];
60
+ }
61
+ }
62
+ catch {
63
+ // Not a URL – continue
64
+ }
65
+ if (trimmed.includes(".")) {
66
+ return trimmed.split(".")[0];
67
+ }
68
+ return trimmed;
69
+ }
70
+ function promptStoreName() {
71
+ const args = process.argv.slice(2);
72
+ const idx = args.indexOf("--store_name");
73
+ if (idx !== -1 && idx + 1 < args.length) {
74
+ return Promise.resolve(args[idx + 1]);
75
+ }
76
+ if (process.env.STORE_NAME) {
77
+ return Promise.resolve(process.env.STORE_NAME);
78
+ }
79
+ const rl = readline.createInterface({
80
+ input: process.stdin,
81
+ output: process.stdout,
82
+ });
83
+ return new Promise((resolve) => {
84
+ console.log(`\n ${CYAN}${BOLD}Enter your store subdomain${RESET} ${DIM}(e.g. "sand" from sand.${STORE_DOMAIN})${RESET}`);
85
+ rl.question(` ${BOLD}▸ Subdomain: ${RESET}`, (answer) => {
86
+ rl.close();
87
+ const storeName = extractSubdomain(answer);
88
+ if (!storeName) {
89
+ console.error(`\n ${RED}${BOLD}✖ Invalid input.${RESET} Please provide a valid subdomain.\n`);
90
+ process.exit(1);
91
+ }
92
+ console.log(` ${GREEN}${BOLD}✔${RESET} ${DIM}Using store:${RESET} ${BOLD}${storeName}${RESET}`);
93
+ resolve(storeName);
94
+ });
95
+ });
96
+ }
97
+ function openBrowser(url) {
98
+ const cmd = process.platform === "darwin"
99
+ ? "open"
100
+ : process.platform === "win32"
101
+ ? "start"
102
+ : "xdg-open";
103
+ exec(`${cmd} "${url}"`);
104
+ }
105
+ function waitForApproval(tokenId) {
106
+ return new Promise((resolve, reject) => {
107
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
108
+ let i = 0;
109
+ const spin = setInterval(() => {
110
+ process.stdout.write(`\r ${YELLOW}${frames[i++ % frames.length]}${RESET} ${DIM}Waiting for approval…${RESET}`);
111
+ }, 100);
112
+ const poll = async () => {
113
+ try {
114
+ const { approved } = await checkTokenStatus(tokenId);
115
+ if (approved) {
116
+ clearInterval(spin);
117
+ process.stdout.write("\r\x1b[K");
118
+ return resolve();
119
+ }
120
+ setTimeout(poll, 3000);
121
+ }
122
+ catch (err) {
123
+ clearInterval(spin);
124
+ process.stdout.write("\r\x1b[K");
125
+ reject(err);
126
+ }
127
+ };
128
+ poll();
129
+ });
130
+ }
131
+ async function authenticate(storeName) {
132
+ const existing = getToken(storeName);
133
+ if (existing) {
134
+ console.log(`\n ${DIM}Found saved token for${RESET} ${BOLD}${storeName}${RESET}${DIM}, checking validity…${RESET}`);
135
+ try {
136
+ const { approved } = await checkTokenStatus(existing.token_id);
137
+ if (approved) {
138
+ console.log(` ${GREEN}${BOLD}✔ Existing token is still valid!${RESET}\n`);
139
+ return existing.token;
140
+ }
141
+ console.log(` ${YELLOW}${BOLD}⚠ Saved token is no longer valid. Generating a new one…${RESET}\n`);
142
+ }
143
+ catch {
144
+ console.log(` ${YELLOW}${BOLD}⚠ Could not verify saved token. Generating a new one…${RESET}\n`);
145
+ }
146
+ }
147
+ console.log(`\n ${DIM}Generating CLI token for store${RESET} ${BOLD}${storeName}${RESET}…`);
148
+ let tokenData;
149
+ try {
150
+ tokenData = await generateCliToken(storeName);
151
+ }
152
+ catch (err) {
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ console.error(`\n ${RED}${BOLD}✖ Failed to generate CLI token:${RESET} ${message}\n`);
155
+ process.exit(1);
156
+ }
157
+ const { store_id, token_id, token, expires_at } = tokenData;
158
+ const approveUrl = `${FRONTEND_BASE_URL}/#/approve-cli-token?store_id=${store_id}&token_id=${token_id}`;
159
+ console.log(`\n ${YELLOW}${BOLD}⟶ Please approve the token in your browser:${RESET}`);
160
+ console.log(` ${CYAN}${UNDERLINE}${approveUrl}${RESET}\n`);
161
+ openBrowser(approveUrl);
162
+ try {
163
+ await waitForApproval(token_id);
164
+ }
165
+ catch (err) {
166
+ const message = err instanceof Error ? err.message : String(err);
167
+ console.error(`\n ${RED}${BOLD}✖ Token approval check failed:${RESET} ${message}\n`);
168
+ process.exit(1);
169
+ }
170
+ console.log(` ${GREEN}${BOLD}✔ Token approved!${RESET}\n`);
171
+ saveToken({
172
+ store_name: storeName,
173
+ store_id,
174
+ token_id,
175
+ token,
176
+ expires_at,
177
+ });
178
+ return token;
179
+ }
180
+ // ── Express routes ──────────────────────────────────────────
181
+ app.get("/health", (_req, res) => {
182
+ res.json({ ok: true });
183
+ });
184
+ app.get("/style.css", (_req, res) => {
185
+ const cssPath = path.join(THEME_DIR, "style.css");
186
+ try {
187
+ const css = fs.readFileSync(cssPath, "utf-8");
188
+ res.type("text/css").send(css);
189
+ }
190
+ catch {
191
+ res.status(404).type("text/css").send("/* style.css not found */");
192
+ }
193
+ });
194
+ app.get("/script.js", (_req, res) => {
195
+ const jsPath = path.join(THEME_DIR, "script.js");
196
+ try {
197
+ const js = fs.readFileSync(jsPath, "utf-8");
198
+ res.type("application/javascript").send(js);
199
+ }
200
+ catch {
201
+ res
202
+ .status(404)
203
+ .type("application/javascript")
204
+ .send("// script.js not found");
205
+ }
206
+ });
207
+ app.get("/", (_req, res) => {
208
+ res.json({ version: "1.0.0" });
209
+ });
210
+ app.get("/theme", (req, res) => {
211
+ const baseUrl = getBaseUrl(req);
212
+ res.json({
213
+ sections: buildSections(),
214
+ theme_data: readJsonFile(path.join(THEME_DIR, "theme-data.json")),
215
+ config: readJsonFile(path.join(THEME_DIR, "config.json")),
216
+ style: `${baseUrl}/style.css`,
217
+ script: `${baseUrl}/script.js`,
218
+ });
219
+ });
220
+ // ── Main ────────────────────────────────────────────────────
221
+ function signalAuthReady(storeName, code) {
222
+ const data = JSON.stringify({ store_name: storeName, code });
223
+ fs.writeFileSync(AUTH_READY_FILE, data, "utf-8");
224
+ }
225
+ function cleanupAuthReady() {
226
+ try {
227
+ fs.unlinkSync(AUTH_READY_FILE);
228
+ }
229
+ catch { }
230
+ }
231
+ async function main() {
232
+ cleanupAuthReady();
233
+ const storeName = await promptStoreName();
234
+ const code = await authenticate(storeName);
235
+ signalAuthReady(storeName, code);
236
+ app.listen(port, () => {
237
+ console.log("");
238
+ console.log(` ${CYAN}${BOLD}Theme Builder${RESET}`);
239
+ console.log(` ${DIM}Server listening on${RESET} http://localhost:${port}`);
240
+ try {
241
+ const tunnelUrl = fs.readFileSync(URL_FILE, "utf-8").trim();
242
+ if (tunnelUrl) {
243
+ const themeUrl = `${tunnelUrl}/theme`;
244
+ const storeUrl = `https://${storeName}.${STORE_DOMAIN}?load_theme=${encodeURIComponent(themeUrl)}&code=${code}`;
245
+ console.log(` ${GREEN}${BOLD}✔${RESET} ${DIM}Tunnel:${RESET} ${CYAN}${UNDERLINE}${tunnelUrl}${RESET}`);
246
+ console.log(` ${GREEN}${BOLD}✔${RESET} ${DIM}Open your store:${RESET} ${CYAN}${UNDERLINE}${storeUrl}${RESET}`);
247
+ }
248
+ }
249
+ catch { }
250
+ console.log("");
251
+ });
252
+ }
253
+ process.once("SIGINT", () => cleanupAuthReady());
254
+ process.once("SIGTERM", () => cleanupAuthReady());
255
+ main();
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const TOKEN_FILE = path.resolve(process.cwd(), ".cli-tokens.json");
4
+ function loadAll() {
5
+ try {
6
+ const raw = fs.readFileSync(TOKEN_FILE, "utf-8");
7
+ const parsed = JSON.parse(raw);
8
+ if (Array.isArray(parsed))
9
+ return parsed;
10
+ }
11
+ catch { }
12
+ return [];
13
+ }
14
+ function saveAll(tokens) {
15
+ fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2), "utf-8");
16
+ }
17
+ export function getToken(storeName) {
18
+ return loadAll().find((t) => t.store_name === storeName);
19
+ }
20
+ export function saveToken(entry) {
21
+ const tokens = loadAll().filter((t) => t.store_name !== entry.store_name);
22
+ tokens.push(entry);
23
+ saveAll(tokens);
24
+ }
@@ -0,0 +1,124 @@
1
+ import "dotenv/config";
2
+ import { Tunnel } from "cloudflared";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ const CYAN = "\x1b[36m";
6
+ const GREEN = "\x1b[32m";
7
+ const YELLOW = "\x1b[33m";
8
+ const RED = "\x1b[31m";
9
+ const DIM = "\x1b[2m";
10
+ const BOLD = "\x1b[1m";
11
+ const RESET = "\x1b[0m";
12
+ const UNDERLINE = "\x1b[4m";
13
+ const STORE_DOMAIN = process.env.STORE_DOMAIN || "myeasyorders.com";
14
+ const port = Number(process.env.PORT) || 4000;
15
+ const URL_FILE = path.resolve(process.cwd(), ".tunnel-url");
16
+ const PID_FILE = path.resolve(process.cwd(), ".tunnel-pid");
17
+ const AUTH_READY_FILE = path.resolve(process.cwd(), ".auth-ready");
18
+ function isAlive(pid) {
19
+ try {
20
+ process.kill(pid, 0);
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ function alreadyRunning() {
28
+ try {
29
+ const pid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
30
+ if (isAlive(pid)) {
31
+ const url = fs.readFileSync(URL_FILE, "utf-8").trim();
32
+ console.log(` ${DIM}Tunnel already running (pid ${pid})${RESET}`);
33
+ console.log(` ${DIM}Public URL:${RESET} ${CYAN}${UNDERLINE}${url}${RESET}`);
34
+ return true;
35
+ }
36
+ }
37
+ catch { }
38
+ return false;
39
+ }
40
+ function spinner(text) {
41
+ const frames = [" ◐", " ◓", " ◑", " ◒"];
42
+ let i = 0;
43
+ return setInterval(() => {
44
+ process.stdout.write(`\r${CYAN}${frames[i++ % frames.length]}${RESET} ${DIM}${text}${RESET}`);
45
+ }, 120);
46
+ }
47
+ function cleanup() {
48
+ try {
49
+ fs.unlinkSync(URL_FILE);
50
+ }
51
+ catch { }
52
+ try {
53
+ fs.unlinkSync(PID_FILE);
54
+ }
55
+ catch { }
56
+ }
57
+ function waitForAuthReady() {
58
+ return new Promise((resolve) => {
59
+ const tryRead = () => {
60
+ if (fs.existsSync(AUTH_READY_FILE)) {
61
+ const raw = fs.readFileSync(AUTH_READY_FILE, "utf-8").trim();
62
+ try {
63
+ const data = JSON.parse(raw);
64
+ return resolve(data);
65
+ }
66
+ catch {
67
+ return resolve({ store_name: "your-store", code: "" });
68
+ }
69
+ }
70
+ };
71
+ tryRead();
72
+ const check = setInterval(() => {
73
+ if (fs.existsSync(AUTH_READY_FILE)) {
74
+ clearInterval(check);
75
+ tryRead();
76
+ }
77
+ }, 500);
78
+ });
79
+ }
80
+ async function main() {
81
+ if (alreadyRunning())
82
+ return;
83
+ const authData = await waitForAuthReady();
84
+ fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
85
+ const spin = spinner("Starting Cloudflare tunnel...");
86
+ const tunnel = Tunnel.quick(`http://127.0.0.1:${port}`);
87
+ const tunnelUrl = await new Promise((resolve) => {
88
+ tunnel.once("url", (url) => resolve(url));
89
+ });
90
+ clearInterval(spin);
91
+ process.stdout.write("\r\x1b[K");
92
+ fs.writeFileSync(URL_FILE, tunnelUrl, "utf-8");
93
+ const themeUrl = `${tunnelUrl}/theme`;
94
+ const storeUrl = `https://${authData.store_name}.${STORE_DOMAIN}?load_theme=${encodeURIComponent(themeUrl)}&code=${authData.code}`;
95
+ console.log("");
96
+ console.log(`${GREEN}${BOLD} ✔ Tunnel is live${RESET}`);
97
+ console.log("");
98
+ console.log(` ${DIM}Public URL:${RESET} ${CYAN}${UNDERLINE}${tunnelUrl}${RESET}`);
99
+ console.log(` ${GREEN}${BOLD}✔${RESET} ${DIM}Open your store:${RESET} ${CYAN}${UNDERLINE}${storeUrl}${RESET}`);
100
+ console.log(` ${DIM}Local:${RESET} http://127.0.0.1:${port}`);
101
+ console.log(` ${DIM}─────────────────────────────────────────${RESET}`);
102
+ console.log("");
103
+ tunnel.once("connected", (conn) => {
104
+ console.log(` ${GREEN}●${RESET} ${DIM}Connected via${RESET} ${BOLD}${conn.location}${RESET} ${DIM}(${conn.id.slice(0, 8)})${RESET}`);
105
+ });
106
+ tunnel.on("exit", (code, signal) => {
107
+ if (code !== 0) {
108
+ console.log(`\n ${RED}✖ Tunnel exited${RESET} ${DIM}(code ${code}, signal ${signal})${RESET}\n`);
109
+ }
110
+ cleanup();
111
+ });
112
+ tunnel.on("error", (err) => {
113
+ console.log(` ${YELLOW}⚠ ${err.message}${RESET}`);
114
+ });
115
+ process.once("SIGINT", () => {
116
+ cleanup();
117
+ tunnel.stop();
118
+ });
119
+ process.once("SIGTERM", () => {
120
+ cleanup();
121
+ tunnel.stop();
122
+ });
123
+ }
124
+ main();
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "easyorders",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "CLI tool for creating and developing Easy Orders themes",
5
+ "type": "module",
5
6
  "bin": {
6
- "easyorders": "cli/bin/cli.ts"
7
+ "easyorders": "dist/bin/cli.js"
7
8
  },
8
9
  "files": [
9
- "cli/"
10
+ "dist/"
10
11
  ],
11
12
  "keywords": [
12
13
  "easy-orders",
@@ -16,8 +17,9 @@
16
17
  ],
17
18
  "license": "MIT",
18
19
  "scripts": {
19
- "dev": "tsx cli/bin/cli.ts start",
20
- "create-theme": "tsx cli/bin/cli.ts create-theme"
20
+ "build": "tsc && cp -r cli/template dist/template",
21
+ "dev": "npx tsx cli/bin/cli.ts start",
22
+ "create-theme": "npx tsx cli/bin/cli.ts create-theme"
21
23
  },
22
24
  "dependencies": {
23
25
  "axios": "^1.9.0",