@within-7/jetr 0.0.1
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 +51 -0
- package/bin/jetr.js +5 -0
- package/package.json +46 -0
- package/src/index.js +545 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@within-7/jetr",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI tool for deploying static websites instantly",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jetr": "./bin/jetr.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node bin/jetr.js --help",
|
|
15
|
+
"publish:release": "node scripts/publish.cjs"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/Within-7/jetr.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Within-7/jetr/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/Within-7/jetr#readme",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"static",
|
|
27
|
+
"deploy",
|
|
28
|
+
"hosting",
|
|
29
|
+
"cli",
|
|
30
|
+
"surge"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"archiver": "^7.0.1",
|
|
36
|
+
"chalk": "^5.3.0",
|
|
37
|
+
"form-data": "^4.0.1",
|
|
38
|
+
"ignore": "^6.0.2",
|
|
39
|
+
"nanoid": "^5.0.9",
|
|
40
|
+
"ora": "^8.1.1"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"type": "module"
|
|
46
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
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 CONFIG_FILE = '.jetrrc';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_JETRIGNORE = `# Dependencies
|
|
19
|
+
node_modules/
|
|
20
|
+
.pnp
|
|
21
|
+
.pnp.js
|
|
22
|
+
|
|
23
|
+
# Build outputs
|
|
24
|
+
dist/
|
|
25
|
+
build/
|
|
26
|
+
out/
|
|
27
|
+
.next/
|
|
28
|
+
.nuxt/
|
|
29
|
+
.output/
|
|
30
|
+
.cache/
|
|
31
|
+
|
|
32
|
+
# Logs
|
|
33
|
+
*.log
|
|
34
|
+
npm-debug.log*
|
|
35
|
+
yarn-debug.log*
|
|
36
|
+
yarn-error.log*
|
|
37
|
+
pnpm-debug.log*
|
|
38
|
+
|
|
39
|
+
# Environment
|
|
40
|
+
.env
|
|
41
|
+
.env.*
|
|
42
|
+
.env.local
|
|
43
|
+
.env.*.local
|
|
44
|
+
|
|
45
|
+
# IDE & Editor
|
|
46
|
+
.vscode/
|
|
47
|
+
.idea/
|
|
48
|
+
*.swp
|
|
49
|
+
*.swo
|
|
50
|
+
*~
|
|
51
|
+
.project
|
|
52
|
+
.classpath
|
|
53
|
+
.settings/
|
|
54
|
+
|
|
55
|
+
# OS files
|
|
56
|
+
.DS_Store
|
|
57
|
+
Thumbs.db
|
|
58
|
+
desktop.ini
|
|
59
|
+
|
|
60
|
+
# Git
|
|
61
|
+
.git/
|
|
62
|
+
.gitignore
|
|
63
|
+
|
|
64
|
+
# Testing
|
|
65
|
+
coverage/
|
|
66
|
+
.nyc_output/
|
|
67
|
+
|
|
68
|
+
# Package managers
|
|
69
|
+
package-lock.json
|
|
70
|
+
yarn.lock
|
|
71
|
+
pnpm-lock.yaml
|
|
72
|
+
|
|
73
|
+
# Misc
|
|
74
|
+
*.zip
|
|
75
|
+
*.tar.gz
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
function initJetrignore(targetDir, silent = false) {
|
|
79
|
+
const ignoreFile = path.join(targetDir, '.jetrignore');
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(ignoreFile)) {
|
|
82
|
+
if (!silent) {
|
|
83
|
+
console.log(chalk.yellow('.jetrignore already exists in this directory'));
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(ignoreFile, DEFAULT_JETRIGNORE);
|
|
89
|
+
if (!silent) {
|
|
90
|
+
console.log(chalk.green('Created .jetrignore with default ignore patterns'));
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function showHelp() {
|
|
96
|
+
console.log(`
|
|
97
|
+
${chalk.bold('jetr')} - Deploy static websites instantly
|
|
98
|
+
|
|
99
|
+
${chalk.bold('Usage:')}
|
|
100
|
+
jetr <directory> [projectName]
|
|
101
|
+
|
|
102
|
+
${chalk.bold('Examples:')}
|
|
103
|
+
jetr ./ # Deploy current directory with random name
|
|
104
|
+
jetr ./ my-site # Deploy current directory as my-site
|
|
105
|
+
jetr ../app # Deploy ../app with random name
|
|
106
|
+
jetr ./dist production # Deploy ./dist as production
|
|
107
|
+
|
|
108
|
+
${chalk.bold('Commands:')}
|
|
109
|
+
init Create a .jetrignore file with default patterns
|
|
110
|
+
|
|
111
|
+
${chalk.bold('Options:')}
|
|
112
|
+
-h, --help Show this help message
|
|
113
|
+
-v, --version Show version number
|
|
114
|
+
|
|
115
|
+
${chalk.bold('.jetrignore:')}
|
|
116
|
+
Create a .jetrignore file in your project root to exclude files.
|
|
117
|
+
Uses the same syntax as .gitignore.
|
|
118
|
+
Run "jetr init" to generate one with common defaults.
|
|
119
|
+
`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function showVersion() {
|
|
123
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
124
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
125
|
+
console.log(pkg.version);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function generateProjectName() {
|
|
129
|
+
return nanoid(12).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sanitizeProjectName(name) {
|
|
133
|
+
const sanitized = name
|
|
134
|
+
.toLowerCase()
|
|
135
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
136
|
+
.replace(/^-+|-+$/g, '')
|
|
137
|
+
.replace(/-+/g, '-');
|
|
138
|
+
|
|
139
|
+
// If sanitization results in empty string, generate a random name
|
|
140
|
+
if (!sanitized) {
|
|
141
|
+
return generateProjectName();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return sanitized;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============ Config file functions ============
|
|
148
|
+
|
|
149
|
+
function loadConfig(targetDir) {
|
|
150
|
+
const configPath = path.join(targetDir, CONFIG_FILE);
|
|
151
|
+
if (fs.existsSync(configPath)) {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
154
|
+
} catch {
|
|
155
|
+
return { directory: null, files: {} };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { directory: null, files: {} };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function saveConfig(targetDir, config) {
|
|
162
|
+
const configPath = path.join(targetDir, CONFIG_FILE);
|
|
163
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getConfigKey(singleFile) {
|
|
167
|
+
// For directory mode, use 'directory'; for single file, use filename
|
|
168
|
+
return singleFile ? path.basename(singleFile) : 'directory';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getProjectFromConfig(config, key) {
|
|
172
|
+
if (key === 'directory') {
|
|
173
|
+
return config.directory;
|
|
174
|
+
}
|
|
175
|
+
return config.files?.[key] || null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function updateConfig(config, key, projectName) {
|
|
179
|
+
if (key === 'directory') {
|
|
180
|
+
if (!config.directory) {
|
|
181
|
+
config.directory = { default: projectName, history: [projectName] };
|
|
182
|
+
} else {
|
|
183
|
+
config.directory.default = projectName;
|
|
184
|
+
if (!config.directory.history.includes(projectName)) {
|
|
185
|
+
config.directory.history.push(projectName);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
if (!config.files) config.files = {};
|
|
190
|
+
if (!config.files[key]) {
|
|
191
|
+
config.files[key] = { default: projectName, history: [projectName] };
|
|
192
|
+
} else {
|
|
193
|
+
config.files[key].default = projectName;
|
|
194
|
+
if (!config.files[key].history.includes(projectName)) {
|
|
195
|
+
config.files[key].history.push(projectName);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return config;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function promptSelection(options, message) {
|
|
203
|
+
const rl = readline.createInterface({
|
|
204
|
+
input: process.stdin,
|
|
205
|
+
output: process.stdout,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
console.log();
|
|
209
|
+
console.log(chalk.bold(message));
|
|
210
|
+
options.forEach((opt, i) => {
|
|
211
|
+
const marker = i === 0 ? chalk.green('(default)') : '';
|
|
212
|
+
console.log(` ${chalk.cyan(i + 1)}) ${opt} ${marker}`);
|
|
213
|
+
});
|
|
214
|
+
console.log(` ${chalk.cyan(options.length + 1)}) ${chalk.gray('Create new project')}`);
|
|
215
|
+
console.log();
|
|
216
|
+
|
|
217
|
+
return new Promise((resolve) => {
|
|
218
|
+
rl.question(chalk.bold('Select option [1]: '), (answer) => {
|
|
219
|
+
rl.close();
|
|
220
|
+
const num = parseInt(answer, 10);
|
|
221
|
+
if (!answer || isNaN(num) || num < 1) {
|
|
222
|
+
resolve({ type: 'existing', value: options[0] });
|
|
223
|
+
} else if (num <= options.length) {
|
|
224
|
+
resolve({ type: 'existing', value: options[num - 1] });
|
|
225
|
+
} else {
|
|
226
|
+
resolve({ type: 'new', value: null });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function promptNewName() {
|
|
233
|
+
const rl = readline.createInterface({
|
|
234
|
+
input: process.stdin,
|
|
235
|
+
output: process.stdout,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
rl.question(chalk.bold('Enter project name (leave empty for random): '), (answer) => {
|
|
240
|
+
rl.close();
|
|
241
|
+
resolve(answer.trim());
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function loadIgnoreRules(targetDir) {
|
|
247
|
+
const ig = ignore();
|
|
248
|
+
const ignoreFile = path.join(targetDir, '.jetrignore');
|
|
249
|
+
|
|
250
|
+
if (fs.existsSync(ignoreFile)) {
|
|
251
|
+
const content = fs.readFileSync(ignoreFile, 'utf-8');
|
|
252
|
+
ig.add(content);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return ig;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function createZip(targetDir, ig, singleFile = null) {
|
|
259
|
+
const tmpFile = path.join(targetDir, `.jetr-upload-${Date.now()}.zip`);
|
|
260
|
+
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
const output = fs.createWriteStream(tmpFile);
|
|
263
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
264
|
+
let fileCount = 0;
|
|
265
|
+
|
|
266
|
+
const cleanup = (err) => {
|
|
267
|
+
if (fs.existsSync(tmpFile)) {
|
|
268
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
269
|
+
}
|
|
270
|
+
reject(err);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
output.on('close', () => {
|
|
274
|
+
if (fileCount === 0) {
|
|
275
|
+
cleanup(new Error('No files to upload (directory is empty or all files are ignored)'));
|
|
276
|
+
} else {
|
|
277
|
+
resolve({ zipPath: tmpFile, fileCount });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
output.on('error', cleanup);
|
|
282
|
+
archive.on('error', cleanup);
|
|
283
|
+
archive.pipe(output);
|
|
284
|
+
|
|
285
|
+
if (singleFile) {
|
|
286
|
+
// Single file mode: just add the one file
|
|
287
|
+
const fileName = path.basename(singleFile);
|
|
288
|
+
archive.file(singleFile, { name: fileName });
|
|
289
|
+
fileCount = 1;
|
|
290
|
+
} else {
|
|
291
|
+
// Directory mode: add all files respecting .jetrignore
|
|
292
|
+
const addFiles = (dir, base = '') => {
|
|
293
|
+
let entries;
|
|
294
|
+
try {
|
|
295
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return; // Skip directories we can't read
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
const relativePath = path.join(base, entry.name);
|
|
302
|
+
const fullPath = path.join(dir, entry.name);
|
|
303
|
+
|
|
304
|
+
// Skip temp zip files, .jetrignore and .jetrrc
|
|
305
|
+
if (entry.name.startsWith('.jetr-upload-') && entry.name.endsWith('.zip')) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (entry.name === '.jetrignore' || entry.name === '.jetrrc') {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Skip ignored files/directories
|
|
313
|
+
if (ig.ignores(relativePath) || ig.ignores(relativePath + '/')) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Skip symbolic links to avoid potential loops
|
|
318
|
+
if (entry.isSymbolicLink()) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (entry.isDirectory()) {
|
|
323
|
+
addFiles(fullPath, relativePath);
|
|
324
|
+
} else if (entry.isFile()) {
|
|
325
|
+
archive.file(fullPath, { name: relativePath });
|
|
326
|
+
fileCount++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
addFiles(targetDir);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
archive.finalize();
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function uploadOnce(zipPath, projectName) {
|
|
339
|
+
const form = new FormData();
|
|
340
|
+
form.append('projectName', projectName);
|
|
341
|
+
form.append('file', fs.createReadStream(zipPath), {
|
|
342
|
+
filename: path.basename(zipPath),
|
|
343
|
+
contentType: 'application/zip',
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
const request = form.submit(API_URL, (err, res) => {
|
|
348
|
+
if (err) {
|
|
349
|
+
reject(err);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let data = '';
|
|
354
|
+
res.on('data', (chunk) => {
|
|
355
|
+
data += chunk;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
res.on('end', () => {
|
|
359
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
360
|
+
try {
|
|
361
|
+
resolve(JSON.parse(data));
|
|
362
|
+
} catch (e) {
|
|
363
|
+
reject(new Error(`Invalid JSON response: ${data}`));
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
reject(new Error(`Upload failed: ${res.statusCode} ${res.statusMessage}\n${data}`));
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
res.on('error', reject);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
request.on('error', reject);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function upload(zipPath, projectName, maxRetries = 3) {
|
|
378
|
+
let lastError;
|
|
379
|
+
|
|
380
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
381
|
+
try {
|
|
382
|
+
return await uploadOnce(zipPath, projectName);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
lastError = err;
|
|
385
|
+
const isRetryable = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EPIPE', 'EAI_AGAIN'].includes(err.code);
|
|
386
|
+
|
|
387
|
+
if (isRetryable && attempt < maxRetries) {
|
|
388
|
+
const delay = attempt * 2000;
|
|
389
|
+
console.log(chalk.yellow(`\nConnection error (${err.code}), retrying in ${delay / 1000}s... (${attempt}/${maxRetries})`));
|
|
390
|
+
await new Promise(r => setTimeout(r, delay));
|
|
391
|
+
} else {
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
throw lastError;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function run() {
|
|
401
|
+
const args = process.argv.slice(2);
|
|
402
|
+
|
|
403
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
404
|
+
showHelp();
|
|
405
|
+
process.exit(0);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (args.includes('-v') || args.includes('--version')) {
|
|
409
|
+
showVersion();
|
|
410
|
+
process.exit(0);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (args[0] === 'init') {
|
|
414
|
+
const targetDir = path.resolve(args[1] || './');
|
|
415
|
+
initJetrignore(targetDir);
|
|
416
|
+
process.exit(0);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const inputPath = path.resolve(args[0] || './');
|
|
420
|
+
let projectName = args[1] || '';
|
|
421
|
+
let targetDir;
|
|
422
|
+
let singleFile = null;
|
|
423
|
+
|
|
424
|
+
if (!fs.existsSync(inputPath)) {
|
|
425
|
+
console.error(chalk.red(`Error: Path does not exist: ${inputPath}`));
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const stats = fs.statSync(inputPath);
|
|
430
|
+
|
|
431
|
+
if (stats.isFile()) {
|
|
432
|
+
// Single file mode: treat the file as a standalone project
|
|
433
|
+
singleFile = inputPath;
|
|
434
|
+
targetDir = path.dirname(inputPath);
|
|
435
|
+
console.log(chalk.gray(`Single file mode: ${path.basename(inputPath)}`));
|
|
436
|
+
} else if (stats.isDirectory()) {
|
|
437
|
+
targetDir = inputPath;
|
|
438
|
+
} else {
|
|
439
|
+
console.error(chalk.red(`Error: Invalid path: ${inputPath}`));
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Auto-create .jetrignore if it doesn't exist (only for directory mode)
|
|
444
|
+
if (!singleFile) {
|
|
445
|
+
const created = initJetrignore(targetDir, true);
|
|
446
|
+
if (created) {
|
|
447
|
+
console.log(chalk.gray('Auto-created .jetrignore with default patterns'));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Load config
|
|
452
|
+
const config = loadConfig(targetDir);
|
|
453
|
+
const configKey = getConfigKey(singleFile);
|
|
454
|
+
const savedProject = getProjectFromConfig(config, configKey);
|
|
455
|
+
|
|
456
|
+
if (projectName) {
|
|
457
|
+
// User specified a project name
|
|
458
|
+
projectName = sanitizeProjectName(projectName);
|
|
459
|
+
} else if (savedProject) {
|
|
460
|
+
// Have saved config, let user choose
|
|
461
|
+
const history = savedProject.history || [savedProject.default];
|
|
462
|
+
// Put default first, then rest of history
|
|
463
|
+
const options = [savedProject.default, ...history.filter(h => h !== savedProject.default)];
|
|
464
|
+
|
|
465
|
+
if (options.length === 1) {
|
|
466
|
+
// Only one option, use it directly
|
|
467
|
+
projectName = options[0];
|
|
468
|
+
console.log(chalk.gray(`Using saved project: ${projectName}`));
|
|
469
|
+
} else {
|
|
470
|
+
// Multiple options, prompt user
|
|
471
|
+
const selection = await promptSelection(options, 'Previous deployments found:');
|
|
472
|
+
if (selection.type === 'existing') {
|
|
473
|
+
projectName = selection.value;
|
|
474
|
+
} else {
|
|
475
|
+
const newName = await promptNewName();
|
|
476
|
+
projectName = newName ? sanitizeProjectName(newName) : generateProjectName();
|
|
477
|
+
console.log(chalk.gray(`Using project name: ${projectName}`));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
// No config, generate random name
|
|
482
|
+
projectName = generateProjectName();
|
|
483
|
+
console.log(chalk.gray(`Generated project name: ${projectName}`));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const ig = loadIgnoreRules(targetDir);
|
|
487
|
+
|
|
488
|
+
const spinner = ora('Packing files...').start();
|
|
489
|
+
let zipPath;
|
|
490
|
+
let fileCount;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const result = await createZip(targetDir, ig, singleFile);
|
|
494
|
+
zipPath = result.zipPath;
|
|
495
|
+
fileCount = result.fileCount;
|
|
496
|
+
const stats = fs.statSync(zipPath);
|
|
497
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
498
|
+
spinner.succeed(`Packed ${fileCount} file${fileCount > 1 ? 's' : ''} (${sizeMB} MB)`);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
spinner.fail('Failed to pack files');
|
|
501
|
+
console.error(chalk.red(err.message));
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const uploadSpinner = ora(`Uploading to ${projectName}...`).start();
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
const result = await upload(zipPath, projectName);
|
|
509
|
+
|
|
510
|
+
fs.unlinkSync(zipPath);
|
|
511
|
+
|
|
512
|
+
if (result.success) {
|
|
513
|
+
uploadSpinner.succeed('Deployed successfully!');
|
|
514
|
+
|
|
515
|
+
// Save config
|
|
516
|
+
const updatedConfig = updateConfig(config, configKey, projectName);
|
|
517
|
+
saveConfig(targetDir, updatedConfig);
|
|
518
|
+
|
|
519
|
+
console.log();
|
|
520
|
+
console.log(chalk.bold('Project: ') + chalk.cyan(result.projectName));
|
|
521
|
+
console.log(chalk.bold('URL: ') + chalk.green(result.defaultUrl || result.url));
|
|
522
|
+
console.log();
|
|
523
|
+
} else {
|
|
524
|
+
uploadSpinner.fail('Deployment failed');
|
|
525
|
+
console.error(chalk.red(JSON.stringify(result, null, 2)));
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
} catch (err) {
|
|
529
|
+
uploadSpinner.fail('Upload failed');
|
|
530
|
+
|
|
531
|
+
if (zipPath && fs.existsSync(zipPath)) {
|
|
532
|
+
fs.unlinkSync(zipPath);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
console.error(chalk.red(err.message));
|
|
536
|
+
if (err.code) {
|
|
537
|
+
console.error(chalk.gray(`Error code: ${err.code}`));
|
|
538
|
+
}
|
|
539
|
+
if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {
|
|
540
|
+
console.error(chalk.yellow('\nTip: This might be a network issue or the file is too large.'));
|
|
541
|
+
console.error(chalk.yellow('Try again or check your internet connection.'));
|
|
542
|
+
}
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
}
|