@within-7/jetr 0.0.3 → 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/dist/cli.js +336 -0
- package/package.json +23 -37
- package/README.md +0 -51
- package/bin/jetr.js +0 -5
- package/src/index.js +0 -565
package/dist/cli.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
|
|
8
|
+
// src/config.ts
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
var CONFIG_DIR = join(homedir(), ".jetr");
|
|
13
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
14
|
+
var DEFAULT_CONFIG = {
|
|
15
|
+
apiUrl: "https://jetr-api.lixilei.workers.dev"
|
|
16
|
+
};
|
|
17
|
+
function loadConfig() {
|
|
18
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
21
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
22
|
+
} catch {
|
|
23
|
+
return { ...DEFAULT_CONFIG };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function saveConfig(config) {
|
|
27
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
28
|
+
const current = loadConfig();
|
|
29
|
+
const merged = { ...current, ...config };
|
|
30
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
31
|
+
}
|
|
32
|
+
function getToken() {
|
|
33
|
+
const config = loadConfig();
|
|
34
|
+
if (!config.token) {
|
|
35
|
+
console.error("Not logged in. Run: jetr login");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
return config.token;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/api.ts
|
|
42
|
+
async function request(path, options = {}) {
|
|
43
|
+
const config = loadConfig();
|
|
44
|
+
const url = `${config.apiUrl}${path}`;
|
|
45
|
+
const headers = {
|
|
46
|
+
Authorization: `Bearer ${getToken()}`,
|
|
47
|
+
...options.headers || {}
|
|
48
|
+
};
|
|
49
|
+
const res = await fetch(url, { ...options, headers });
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
52
|
+
async function json(path, options = {}) {
|
|
53
|
+
const res = await request(path, options);
|
|
54
|
+
const body = await res.json();
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
throw new Error(body.error || `API error: ${res.status}`);
|
|
57
|
+
}
|
|
58
|
+
return body;
|
|
59
|
+
}
|
|
60
|
+
var api = {
|
|
61
|
+
async createSite(name, opts) {
|
|
62
|
+
return json("/sites", {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({ name, ...opts })
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
async listSites() {
|
|
69
|
+
return json("/sites");
|
|
70
|
+
},
|
|
71
|
+
async getSite(name) {
|
|
72
|
+
return json(`/sites/${name}`);
|
|
73
|
+
},
|
|
74
|
+
async updateSite(name, opts) {
|
|
75
|
+
return json(`/sites/${name}`, {
|
|
76
|
+
method: "PATCH",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: JSON.stringify(opts)
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
async deleteSite(name) {
|
|
82
|
+
const res = await request(`/sites/${name}`, { method: "DELETE" });
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
const body = await res.json();
|
|
85
|
+
throw new Error(body.error || `Delete failed: ${res.status}`);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async deployDiff(name, files) {
|
|
89
|
+
return json(`/sites/${name}/deploy`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ files })
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
async uploadFile(name, filePath, body, deployId, hash) {
|
|
96
|
+
const res = await request(`/sites/${name}/files/${filePath}`, {
|
|
97
|
+
method: "PUT",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/octet-stream",
|
|
100
|
+
"X-Deploy-Id": deployId,
|
|
101
|
+
"X-File-Hash": hash
|
|
102
|
+
},
|
|
103
|
+
body
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const data = await res.json();
|
|
107
|
+
throw new Error(`Upload ${filePath}: ${data.error}`);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async finalize(name, deployId) {
|
|
111
|
+
return json(`/sites/${name}/deploy/${deployId}/finalize`, { method: "POST" });
|
|
112
|
+
},
|
|
113
|
+
async createToken(name, opts) {
|
|
114
|
+
return json(`/sites/${name}/tokens`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
body: JSON.stringify(opts || {})
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
async revokeToken(name, tokenId) {
|
|
121
|
+
const res = await request(`/sites/${name}/tokens/${tokenId}`, { method: "DELETE" });
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
const body = await res.json();
|
|
124
|
+
throw new Error(body.error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/deploy.ts
|
|
130
|
+
import { createHash } from "crypto";
|
|
131
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
132
|
+
import { resolve, join as join2, posix } from "path";
|
|
133
|
+
import { glob } from "glob";
|
|
134
|
+
function hashFile(filePath) {
|
|
135
|
+
const content = readFileSync2(filePath);
|
|
136
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
137
|
+
return `sha256:${hash}`;
|
|
138
|
+
}
|
|
139
|
+
async function deploy(siteName, directory, onProgress) {
|
|
140
|
+
const absDir = resolve(directory);
|
|
141
|
+
onProgress?.("Scanning files...");
|
|
142
|
+
const filePaths = await glob("**/*", {
|
|
143
|
+
cwd: absDir,
|
|
144
|
+
nodir: true,
|
|
145
|
+
dot: false
|
|
146
|
+
});
|
|
147
|
+
if (filePaths.length === 0) {
|
|
148
|
+
throw new Error(`No files found in ${directory}`);
|
|
149
|
+
}
|
|
150
|
+
const manifest = {};
|
|
151
|
+
for (const fp of filePaths) {
|
|
152
|
+
const absPath = join2(absDir, fp);
|
|
153
|
+
const hash = hashFile(absPath);
|
|
154
|
+
const size = statSync(absPath).size;
|
|
155
|
+
const normalizedPath = fp.split("/").join(posix.sep);
|
|
156
|
+
manifest[normalizedPath] = { hash, size };
|
|
157
|
+
}
|
|
158
|
+
onProgress?.(`Found ${filePaths.length} files`);
|
|
159
|
+
onProgress?.("Computing diff...");
|
|
160
|
+
const diff = await api.deployDiff(siteName, manifest);
|
|
161
|
+
onProgress?.(
|
|
162
|
+
`Upload: ${diff.upload.length}, Delete: ${diff.delete.length}, Unchanged: ${diff.unchanged.length}`
|
|
163
|
+
);
|
|
164
|
+
if (diff.upload.length > 0) {
|
|
165
|
+
for (let i = 0; i < diff.upload.length; i++) {
|
|
166
|
+
const fp = diff.upload[i];
|
|
167
|
+
const absPath = join2(absDir, fp);
|
|
168
|
+
const content = readFileSync2(absPath);
|
|
169
|
+
onProgress?.(`Uploading (${i + 1}/${diff.upload.length}) ${fp}`);
|
|
170
|
+
await api.uploadFile(siteName, fp, content, diff.deploy_id, manifest[fp].hash);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
onProgress?.("Finalizing...");
|
|
174
|
+
const result = await api.finalize(siteName, diff.deploy_id);
|
|
175
|
+
return {
|
|
176
|
+
url: `https://${siteName}.jetr.within-7.com`,
|
|
177
|
+
filesUploaded: result.files_uploaded,
|
|
178
|
+
filesDeleted: result.files_deleted,
|
|
179
|
+
filesUnchanged: diff.unchanged.length,
|
|
180
|
+
totalSize: result.total_size
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/cli.ts
|
|
185
|
+
var program = new Command();
|
|
186
|
+
program.name("jetr").description("CLI for Jetr static site hosting").version("0.1.0");
|
|
187
|
+
program.command("login").description("Save JWT token for authentication").argument("<token>", "JWT token from auth-gateway login").option("--api-url <url>", "API base URL").action((token, opts) => {
|
|
188
|
+
saveConfig({ token, ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {} });
|
|
189
|
+
console.log(chalk.green("\u2713 Token saved to ~/.jetr/config.json"));
|
|
190
|
+
});
|
|
191
|
+
program.command("whoami").description("Show current config").action(() => {
|
|
192
|
+
const config = loadConfig();
|
|
193
|
+
console.log(`API: ${config.apiUrl}`);
|
|
194
|
+
console.log(`Token: ${config.token ? config.token.slice(0, 20) + "..." : chalk.red("not set")}`);
|
|
195
|
+
});
|
|
196
|
+
program.command("create").description("Create a new site").argument("<name>", "Site name (becomes {name}.jetr.within-7.com)").option("-p, --password <password>", "Set access password").option("-e, --expires <seconds>", "Expire after N seconds", parseInt).action(async (name, opts) => {
|
|
197
|
+
const spinner = ora("Creating site...").start();
|
|
198
|
+
try {
|
|
199
|
+
const site = await api.createSite(name, {
|
|
200
|
+
password: opts.password,
|
|
201
|
+
expires_in: opts.expires
|
|
202
|
+
});
|
|
203
|
+
spinner.succeed(`Created ${chalk.bold(site.name)}`);
|
|
204
|
+
console.log(` URL: ${chalk.cyan(site.url)}`);
|
|
205
|
+
if (site.password_protected) console.log(` Password: ${chalk.yellow("enabled")}`);
|
|
206
|
+
if (site.expires_at) console.log(` Expires: ${new Date(site.expires_at * 1e3).toISOString()}`);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
spinner.fail(e.message);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
program.command("deploy").description("Deploy a directory to a site").argument("<name>", "Site name").argument("[directory]", "Directory to deploy", ".").action(async (name, directory) => {
|
|
213
|
+
const spinner = ora("").start();
|
|
214
|
+
try {
|
|
215
|
+
const result = await deploy(name, directory, (msg) => {
|
|
216
|
+
spinner.text = msg;
|
|
217
|
+
});
|
|
218
|
+
spinner.succeed("Deployed!");
|
|
219
|
+
console.log(` URL: ${chalk.cyan(result.url)}`);
|
|
220
|
+
console.log(` Uploaded: ${result.filesUploaded} files`);
|
|
221
|
+
if (result.filesDeleted > 0) console.log(` Deleted: ${result.filesDeleted} files`);
|
|
222
|
+
if (result.filesUnchanged > 0) console.log(` Unchanged: ${result.filesUnchanged} files`);
|
|
223
|
+
console.log(` Size: ${formatBytes(result.totalSize)}`);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
spinner.fail(e.message);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
program.command("list").alias("ls").description("List your sites").action(async () => {
|
|
230
|
+
try {
|
|
231
|
+
const { sites } = await api.listSites();
|
|
232
|
+
if (sites.length === 0) {
|
|
233
|
+
console.log(chalk.dim("No sites yet. Create one: jetr create <name>"));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
for (const s of sites) {
|
|
237
|
+
const lock = s.password_protected ? chalk.yellow(" \u{1F512}") : "";
|
|
238
|
+
const expire = s.expires_at ? chalk.dim(` expires ${new Date(s.expires_at * 1e3).toLocaleDateString()}`) : "";
|
|
239
|
+
console.log(
|
|
240
|
+
` ${chalk.bold(s.name)}${lock}${expire} ${chalk.dim(formatBytes(s.total_size))} ${chalk.cyan(s.url)}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
console.log(chalk.dim(`
|
|
244
|
+
${sites.length} site(s)`));
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.error(chalk.red(e.message));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
program.command("info").description("Show site details").argument("<name>", "Site name").action(async (name) => {
|
|
251
|
+
try {
|
|
252
|
+
const site = await api.getSite(name);
|
|
253
|
+
console.log(`${chalk.bold(site.name)}`);
|
|
254
|
+
console.log(` URL: ${chalk.cyan(site.url)}`);
|
|
255
|
+
console.log(` Password: ${site.password_protected ? chalk.yellow("yes") : "no"}`);
|
|
256
|
+
console.log(` Expires: ${site.expires_at ? new Date(site.expires_at * 1e3).toISOString() : "never"}`);
|
|
257
|
+
console.log(` Files: ${site.files.length}`);
|
|
258
|
+
console.log(` Created: ${new Date(site.created_at * 1e3).toISOString()}`);
|
|
259
|
+
console.log(` Updated: ${new Date(site.updated_at * 1e3).toISOString()}`);
|
|
260
|
+
if (site.files.length > 0) {
|
|
261
|
+
console.log(`
|
|
262
|
+
${chalk.dim("Files:")}`);
|
|
263
|
+
for (const f of site.files) {
|
|
264
|
+
console.log(` ${f.path} ${chalk.dim(formatBytes(f.size))}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (site.tokens.length > 0) {
|
|
268
|
+
console.log(`
|
|
269
|
+
${chalk.dim("Tokens:")}`);
|
|
270
|
+
for (const t of site.tokens) {
|
|
271
|
+
const note = t.note ? ` (${t.note})` : "";
|
|
272
|
+
const exp = t.expires_at ? ` expires ${new Date(t.expires_at * 1e3).toLocaleDateString()}` : "";
|
|
273
|
+
console.log(` ${t.id}${note}${chalk.dim(exp)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
console.error(chalk.red(e.message));
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
program.command("delete").alias("rm").description("Delete a site and all its files").argument("<name>", "Site name").action(async (name) => {
|
|
282
|
+
const spinner = ora(`Deleting ${name}...`).start();
|
|
283
|
+
try {
|
|
284
|
+
await api.deleteSite(name);
|
|
285
|
+
spinner.succeed(`Deleted ${chalk.bold(name)}`);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
spinner.fail(e.message);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
program.command("password").description("Set or remove site password").argument("<name>", "Site name").argument("[password]", "New password (omit to remove)").action(async (name, password) => {
|
|
292
|
+
try {
|
|
293
|
+
const site = await api.updateSite(name, { password: password ?? null });
|
|
294
|
+
if (site.password_protected) {
|
|
295
|
+
console.log(chalk.green(`\u2713 Password set for ${chalk.bold(name)}`));
|
|
296
|
+
} else {
|
|
297
|
+
console.log(chalk.green(`\u2713 Password removed from ${chalk.bold(name)}`));
|
|
298
|
+
}
|
|
299
|
+
} catch (e) {
|
|
300
|
+
console.error(chalk.red(e.message));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
var tokenCmd = program.command("token").description("Manage share tokens");
|
|
305
|
+
tokenCmd.command("create").description("Create a share token").argument("<site>", "Site name").option("-n, --note <note>", "Token note").option("-e, --expires <seconds>", "Expire after N seconds", parseInt).action(async (site, opts) => {
|
|
306
|
+
try {
|
|
307
|
+
const token = await api.createToken(site, {
|
|
308
|
+
note: opts.note,
|
|
309
|
+
expires_in: opts.expires
|
|
310
|
+
});
|
|
311
|
+
console.log(chalk.green("\u2713 Token created"));
|
|
312
|
+
console.log(` ID: ${token.id}`);
|
|
313
|
+
console.log(` URL: ${chalk.cyan(token.url)}`);
|
|
314
|
+
if (token.note) console.log(` Note: ${token.note}`);
|
|
315
|
+
if (token.expires_at) console.log(` Expires: ${new Date(token.expires_at * 1e3).toISOString()}`);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
console.error(chalk.red(e.message));
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
tokenCmd.command("revoke").description("Revoke a share token").argument("<site>", "Site name").argument("<id>", "Token ID").action(async (site, id) => {
|
|
322
|
+
try {
|
|
323
|
+
await api.revokeToken(site, id);
|
|
324
|
+
console.log(chalk.green(`\u2713 Token ${id} revoked`));
|
|
325
|
+
} catch (e) {
|
|
326
|
+
console.error(chalk.red(e.message));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
function formatBytes(bytes) {
|
|
331
|
+
if (bytes === 0) return "0 B";
|
|
332
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
333
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
334
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
335
|
+
}
|
|
336
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,48 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@within-7/jetr",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "CLI
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Jetr static site hosting",
|
|
5
|
+
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"jetr": "./
|
|
7
|
+
"jetr": "./dist/cli.js"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
10
|
-
"bin/",
|
|
11
|
-
"src/"
|
|
12
|
-
],
|
|
9
|
+
"files": ["dist"],
|
|
13
10
|
"scripts": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"publish:release": "node scripts/publish.cjs"
|
|
11
|
+
"build": "tsup src/cli.ts --format esm --target node20 --clean",
|
|
12
|
+
"dev": "tsx src/cli.ts",
|
|
13
|
+
"prepublishOnly": "pnpm build"
|
|
18
14
|
},
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/Within-7/jetr.git"
|
|
22
|
-
},
|
|
23
|
-
"bugs": {
|
|
24
|
-
"url": "https://github.com/Within-7/jetr/issues"
|
|
25
|
-
},
|
|
26
|
-
"homepage": "https://github.com/Within-7/jetr#readme",
|
|
27
|
-
"keywords": [
|
|
28
|
-
"static",
|
|
29
|
-
"deploy",
|
|
30
|
-
"hosting",
|
|
31
|
-
"cli",
|
|
32
|
-
"surge"
|
|
33
|
-
],
|
|
34
|
-
"author": "",
|
|
35
|
-
"license": "MIT",
|
|
36
15
|
"dependencies": {
|
|
37
|
-
"
|
|
38
|
-
"chalk": "^5
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
16
|
+
"commander": "^13",
|
|
17
|
+
"chalk": "^5",
|
|
18
|
+
"ora": "^8",
|
|
19
|
+
"glob": "^11"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"tsup": "^8",
|
|
23
|
+
"tsx": "^4",
|
|
24
|
+
"typescript": "^5",
|
|
25
|
+
"@types/node": "^22"
|
|
43
26
|
},
|
|
44
27
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
46
32
|
},
|
|
47
|
-
"
|
|
33
|
+
"license": "MIT"
|
|
48
34
|
}
|
package/README.md
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# jetr
|
|
2
|
-
|
|
3
|
-
CLI tool for deploying static websites instantly.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g @within-7/jetr
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
# Deploy current directory with random project name
|
|
15
|
-
jetr ./
|
|
16
|
-
|
|
17
|
-
# Deploy current directory with specific project name
|
|
18
|
-
jetr ./ my-site
|
|
19
|
-
|
|
20
|
-
# Deploy a specific directory
|
|
21
|
-
jetr ./dist
|
|
22
|
-
jetr ./dist my-app
|
|
23
|
-
|
|
24
|
-
# Deploy a single file
|
|
25
|
-
jetr ./index.html
|
|
26
|
-
jetr ./demo.html my-demo
|
|
27
|
-
|
|
28
|
-
# Generate .jetrignore file
|
|
29
|
-
jetr init
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
After deployment, your site will be available at `http://<projectName>.statics.within-7.com`.
|
|
33
|
-
|
|
34
|
-
## .jetrignore
|
|
35
|
-
|
|
36
|
-
Create a `.jetrignore` file in your project root to exclude files from deployment. It uses the same syntax as `.gitignore`.
|
|
37
|
-
|
|
38
|
-
If no `.jetrignore` exists, one will be auto-created with common defaults (node_modules, .git, .env, etc.).
|
|
39
|
-
|
|
40
|
-
Run `jetr init` to manually generate a `.jetrignore` with default patterns.
|
|
41
|
-
|
|
42
|
-
## Options
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
-h, --help Show help message
|
|
46
|
-
-v, --version Show version number
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## License
|
|
50
|
-
|
|
51
|
-
MIT
|
package/bin/jetr.js
DELETED
package/src/index.js
DELETED
|
@@ -1,565 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import readline from 'readline';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import archiver from 'archiver';
|
|
6
|
-
import FormData from 'form-data';
|
|
7
|
-
import ignore from 'ignore';
|
|
8
|
-
import { nanoid } from 'nanoid';
|
|
9
|
-
import chalk from 'chalk';
|
|
10
|
-
import ora from 'ora';
|
|
11
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = path.dirname(__filename);
|
|
14
|
-
|
|
15
|
-
// const API_URL = 'https://api-statics.within-7.com/api/deploy';
|
|
16
|
-
const API_URL = 'https://api-statics.within-7.com/api/deploy/s3';
|
|
17
|
-
const CONFIG_FILE = '.jetrrc';
|
|
18
|
-
|
|
19
|
-
const DEFAULT_JETRIGNORE = `# Dependencies
|
|
20
|
-
node_modules/
|
|
21
|
-
.pnp
|
|
22
|
-
.pnp.js
|
|
23
|
-
|
|
24
|
-
# Build outputs
|
|
25
|
-
dist/
|
|
26
|
-
build/
|
|
27
|
-
out/
|
|
28
|
-
.next/
|
|
29
|
-
.nuxt/
|
|
30
|
-
.output/
|
|
31
|
-
.cache/
|
|
32
|
-
|
|
33
|
-
# Logs
|
|
34
|
-
*.log
|
|
35
|
-
npm-debug.log*
|
|
36
|
-
yarn-debug.log*
|
|
37
|
-
yarn-error.log*
|
|
38
|
-
pnpm-debug.log*
|
|
39
|
-
|
|
40
|
-
# Environment
|
|
41
|
-
.env
|
|
42
|
-
.env.*
|
|
43
|
-
.env.local
|
|
44
|
-
.env.*.local
|
|
45
|
-
|
|
46
|
-
# IDE & Editor
|
|
47
|
-
.vscode/
|
|
48
|
-
.idea/
|
|
49
|
-
*.swp
|
|
50
|
-
*.swo
|
|
51
|
-
*~
|
|
52
|
-
.project
|
|
53
|
-
.classpath
|
|
54
|
-
.settings/
|
|
55
|
-
|
|
56
|
-
# OS files
|
|
57
|
-
.DS_Store
|
|
58
|
-
Thumbs.db
|
|
59
|
-
desktop.ini
|
|
60
|
-
|
|
61
|
-
# Git
|
|
62
|
-
.git/
|
|
63
|
-
.gitignore
|
|
64
|
-
|
|
65
|
-
# Testing
|
|
66
|
-
coverage/
|
|
67
|
-
.nyc_output/
|
|
68
|
-
|
|
69
|
-
# Package managers
|
|
70
|
-
package-lock.json
|
|
71
|
-
yarn.lock
|
|
72
|
-
pnpm-lock.yaml
|
|
73
|
-
|
|
74
|
-
# Misc
|
|
75
|
-
*.zip
|
|
76
|
-
*.tar.gz
|
|
77
|
-
`;
|
|
78
|
-
|
|
79
|
-
function initJetrignore(targetDir, silent = false) {
|
|
80
|
-
const ignoreFile = path.join(targetDir, '.jetrignore');
|
|
81
|
-
|
|
82
|
-
if (fs.existsSync(ignoreFile)) {
|
|
83
|
-
if (!silent) {
|
|
84
|
-
console.log(chalk.yellow('.jetrignore already exists in this directory'));
|
|
85
|
-
}
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
fs.writeFileSync(ignoreFile, DEFAULT_JETRIGNORE);
|
|
90
|
-
if (!silent) {
|
|
91
|
-
console.log(chalk.green('Created .jetrignore with default ignore patterns'));
|
|
92
|
-
}
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function showHelp() {
|
|
97
|
-
console.log(`
|
|
98
|
-
${chalk.bold('jetr')} - Deploy static websites instantly
|
|
99
|
-
|
|
100
|
-
${chalk.bold('Usage:')}
|
|
101
|
-
jetr <directory> [projectName]
|
|
102
|
-
|
|
103
|
-
${chalk.bold('Examples:')}
|
|
104
|
-
jetr ./ # Deploy current directory with random name
|
|
105
|
-
jetr ./ my-site # Deploy current directory as my-site
|
|
106
|
-
jetr ../app # Deploy ../app with random name
|
|
107
|
-
jetr ./dist production # Deploy ./dist as production
|
|
108
|
-
|
|
109
|
-
${chalk.bold('Commands:')}
|
|
110
|
-
init Create a .jetrignore file with default patterns
|
|
111
|
-
|
|
112
|
-
${chalk.bold('Options:')}
|
|
113
|
-
-h, --help Show this help message
|
|
114
|
-
-v, --version Show version number
|
|
115
|
-
|
|
116
|
-
${chalk.bold('.jetrignore:')}
|
|
117
|
-
Create a .jetrignore file in your project root to exclude files.
|
|
118
|
-
Uses the same syntax as .gitignore.
|
|
119
|
-
Run "jetr init" to generate one with common defaults.
|
|
120
|
-
|
|
121
|
-
${chalk.bold('.jetrkeep:')}
|
|
122
|
-
Create a .jetrkeep file to force-include files that match .jetrignore patterns.
|
|
123
|
-
Uses the same syntax as .gitignore. Patterns in .jetrkeep override .jetrignore.
|
|
124
|
-
`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function showVersion() {
|
|
128
|
-
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
129
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
130
|
-
console.log(pkg.version);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function generateProjectName() {
|
|
134
|
-
return nanoid(12).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function sanitizeProjectName(name) {
|
|
138
|
-
const sanitized = name
|
|
139
|
-
.toLowerCase()
|
|
140
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
141
|
-
.replace(/^-+|-+$/g, '')
|
|
142
|
-
.replace(/-+/g, '-');
|
|
143
|
-
|
|
144
|
-
// If sanitization results in empty string, generate a random name
|
|
145
|
-
if (!sanitized) {
|
|
146
|
-
return generateProjectName();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return sanitized;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ============ Config file functions ============
|
|
153
|
-
|
|
154
|
-
function loadConfig(targetDir) {
|
|
155
|
-
const configPath = path.join(targetDir, CONFIG_FILE);
|
|
156
|
-
if (fs.existsSync(configPath)) {
|
|
157
|
-
try {
|
|
158
|
-
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
159
|
-
} catch {
|
|
160
|
-
return { directory: null, files: {} };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return { directory: null, files: {} };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function saveConfig(targetDir, config) {
|
|
167
|
-
const configPath = path.join(targetDir, CONFIG_FILE);
|
|
168
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function getConfigKey(singleFile) {
|
|
172
|
-
// For directory mode, use 'directory'; for single file, use filename
|
|
173
|
-
return singleFile ? path.basename(singleFile) : 'directory';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function getProjectFromConfig(config, key) {
|
|
177
|
-
if (key === 'directory') {
|
|
178
|
-
return config.directory;
|
|
179
|
-
}
|
|
180
|
-
return config.files?.[key] || null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function updateConfig(config, key, projectName) {
|
|
184
|
-
if (key === 'directory') {
|
|
185
|
-
if (!config.directory) {
|
|
186
|
-
config.directory = { default: projectName, history: [projectName] };
|
|
187
|
-
} else {
|
|
188
|
-
config.directory.default = projectName;
|
|
189
|
-
if (!config.directory.history.includes(projectName)) {
|
|
190
|
-
config.directory.history.push(projectName);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
if (!config.files) config.files = {};
|
|
195
|
-
if (!config.files[key]) {
|
|
196
|
-
config.files[key] = { default: projectName, history: [projectName] };
|
|
197
|
-
} else {
|
|
198
|
-
config.files[key].default = projectName;
|
|
199
|
-
if (!config.files[key].history.includes(projectName)) {
|
|
200
|
-
config.files[key].history.push(projectName);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return config;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function promptSelection(options, message) {
|
|
208
|
-
const rl = readline.createInterface({
|
|
209
|
-
input: process.stdin,
|
|
210
|
-
output: process.stdout,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
console.log();
|
|
214
|
-
console.log(chalk.bold(message));
|
|
215
|
-
options.forEach((opt, i) => {
|
|
216
|
-
const marker = i === 0 ? chalk.green('(default)') : '';
|
|
217
|
-
console.log(` ${chalk.cyan(i + 1)}) ${opt} ${marker}`);
|
|
218
|
-
});
|
|
219
|
-
console.log(` ${chalk.cyan(options.length + 1)}) ${chalk.gray('Create new project')}`);
|
|
220
|
-
console.log();
|
|
221
|
-
|
|
222
|
-
return new Promise((resolve) => {
|
|
223
|
-
rl.question(chalk.bold('Select option [1]: '), (answer) => {
|
|
224
|
-
rl.close();
|
|
225
|
-
const num = parseInt(answer, 10);
|
|
226
|
-
if (!answer || isNaN(num) || num < 1) {
|
|
227
|
-
resolve({ type: 'existing', value: options[0] });
|
|
228
|
-
} else if (num <= options.length) {
|
|
229
|
-
resolve({ type: 'existing', value: options[num - 1] });
|
|
230
|
-
} else {
|
|
231
|
-
resolve({ type: 'new', value: null });
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function promptNewName() {
|
|
238
|
-
const rl = readline.createInterface({
|
|
239
|
-
input: process.stdin,
|
|
240
|
-
output: process.stdout,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
return new Promise((resolve) => {
|
|
244
|
-
rl.question(chalk.bold('Enter project name (leave empty for random): '), (answer) => {
|
|
245
|
-
rl.close();
|
|
246
|
-
resolve(answer.trim());
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function loadKeepRules(targetDir) {
|
|
252
|
-
const keepFile = path.join(targetDir, '.jetrkeep');
|
|
253
|
-
if (!fs.existsSync(keepFile)) {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const ig = ignore();
|
|
258
|
-
const content = fs.readFileSync(keepFile, 'utf-8');
|
|
259
|
-
ig.add(content);
|
|
260
|
-
return ig;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function loadIgnoreRules(targetDir) {
|
|
264
|
-
const ig = ignore();
|
|
265
|
-
const ignoreFile = path.join(targetDir, '.jetrignore');
|
|
266
|
-
|
|
267
|
-
if (fs.existsSync(ignoreFile)) {
|
|
268
|
-
const content = fs.readFileSync(ignoreFile, 'utf-8');
|
|
269
|
-
ig.add(content);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return ig;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function createZip(targetDir, ig, keep, singleFile = null) {
|
|
276
|
-
const tmpFile = path.join(targetDir, `.jetr-upload-${Date.now()}.zip`);
|
|
277
|
-
|
|
278
|
-
return new Promise((resolve, reject) => {
|
|
279
|
-
const output = fs.createWriteStream(tmpFile);
|
|
280
|
-
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
281
|
-
let fileCount = 0;
|
|
282
|
-
|
|
283
|
-
const cleanup = (err) => {
|
|
284
|
-
if (fs.existsSync(tmpFile)) {
|
|
285
|
-
try { fs.unlinkSync(tmpFile); } catch {}
|
|
286
|
-
}
|
|
287
|
-
reject(err);
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
output.on('close', () => {
|
|
291
|
-
if (fileCount === 0) {
|
|
292
|
-
cleanup(new Error('No files to upload (directory is empty or all files are ignored)'));
|
|
293
|
-
} else {
|
|
294
|
-
resolve({ zipPath: tmpFile, fileCount });
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
output.on('error', cleanup);
|
|
299
|
-
archive.on('error', cleanup);
|
|
300
|
-
archive.pipe(output);
|
|
301
|
-
|
|
302
|
-
if (singleFile) {
|
|
303
|
-
// Single file mode: just add the one file
|
|
304
|
-
const fileName = path.basename(singleFile);
|
|
305
|
-
archive.file(singleFile, { name: fileName });
|
|
306
|
-
fileCount = 1;
|
|
307
|
-
} else {
|
|
308
|
-
// Directory mode: add all files respecting .jetrignore
|
|
309
|
-
const addFiles = (dir, base = '') => {
|
|
310
|
-
let entries;
|
|
311
|
-
try {
|
|
312
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
313
|
-
} catch (err) {
|
|
314
|
-
return; // Skip directories we can't read
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
for (const entry of entries) {
|
|
318
|
-
const relativePath = path.join(base, entry.name);
|
|
319
|
-
const fullPath = path.join(dir, entry.name);
|
|
320
|
-
|
|
321
|
-
// Skip temp zip files, .jetrignore and .jetrrc
|
|
322
|
-
if (entry.name.startsWith('.jetr-upload-') && entry.name.endsWith('.zip')) {
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (entry.name === '.jetrignore' || entry.name === '.jetrkeep' || entry.name === '.jetrrc') {
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Skip ignored files/directories (unless kept by .jetrkeep)
|
|
330
|
-
if (ig.ignores(relativePath) || ig.ignores(relativePath + '/')) {
|
|
331
|
-
if (!keep || !keep.ignores(relativePath)) {
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Skip symbolic links to avoid potential loops
|
|
337
|
-
if (entry.isSymbolicLink()) {
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (entry.isDirectory()) {
|
|
342
|
-
addFiles(fullPath, relativePath);
|
|
343
|
-
} else if (entry.isFile()) {
|
|
344
|
-
archive.file(fullPath, { name: relativePath });
|
|
345
|
-
fileCount++;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
addFiles(targetDir);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
archive.finalize();
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function uploadOnce(zipPath, projectName) {
|
|
358
|
-
const form = new FormData();
|
|
359
|
-
form.append('projectName', projectName);
|
|
360
|
-
form.append('file', fs.createReadStream(zipPath), {
|
|
361
|
-
filename: path.basename(zipPath),
|
|
362
|
-
contentType: 'application/zip',
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
return new Promise((resolve, reject) => {
|
|
366
|
-
const request = form.submit(API_URL, (err, res) => {
|
|
367
|
-
if (err) {
|
|
368
|
-
reject(err);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
let data = '';
|
|
373
|
-
res.on('data', (chunk) => {
|
|
374
|
-
data += chunk;
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
res.on('end', () => {
|
|
378
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
379
|
-
try {
|
|
380
|
-
resolve(JSON.parse(data));
|
|
381
|
-
} catch (e) {
|
|
382
|
-
reject(new Error(`Invalid JSON response: ${data}`));
|
|
383
|
-
}
|
|
384
|
-
} else {
|
|
385
|
-
reject(new Error(`Upload failed: ${res.statusCode} ${res.statusMessage}\n${data}`));
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
res.on('error', reject);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
request.on('error', reject);
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async function upload(zipPath, projectName, maxRetries = 3) {
|
|
397
|
-
let lastError;
|
|
398
|
-
|
|
399
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
400
|
-
try {
|
|
401
|
-
return await uploadOnce(zipPath, projectName);
|
|
402
|
-
} catch (err) {
|
|
403
|
-
lastError = err;
|
|
404
|
-
const isRetryable = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EPIPE', 'EAI_AGAIN'].includes(err.code);
|
|
405
|
-
|
|
406
|
-
if (isRetryable && attempt < maxRetries) {
|
|
407
|
-
const delay = attempt * 2000;
|
|
408
|
-
console.log(chalk.yellow(`\nConnection error (${err.code}), retrying in ${delay / 1000}s... (${attempt}/${maxRetries})`));
|
|
409
|
-
await new Promise(r => setTimeout(r, delay));
|
|
410
|
-
} else {
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
throw lastError;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
export async function run() {
|
|
420
|
-
const args = process.argv.slice(2);
|
|
421
|
-
|
|
422
|
-
if (args.includes('-h') || args.includes('--help')) {
|
|
423
|
-
showHelp();
|
|
424
|
-
process.exit(0);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (args.includes('-v') || args.includes('--version')) {
|
|
428
|
-
showVersion();
|
|
429
|
-
process.exit(0);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (args[0] === 'init') {
|
|
433
|
-
const targetDir = path.resolve(args[1] || './');
|
|
434
|
-
initJetrignore(targetDir);
|
|
435
|
-
process.exit(0);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const inputPath = path.resolve(args[0] || './');
|
|
439
|
-
let projectName = args[1] || '';
|
|
440
|
-
let targetDir;
|
|
441
|
-
let singleFile = null;
|
|
442
|
-
|
|
443
|
-
if (!fs.existsSync(inputPath)) {
|
|
444
|
-
console.error(chalk.red(`Error: Path does not exist: ${inputPath}`));
|
|
445
|
-
process.exit(1);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const stats = fs.statSync(inputPath);
|
|
449
|
-
|
|
450
|
-
if (stats.isFile()) {
|
|
451
|
-
// Single file mode: treat the file as a standalone project
|
|
452
|
-
singleFile = inputPath;
|
|
453
|
-
targetDir = path.dirname(inputPath);
|
|
454
|
-
console.log(chalk.gray(`Single file mode: ${path.basename(inputPath)}`));
|
|
455
|
-
} else if (stats.isDirectory()) {
|
|
456
|
-
targetDir = inputPath;
|
|
457
|
-
} else {
|
|
458
|
-
console.error(chalk.red(`Error: Invalid path: ${inputPath}`));
|
|
459
|
-
process.exit(1);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Auto-create .jetrignore if it doesn't exist (only for directory mode)
|
|
463
|
-
if (!singleFile) {
|
|
464
|
-
const created = initJetrignore(targetDir, true);
|
|
465
|
-
if (created) {
|
|
466
|
-
console.log(chalk.gray('Auto-created .jetrignore with default patterns'));
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Load config
|
|
471
|
-
const config = loadConfig(targetDir);
|
|
472
|
-
const configKey = getConfigKey(singleFile);
|
|
473
|
-
const savedProject = getProjectFromConfig(config, configKey);
|
|
474
|
-
|
|
475
|
-
if (projectName) {
|
|
476
|
-
// User specified a project name
|
|
477
|
-
projectName = sanitizeProjectName(projectName);
|
|
478
|
-
} else if (savedProject) {
|
|
479
|
-
// Have saved config, let user choose
|
|
480
|
-
const history = savedProject.history || [savedProject.default];
|
|
481
|
-
// Put default first, then rest of history
|
|
482
|
-
const options = [savedProject.default, ...history.filter(h => h !== savedProject.default)];
|
|
483
|
-
|
|
484
|
-
if (options.length === 1) {
|
|
485
|
-
// Only one option, use it directly
|
|
486
|
-
projectName = options[0];
|
|
487
|
-
console.log(chalk.gray(`Using saved project: ${projectName}`));
|
|
488
|
-
} else {
|
|
489
|
-
// Multiple options, prompt user
|
|
490
|
-
const selection = await promptSelection(options, 'Previous deployments found:');
|
|
491
|
-
if (selection.type === 'existing') {
|
|
492
|
-
projectName = selection.value;
|
|
493
|
-
} else {
|
|
494
|
-
const newName = await promptNewName();
|
|
495
|
-
projectName = newName ? sanitizeProjectName(newName) : generateProjectName();
|
|
496
|
-
console.log(chalk.gray(`Using project name: ${projectName}`));
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
// No config, generate random name
|
|
501
|
-
projectName = generateProjectName();
|
|
502
|
-
console.log(chalk.gray(`Generated project name: ${projectName}`));
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const ig = loadIgnoreRules(targetDir);
|
|
506
|
-
const keep = loadKeepRules(targetDir);
|
|
507
|
-
|
|
508
|
-
const spinner = ora('Packing files...').start();
|
|
509
|
-
let zipPath;
|
|
510
|
-
let fileCount;
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
const result = await createZip(targetDir, ig, keep, singleFile);
|
|
514
|
-
zipPath = result.zipPath;
|
|
515
|
-
fileCount = result.fileCount;
|
|
516
|
-
const stats = fs.statSync(zipPath);
|
|
517
|
-
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
518
|
-
spinner.succeed(`Packed ${fileCount} file${fileCount > 1 ? 's' : ''} (${sizeMB} MB)`);
|
|
519
|
-
} catch (err) {
|
|
520
|
-
spinner.fail('Failed to pack files');
|
|
521
|
-
console.error(chalk.red(err.message));
|
|
522
|
-
process.exit(1);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const uploadSpinner = ora(`Uploading to ${projectName}...`).start();
|
|
526
|
-
|
|
527
|
-
try {
|
|
528
|
-
const result = await upload(zipPath, projectName);
|
|
529
|
-
|
|
530
|
-
fs.unlinkSync(zipPath);
|
|
531
|
-
|
|
532
|
-
if (result.success) {
|
|
533
|
-
uploadSpinner.succeed('Deployed successfully!');
|
|
534
|
-
|
|
535
|
-
// Save config
|
|
536
|
-
const updatedConfig = updateConfig(config, configKey, projectName);
|
|
537
|
-
saveConfig(targetDir, updatedConfig);
|
|
538
|
-
|
|
539
|
-
console.log();
|
|
540
|
-
console.log(chalk.bold('Project: ') + chalk.cyan(result.projectName));
|
|
541
|
-
console.log(chalk.bold('URL: ') + chalk.green(result.defaultUrl || result.url));
|
|
542
|
-
console.log();
|
|
543
|
-
} else {
|
|
544
|
-
uploadSpinner.fail('Deployment failed');
|
|
545
|
-
console.error(chalk.red(JSON.stringify(result, null, 2)));
|
|
546
|
-
process.exit(1);
|
|
547
|
-
}
|
|
548
|
-
} catch (err) {
|
|
549
|
-
uploadSpinner.fail('Upload failed');
|
|
550
|
-
|
|
551
|
-
if (zipPath && fs.existsSync(zipPath)) {
|
|
552
|
-
fs.unlinkSync(zipPath);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
console.error(chalk.red(err.message));
|
|
556
|
-
if (err.code) {
|
|
557
|
-
console.error(chalk.gray(`Error code: ${err.code}`));
|
|
558
|
-
}
|
|
559
|
-
if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {
|
|
560
|
-
console.error(chalk.yellow('\nTip: This might be a network issue or the file is too large.'));
|
|
561
|
-
console.error(chalk.yellow('Try again or check your internet connection.'));
|
|
562
|
-
}
|
|
563
|
-
process.exit(1);
|
|
564
|
-
}
|
|
565
|
-
}
|