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