moontraze 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/moontraze.js +11 -7
- package/lib/auth.js +199 -16
- package/lib/deploy.js +41 -9
- package/package.json +1 -1
package/bin/moontraze.js
CHANGED
|
@@ -10,13 +10,13 @@ const program = new Command();
|
|
|
10
10
|
program
|
|
11
11
|
.name('moontraze')
|
|
12
12
|
.description('Deploy sites to Moontraze hosting')
|
|
13
|
-
.version('1.0.
|
|
13
|
+
.version('1.0.2');
|
|
14
14
|
|
|
15
15
|
// npx moontraze login
|
|
16
16
|
program
|
|
17
17
|
.command('login')
|
|
18
|
-
.description('
|
|
19
|
-
.option('--token <jwt>', 'JWT token (
|
|
18
|
+
.description('Log in with email/password or MoonID')
|
|
19
|
+
.option('--token <jwt>', 'JWT token (advanced, skip interactive login)')
|
|
20
20
|
.action(async (opts) => {
|
|
21
21
|
try {
|
|
22
22
|
await login(opts.token);
|
|
@@ -49,8 +49,14 @@ program
|
|
|
49
49
|
if (!projectName) process.exit(1);
|
|
50
50
|
projectName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
51
51
|
setProject({ projectName });
|
|
52
|
-
console.log(
|
|
53
|
-
|
|
52
|
+
console.log(
|
|
53
|
+
chalk.green(
|
|
54
|
+
`Linked → ${projectName}.${getConfig().domain || 'frelanceo.com'}`
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
console.log(
|
|
58
|
+
chalk.gray('(saved .moontraze/project.json — same as .vercel)')
|
|
59
|
+
);
|
|
54
60
|
} catch (e) {
|
|
55
61
|
console.error(chalk.red(e.message));
|
|
56
62
|
process.exit(1);
|
|
@@ -58,7 +64,6 @@ program
|
|
|
58
64
|
});
|
|
59
65
|
|
|
60
66
|
// npx moontraze deploy --prod
|
|
61
|
-
// also: npx moontraze --prod (vercel style)
|
|
62
67
|
async function runDeploy(opts) {
|
|
63
68
|
try {
|
|
64
69
|
await deploy({
|
|
@@ -85,7 +90,6 @@ program
|
|
|
85
90
|
.option('--production', 'Deploy to production')
|
|
86
91
|
.option('-p, --project <name>', 'Project name')
|
|
87
92
|
.action(async (opts, cmd) => {
|
|
88
|
-
// only if no subcommand
|
|
89
93
|
if (cmd.args?.length) return;
|
|
90
94
|
if (opts.prod || opts.production || opts.project) {
|
|
91
95
|
await runDeploy(opts);
|
package/lib/auth.js
CHANGED
|
@@ -1,38 +1,221 @@
|
|
|
1
1
|
const prompts = require('prompts');
|
|
2
2
|
const chalk = require('chalk');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const { URL } = require('url');
|
|
3
5
|
const { setConfig, getConfig } = require('./config');
|
|
4
6
|
|
|
5
7
|
async function login(tokenArg) {
|
|
6
|
-
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
// Power user: --token still works (hidden)
|
|
9
|
+
if (tokenArg && String(tokenArg).trim()) {
|
|
10
|
+
return saveAndValidateToken(String(tokenArg).trim());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(chalk.cyan('Moontraze Login'));
|
|
15
|
+
console.log('');
|
|
16
|
+
|
|
17
|
+
const { method } = await prompts({
|
|
18
|
+
type: 'select',
|
|
19
|
+
name: 'method',
|
|
20
|
+
message: 'How do you want to log in?',
|
|
21
|
+
choices: [
|
|
22
|
+
{ title: 'Email & Password', value: 'email' },
|
|
23
|
+
{ title: 'MoonID (scan QR with app)', value: 'moonid' },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!method) {
|
|
28
|
+
throw new Error('Login cancelled');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (method === 'email') {
|
|
32
|
+
await loginWithEmail();
|
|
33
|
+
} else {
|
|
34
|
+
await loginWithMoonID();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function loginWithEmail() {
|
|
39
|
+
const answers = await prompts([
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
name: 'email',
|
|
43
|
+
message: 'Email',
|
|
44
|
+
validate: (v) =>
|
|
45
|
+
v && v.includes('@') ? true : 'Enter a valid email',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
12
48
|
type: 'password',
|
|
13
|
-
name: '
|
|
14
|
-
message: '
|
|
15
|
-
|
|
16
|
-
|
|
49
|
+
name: 'password',
|
|
50
|
+
message: 'Password',
|
|
51
|
+
validate: (v) => (v && v.length >= 1 ? true : 'Password required'),
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
if (!answers.email || !answers.password) {
|
|
56
|
+
throw new Error('Login cancelled');
|
|
17
57
|
}
|
|
18
|
-
|
|
19
|
-
|
|
58
|
+
|
|
59
|
+
const cfg = getConfig();
|
|
60
|
+
const fetch = require('node-fetch');
|
|
61
|
+
|
|
62
|
+
const res = await fetch(`${cfg.apiUrl}/api/platform/login`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
email: answers.email.trim(),
|
|
67
|
+
password: answers.password,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const data = await res.json().catch(() => ({}));
|
|
72
|
+
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
throw new Error(data.error || 'Invalid email or password');
|
|
20
75
|
}
|
|
21
|
-
token = String(token).trim();
|
|
22
76
|
|
|
77
|
+
if (!data.token) {
|
|
78
|
+
throw new Error('Login succeeded but no token returned');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setConfig({ token: data.token });
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(chalk.green('✓ Logged in'));
|
|
84
|
+
if (data.user?.email) {
|
|
85
|
+
console.log(chalk.gray(` ${data.user.email}`));
|
|
86
|
+
}
|
|
87
|
+
console.log(chalk.gray(` Config: ~/.moontraze/config.json`));
|
|
88
|
+
console.log('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function loginWithMoonID() {
|
|
92
|
+
const cfg = getConfig();
|
|
93
|
+
const state = Math.random().toString(36).substring(2, 12);
|
|
94
|
+
const port = 17321 + Math.floor(Math.random() * 100);
|
|
95
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
96
|
+
|
|
97
|
+
const appId = 'com.moonstorage.app';
|
|
98
|
+
const deepLink =
|
|
99
|
+
`moonid://auth` +
|
|
100
|
+
`?app_id=${encodeURIComponent(appId)}` +
|
|
101
|
+
`&action=login` +
|
|
102
|
+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
103
|
+
`&state=${encodeURIComponent(state)}`;
|
|
104
|
+
|
|
105
|
+
const qrImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(
|
|
106
|
+
deepLink
|
|
107
|
+
)}`;
|
|
108
|
+
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(chalk.cyan('MoonID Login'));
|
|
111
|
+
console.log(chalk.gray('1. Open MoonID app on your phone'));
|
|
112
|
+
console.log(chalk.gray('2. Scan the QR code (open link below in browser)'));
|
|
113
|
+
console.log(chalk.gray('3. Approve login'));
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(chalk.yellow('QR code image:'));
|
|
116
|
+
console.log(chalk.underline(qrImageUrl));
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(chalk.gray('Deep link (if needed):'));
|
|
119
|
+
console.log(chalk.gray(deepLink));
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log(chalk.gray('Waiting for approval... (Ctrl+C to cancel)'));
|
|
122
|
+
console.log('');
|
|
123
|
+
|
|
124
|
+
// Local server — MoonID redirect yahan aayega
|
|
125
|
+
const token = await waitForMoonIDCallback({ port, state, timeoutMs: 120000 });
|
|
126
|
+
|
|
127
|
+
if (!token) {
|
|
128
|
+
throw new Error('MoonID login timed out or denied');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await saveAndValidateToken(token);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function waitForMoonIDCallback({ port, state, timeoutMs }) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const server = http.createServer((req, res) => {
|
|
137
|
+
try {
|
|
138
|
+
const u = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
139
|
+
|
|
140
|
+
if (u.pathname !== '/callback') {
|
|
141
|
+
res.writeHead(404);
|
|
142
|
+
res.end('Not found');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const returnedState = u.searchParams.get('state');
|
|
147
|
+
const token =
|
|
148
|
+
u.searchParams.get('token') ||
|
|
149
|
+
u.searchParams.get('access_token') ||
|
|
150
|
+
u.searchParams.get('jwt');
|
|
151
|
+
const error = u.searchParams.get('error');
|
|
152
|
+
|
|
153
|
+
if (error) {
|
|
154
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
155
|
+
res.end(
|
|
156
|
+
'<html><body style="font-family:sans-serif;text-align:center;padding:40px"><h2>Login denied</h2><p>You can close this tab.</p></body></html>'
|
|
157
|
+
);
|
|
158
|
+
server.close();
|
|
159
|
+
resolve(null);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (returnedState && returnedState !== state) {
|
|
164
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
165
|
+
res.end('Invalid state');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!token) {
|
|
170
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
171
|
+
res.end(
|
|
172
|
+
'<html><body style="font-family:sans-serif;text-align:center;padding:40px"><h2>No token received</h2><p>Close this tab and try again.</p></body></html>'
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
178
|
+
res.end(
|
|
179
|
+
'<html><body style="font-family:sans-serif;text-align:center;padding:40px"><h2>✓ Login successful</h2><p>You can close this tab and return to the terminal.</p></body></html>'
|
|
180
|
+
);
|
|
181
|
+
server.close();
|
|
182
|
+
resolve(token);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
res.writeHead(500);
|
|
185
|
+
res.end('Error');
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
server.listen(port, '127.0.0.1');
|
|
190
|
+
|
|
191
|
+
const timer = setTimeout(() => {
|
|
192
|
+
try {
|
|
193
|
+
server.close();
|
|
194
|
+
} catch (_) {}
|
|
195
|
+
resolve(null);
|
|
196
|
+
}, timeoutMs);
|
|
197
|
+
|
|
198
|
+
server.on('close', () => clearTimeout(timer));
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function saveAndValidateToken(token) {
|
|
23
203
|
const cfg = getConfig();
|
|
24
|
-
// optional: validate token
|
|
25
204
|
const fetch = require('node-fetch');
|
|
205
|
+
|
|
26
206
|
const res = await fetch(`${cfg.apiUrl}/api/hosting/quota`, {
|
|
27
207
|
headers: { Authorization: `Bearer ${token}` },
|
|
28
208
|
});
|
|
209
|
+
|
|
29
210
|
if (res.status === 401) {
|
|
30
211
|
throw new Error('Invalid or expired token');
|
|
31
212
|
}
|
|
32
|
-
|
|
213
|
+
|
|
33
214
|
setConfig({ token });
|
|
215
|
+
console.log('');
|
|
34
216
|
console.log(chalk.green('✓ Logged in'));
|
|
35
|
-
console.log(chalk.gray(`Config: ~/.moontraze/config.json`));
|
|
217
|
+
console.log(chalk.gray(` Config: ~/.moontraze/config.json`));
|
|
218
|
+
console.log('');
|
|
36
219
|
}
|
|
37
220
|
|
|
38
221
|
module.exports = { login };
|
package/lib/deploy.js
CHANGED
|
@@ -14,7 +14,10 @@ async function deploy({ prod = true, projectName } = {}) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const linked = getProject();
|
|
17
|
-
const name = (projectName || linked?.projectName || '')
|
|
17
|
+
const name = (projectName || linked?.projectName || '')
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
20
|
+
|
|
18
21
|
if (!name || name.length < 3) {
|
|
19
22
|
throw new Error('No project linked. Run: npx moontraze link my-site');
|
|
20
23
|
}
|
|
@@ -24,28 +27,54 @@ async function deploy({ prod = true, projectName } = {}) {
|
|
|
24
27
|
|
|
25
28
|
console.log('');
|
|
26
29
|
console.log(chalk.cyan('Moontraze'));
|
|
27
|
-
console.log(`
|
|
28
|
-
console.log(`
|
|
29
|
-
console.log(`
|
|
30
|
+
console.log(` Inspect ${api}`);
|
|
31
|
+
console.log(` Project ${name}`);
|
|
32
|
+
console.log(` Target https://${name}.${domain}`);
|
|
30
33
|
console.log('');
|
|
31
34
|
|
|
32
35
|
const spinner = ora('Packaging...').start();
|
|
33
36
|
const t0 = Date.now();
|
|
34
37
|
let zipPath;
|
|
38
|
+
|
|
35
39
|
try {
|
|
36
40
|
zipPath = await zipCwd();
|
|
37
41
|
const mb = (fs.statSync(zipPath).size / 1024 / 1024).toFixed(2);
|
|
42
|
+
spinner.text = `Checking project...`;
|
|
43
|
+
|
|
44
|
+
// 1. Pehle check karo project already exist karta hai ya nahi
|
|
45
|
+
let alreadyExists = false;
|
|
46
|
+
try {
|
|
47
|
+
const statusRes = await fetch(`${api}/api/hosting/status/${name}`, {
|
|
48
|
+
headers: { Authorization: `Bearer ${cfg.token}` },
|
|
49
|
+
});
|
|
50
|
+
if (statusRes.ok) {
|
|
51
|
+
const statusData = await statusRes.json();
|
|
52
|
+
alreadyExists = !!statusData.exists;
|
|
53
|
+
}
|
|
54
|
+
} catch (_) {
|
|
55
|
+
// status fail → assume new
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
spinner.text = `Uploading (${mb} MB)...`;
|
|
39
59
|
|
|
40
60
|
const form = new FormData();
|
|
41
61
|
form.append('projectName', name);
|
|
42
|
-
form.append('redeploy', prod ? 'true' : 'true'); // always overwrite for existing
|
|
43
62
|
form.append('file', fs.createReadStream(zipPath), {
|
|
44
63
|
filename: `${name}.zip`,
|
|
45
64
|
contentType: 'application/zip',
|
|
46
65
|
});
|
|
47
66
|
|
|
48
|
-
|
|
67
|
+
// 2. Exist karta hai to /redeploy, nahi to /deploy
|
|
68
|
+
const endpoint = alreadyExists
|
|
69
|
+
? `${api}/api/hosting/redeploy`
|
|
70
|
+
: `${api}/api/hosting/deploy`;
|
|
71
|
+
|
|
72
|
+
// /deploy pe redeploy flag bhejo (safety)
|
|
73
|
+
if (!alreadyExists) {
|
|
74
|
+
form.append('redeploy', 'false');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const res = await fetch(endpoint, {
|
|
49
78
|
method: 'POST',
|
|
50
79
|
headers: {
|
|
51
80
|
Authorization: `Bearer ${cfg.token}`,
|
|
@@ -71,12 +100,15 @@ async function deploy({ prod = true, projectName } = {}) {
|
|
|
71
100
|
|
|
72
101
|
spinner.succeed(`Ready in ${sec}s`);
|
|
73
102
|
const url = data.url || `https://${name}.${domain}`;
|
|
103
|
+
|
|
74
104
|
console.log('');
|
|
75
|
-
console.log(chalk.green(`
|
|
105
|
+
console.log(chalk.green(` Production ${url}`));
|
|
76
106
|
if (data.deployment?.version) {
|
|
77
|
-
console.log(chalk.gray(`
|
|
107
|
+
console.log(chalk.gray(` Version ${data.deployment.version}`));
|
|
78
108
|
}
|
|
79
|
-
console.log(
|
|
109
|
+
console.log(
|
|
110
|
+
chalk.green(alreadyExists ? '✓ Redeployed' : '✓ Deployed')
|
|
111
|
+
);
|
|
80
112
|
console.log('');
|
|
81
113
|
} finally {
|
|
82
114
|
if (zipPath && fs.existsSync(zipPath)) {
|