@micropage-sh/cli 2.0.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/README.md +183 -0
- package/bin/micropage.js +3 -0
- package/package.json +65 -0
- package/src/api.js +51 -0
- package/src/auth.js +123 -0
- package/src/commands/builds.js +496 -0
- package/src/commands/files.js +225 -0
- package/src/commands/forms.js +71 -0
- package/src/commands/links.js +107 -0
- package/src/commands/login.js +255 -0
- package/src/commands/logout.js +16 -0
- package/src/commands/projects.js +573 -0
- package/src/commands/publish.js +10 -0
- package/src/commands/submissions.js +194 -0
- package/src/commands/whoami.js +87 -0
- package/src/config.js +22 -0
- package/src/index.js +243 -0
- package/src/mime.js +18 -0
- package/src/parser.js +72 -0
- package/src/project-ref.js +54 -0
- package/src/supabase.js +637 -0
- package/src/utils.js +47 -0
- package/templates/PROJECT_AGENT.template.md +31 -0
- package/templates/assets/favicon.svg +8 -0
- package/templates/assets/logo.svg +14 -0
- package/templates/examples/components-colors.page +84 -0
- package/templates/examples/components-hero-variants.page +28 -0
- package/templates/examples/components-pricing-and-forms.page +45 -0
- package/templates/examples/mobile-app-landing.page +77 -0
- package/templates/examples/portfolio.page +75 -0
- package/templates/examples/startup-landing.page +100 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const {
|
|
8
|
+
db,
|
|
9
|
+
fn,
|
|
10
|
+
handleAuthError,
|
|
11
|
+
getValidAccessToken,
|
|
12
|
+
getMaxDeployEventIdForBuild,
|
|
13
|
+
streamDeployEventsUntilDone,
|
|
14
|
+
} = require('../supabase');
|
|
15
|
+
const { getProjectConfig, setProjectConfig } = require('../auth');
|
|
16
|
+
const { readPageFilesFromDir } = require('../parser');
|
|
17
|
+
const { BUILD_COMPILER_URL } = require('../config');
|
|
18
|
+
const { formatTable, formatDate } = require('../utils');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** CLI build version args: `16`, `v16`, ` v16 ` → build number; undefined if omitted. */
|
|
25
|
+
function parseOptionalBuildNumber(version) {
|
|
26
|
+
if (version === undefined || version === null) return undefined;
|
|
27
|
+
if (typeof version === 'number') {
|
|
28
|
+
if (!Number.isFinite(version)) {
|
|
29
|
+
console.error('Invalid build version. Use a number or vN (e.g. 16 or v16).');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
return version;
|
|
33
|
+
}
|
|
34
|
+
const s = String(version).trim();
|
|
35
|
+
if (s === '') return undefined;
|
|
36
|
+
const n = Number(s.replace(/^v\s*/i, ''));
|
|
37
|
+
if (!Number.isFinite(n)) {
|
|
38
|
+
console.error('Invalid build version. Use a number or vN (e.g. 16 or v16).');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function parseContent(rawContent, projectId, buildId) {
|
|
45
|
+
const accessToken = await getValidAccessToken();
|
|
46
|
+
|
|
47
|
+
const res = await fetch(`${BUILD_COMPILER_URL}/parse`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
Authorization: `Bearer ${accessToken}`,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
text: rawContent,
|
|
55
|
+
project_id: projectId,
|
|
56
|
+
build_id: buildId || null,
|
|
57
|
+
version: '2',
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
throw new Error(`Parser error: ${text}`);
|
|
63
|
+
}
|
|
64
|
+
return res.json();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requireProjectConfig(cwd) {
|
|
68
|
+
const config = getProjectConfig(cwd);
|
|
69
|
+
if (!config?.projectId) {
|
|
70
|
+
console.error('Not in a project folder. Run from a folder with .micropage/project.json');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function setActiveBuildId(projectId, buildId) {
|
|
77
|
+
await db.from('projects').eq('id', projectId).update({ active_build_id: buildId });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// build list
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
async function list(options = {}) {
|
|
85
|
+
const cwd = process.cwd();
|
|
86
|
+
const config = requireProjectConfig(cwd);
|
|
87
|
+
|
|
88
|
+
let builds;
|
|
89
|
+
try {
|
|
90
|
+
builds = await db
|
|
91
|
+
.from('builds')
|
|
92
|
+
.select('id,number,status,updated_at,failure_reason,archive_url,archive_size_bytes')
|
|
93
|
+
.eq('project_id', config.projectId)
|
|
94
|
+
.order('number', 'desc')
|
|
95
|
+
.get();
|
|
96
|
+
} catch (err) {
|
|
97
|
+
handleAuthError(err);
|
|
98
|
+
console.error('Failed to list builds:', err.message);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!Array.isArray(builds) || builds.length === 0) {
|
|
103
|
+
console.log('No builds for this project.');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (options.json) {
|
|
108
|
+
console.log(JSON.stringify(builds, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const rows = builds.map((b) => [
|
|
113
|
+
`v${b.number}`,
|
|
114
|
+
b.status,
|
|
115
|
+
formatDate(b.updated_at),
|
|
116
|
+
b.failure_reason ? b.failure_reason.slice(0, 40) : '',
|
|
117
|
+
]);
|
|
118
|
+
formatTable(rows, ['Version', 'Status', 'Updated', 'Failure reason']);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// push (merge local files → update/create draft build)
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
async function push(options = {}) {
|
|
126
|
+
const cwd = process.cwd();
|
|
127
|
+
const config = requireProjectConfig(cwd);
|
|
128
|
+
|
|
129
|
+
let rawContent;
|
|
130
|
+
try {
|
|
131
|
+
rawContent = readPageFilesFromDir(cwd);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(err.message);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!rawContent.trim()) {
|
|
138
|
+
console.error('No content found in .page files.');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Parse content via parser API
|
|
143
|
+
console.log('Parsing content…');
|
|
144
|
+
let jsonContent;
|
|
145
|
+
try {
|
|
146
|
+
jsonContent = await parseContent(rawContent, config.projectId, config.buildId);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('Parse failed:', err.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Decide whether to update existing draft or create new build
|
|
153
|
+
let existingBuild = null;
|
|
154
|
+
if (config.buildId) {
|
|
155
|
+
try {
|
|
156
|
+
existingBuild = await db.from('builds').select('id,number,status').eq('id', config.buildId).single();
|
|
157
|
+
} catch {
|
|
158
|
+
// build not found – will create new
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const isDraft = existingBuild && (existingBuild.status === 'draft' || existingBuild.status === 'failed');
|
|
163
|
+
|
|
164
|
+
let build;
|
|
165
|
+
try {
|
|
166
|
+
if (isDraft) {
|
|
167
|
+
// Update existing draft
|
|
168
|
+
const results = await db
|
|
169
|
+
.from('builds')
|
|
170
|
+
.eq('id', existingBuild.id)
|
|
171
|
+
.update({ raw_content: rawContent, json_content: jsonContent });
|
|
172
|
+
build = Array.isArray(results) ? results[0] : existingBuild;
|
|
173
|
+
console.log(`Updated build v${existingBuild.number}.`);
|
|
174
|
+
} else {
|
|
175
|
+
// Create new draft build
|
|
176
|
+
const payload = {
|
|
177
|
+
project_id: config.projectId,
|
|
178
|
+
raw_content: rawContent,
|
|
179
|
+
json_content: jsonContent,
|
|
180
|
+
status: 'draft',
|
|
181
|
+
parser_version: '2',
|
|
182
|
+
};
|
|
183
|
+
const results = await db.from('builds').insert(payload);
|
|
184
|
+
build = Array.isArray(results) ? results[0] : results;
|
|
185
|
+
console.log(`Created build v${build.number}.`);
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
handleAuthError(err);
|
|
189
|
+
console.error('Push failed:', err.message);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Persist buildId and align project.active_build_id (editor / publish default)
|
|
194
|
+
setProjectConfig(cwd, { ...config, buildId: build.id });
|
|
195
|
+
try {
|
|
196
|
+
await setActiveBuildId(config.projectId, build.id);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
handleAuthError(err);
|
|
199
|
+
console.error('Failed to set active build:', err.message);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// publish (push if needed, then trigger publish)
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
async function publish(options = {}) {
|
|
209
|
+
const cwd = process.cwd();
|
|
210
|
+
const config = requireProjectConfig(cwd);
|
|
211
|
+
|
|
212
|
+
// Always push first to upload latest local content
|
|
213
|
+
if (!options.noPush) {
|
|
214
|
+
console.log('Pushing local content…');
|
|
215
|
+
await push({ ...options, skipPublish: true });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Re-read config after push (buildId may have changed)
|
|
219
|
+
const updatedConfig = getProjectConfig(cwd);
|
|
220
|
+
|
|
221
|
+
if (!updatedConfig.buildId) {
|
|
222
|
+
console.error('No build ID found after push. Cannot publish.');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Fetch live project record to pick up custom_domain (not stored in local config).
|
|
227
|
+
let customDomain = null;
|
|
228
|
+
try {
|
|
229
|
+
const proj = await db
|
|
230
|
+
.from('projects')
|
|
231
|
+
.select('custom_domain')
|
|
232
|
+
.eq('id', updatedConfig.projectId)
|
|
233
|
+
.single();
|
|
234
|
+
customDomain = proj?.custom_domain || null;
|
|
235
|
+
} catch {
|
|
236
|
+
// best-effort; fall back to default domain
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log('Publishing…');
|
|
240
|
+
try {
|
|
241
|
+
const eventCursor = await getMaxDeployEventIdForBuild(updatedConfig.buildId);
|
|
242
|
+
await fn.invoke('publish-build', {
|
|
243
|
+
buildId: updatedConfig.buildId,
|
|
244
|
+
projectId: updatedConfig.projectId,
|
|
245
|
+
});
|
|
246
|
+
console.log('Publish triggered. The build will be deployed in a few moments.');
|
|
247
|
+
const viewUrl = customDomain
|
|
248
|
+
? `https://${customDomain}`
|
|
249
|
+
: `https://${updatedConfig.domain}.micropage.sh`;
|
|
250
|
+
console.log(`View at: ${viewUrl}`);
|
|
251
|
+
if (options.watch) {
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log('Build / deploy events:');
|
|
254
|
+
try {
|
|
255
|
+
const accessToken = await getValidAccessToken();
|
|
256
|
+
const { terminalEvent } = await streamDeployEventsUntilDone(
|
|
257
|
+
accessToken,
|
|
258
|
+
updatedConfig.projectId,
|
|
259
|
+
updatedConfig.buildId,
|
|
260
|
+
{ afterId: eventCursor },
|
|
261
|
+
);
|
|
262
|
+
if (terminalEvent?.event_type === 'build.failed') {
|
|
263
|
+
const msg =
|
|
264
|
+
(terminalEvent.payload && (terminalEvent.payload.error || terminalEvent.payload.message)) ||
|
|
265
|
+
'Build failed';
|
|
266
|
+
console.error(msg);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
} catch (streamErr) {
|
|
270
|
+
console.error('Event stream failed:', streamErr.message);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
handleAuthError(err);
|
|
276
|
+
console.error('Publish failed:', err.message);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// build redeploy (set active build + republish that snapshot, no new row)
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
async function redeploy(versionArg, options = {}) {
|
|
286
|
+
const cwd = process.cwd();
|
|
287
|
+
const config = requireProjectConfig(cwd);
|
|
288
|
+
const buildNumber = parseOptionalBuildNumber(versionArg);
|
|
289
|
+
|
|
290
|
+
// Find the build by number
|
|
291
|
+
let sourceBuild;
|
|
292
|
+
try {
|
|
293
|
+
if (buildNumber !== undefined) {
|
|
294
|
+
sourceBuild = await db
|
|
295
|
+
.from('builds')
|
|
296
|
+
.select('id,number,status')
|
|
297
|
+
.eq('project_id', config.projectId)
|
|
298
|
+
.eq('number', buildNumber)
|
|
299
|
+
.single();
|
|
300
|
+
} else {
|
|
301
|
+
// Default: use current build
|
|
302
|
+
if (!config.buildId) {
|
|
303
|
+
console.error('No build number specified and no current build ID found.');
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
sourceBuild = await db
|
|
307
|
+
.from('builds')
|
|
308
|
+
.select('id,number,status')
|
|
309
|
+
.eq('id', config.buildId)
|
|
310
|
+
.single();
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
handleAuthError(err);
|
|
314
|
+
console.error('Failed to find build:', err.message);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!sourceBuild) {
|
|
319
|
+
console.error('Build not found.');
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log(`Republishing v${sourceBuild.number} (setting active build)…`);
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
await setActiveBuildId(config.projectId, sourceBuild.id);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
handleAuthError(err);
|
|
329
|
+
console.error('Failed to set active build:', err.message);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
setProjectConfig(cwd, { ...config, buildId: sourceBuild.id });
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
await fn.invoke('publish-build', {
|
|
337
|
+
buildId: sourceBuild.id,
|
|
338
|
+
projectId: config.projectId,
|
|
339
|
+
});
|
|
340
|
+
console.log(`Publish triggered for v${sourceBuild.number}.`);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
handleAuthError(err);
|
|
343
|
+
console.error('Publish failed:', err.message);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// build download
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
async function download(versionArg, options = {}) {
|
|
353
|
+
const cwd = process.cwd();
|
|
354
|
+
const config = requireProjectConfig(cwd);
|
|
355
|
+
const buildNumber = parseOptionalBuildNumber(versionArg);
|
|
356
|
+
|
|
357
|
+
let build;
|
|
358
|
+
try {
|
|
359
|
+
if (buildNumber !== undefined) {
|
|
360
|
+
build = await db
|
|
361
|
+
.from('builds')
|
|
362
|
+
.select('id,number,status,archive_url,project_id')
|
|
363
|
+
.eq('project_id', config.projectId)
|
|
364
|
+
.eq('number', buildNumber)
|
|
365
|
+
.single();
|
|
366
|
+
} else {
|
|
367
|
+
// Download latest deployed build
|
|
368
|
+
const builds = await db
|
|
369
|
+
.from('builds')
|
|
370
|
+
.select('id,number,status,archive_url,project_id')
|
|
371
|
+
.eq('project_id', config.projectId)
|
|
372
|
+
.eq('status', 'deployed')
|
|
373
|
+
.order('number', 'desc')
|
|
374
|
+
.limit(1)
|
|
375
|
+
.get();
|
|
376
|
+
build = builds?.[0] || null;
|
|
377
|
+
}
|
|
378
|
+
} catch (err) {
|
|
379
|
+
handleAuthError(err);
|
|
380
|
+
console.error('Failed to find build:', err.message);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!build) {
|
|
385
|
+
console.error('No matching build found.');
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (build.status !== 'deployed') {
|
|
390
|
+
console.error(`Build v${build.number} is not deployed; archives are only available for deployed builds.`);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const projectIdForArchive = build.project_id != null ? build.project_id : config.projectId;
|
|
395
|
+
const archiveEventCursor = await getMaxDeployEventIdForBuild(build.id);
|
|
396
|
+
|
|
397
|
+
// Invoke async archive creation; publisher job uploads zip and sets archive_url; events stream progress.
|
|
398
|
+
try {
|
|
399
|
+
const data = await fn.invoke('request-build-archive', {
|
|
400
|
+
buildId: String(build.id),
|
|
401
|
+
projectId: String(projectIdForArchive),
|
|
402
|
+
});
|
|
403
|
+
if (data?.already_ready && data.archive_url) {
|
|
404
|
+
build.archive_url = data.archive_url;
|
|
405
|
+
} else if (data?.queued || data?.pending) {
|
|
406
|
+
console.log('Archive generation queued — streaming progress (zip may take several minutes)…');
|
|
407
|
+
try {
|
|
408
|
+
const accessToken = await getValidAccessToken();
|
|
409
|
+
const { terminalEvent } = await streamDeployEventsUntilDone(
|
|
410
|
+
accessToken,
|
|
411
|
+
projectIdForArchive,
|
|
412
|
+
build.id,
|
|
413
|
+
{ afterId: archiveEventCursor },
|
|
414
|
+
);
|
|
415
|
+
if (terminalEvent?.event_type === 'archive.failed') {
|
|
416
|
+
const msg =
|
|
417
|
+
(terminalEvent.payload && (terminalEvent.payload.error || terminalEvent.payload.message)) ||
|
|
418
|
+
'Archive generation failed';
|
|
419
|
+
console.error(msg);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
if (terminalEvent?.event_type === 'archive.completed' && terminalEvent.payload?.archive_url) {
|
|
423
|
+
build.archive_url = terminalEvent.payload.archive_url;
|
|
424
|
+
}
|
|
425
|
+
} catch (streamErr) {
|
|
426
|
+
handleAuthError(streamErr);
|
|
427
|
+
console.error('Event stream failed:', streamErr.message);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
console.warn('Archive response did not include ready/queued/pending; polling builds table.');
|
|
432
|
+
}
|
|
433
|
+
} catch (err) {
|
|
434
|
+
handleAuthError(err);
|
|
435
|
+
console.error('Failed to invoke request-build-archive:', err.message);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Poll builds table until archive_url is populated or timeout.
|
|
440
|
+
const started = Date.now();
|
|
441
|
+
const timeoutMs = 2 * 60 * 1000; // 2 minutes
|
|
442
|
+
const pollIntervalMs = 5000;
|
|
443
|
+
while (!build.archive_url && Date.now() - started < timeoutMs) {
|
|
444
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
445
|
+
const rows = await db
|
|
446
|
+
.from('builds')
|
|
447
|
+
.select('id,number,status,archive_url')
|
|
448
|
+
.eq('id', build.id)
|
|
449
|
+
.get();
|
|
450
|
+
const refreshed = rows?.[0];
|
|
451
|
+
if (!refreshed) {
|
|
452
|
+
console.error('Build disappeared while waiting for archive.');
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
if (refreshed.status !== 'deployed') {
|
|
456
|
+
console.error(`Build changed status to ${refreshed.status} while waiting for archive; aborting.`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
build = refreshed;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!build.archive_url) {
|
|
463
|
+
console.error('Timed out waiting for archive to become available.');
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const outFile = options.output || path.join(cwd, `build-v${build.number}.zip`);
|
|
468
|
+
console.log(`Downloading build v${build.number} → ${outFile}`);
|
|
469
|
+
|
|
470
|
+
await downloadUrl(build.archive_url, outFile);
|
|
471
|
+
console.log('Download complete.');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function downloadUrl(url, dest) {
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
const proto = url.startsWith('https') ? https : http;
|
|
477
|
+
const file = fs.createWriteStream(dest);
|
|
478
|
+
proto.get(url, (res) => {
|
|
479
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
480
|
+
file.close();
|
|
481
|
+
fs.unlinkSync(dest);
|
|
482
|
+
downloadUrl(res.headers.location, dest).then(resolve).catch(reject);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (res.statusCode !== 200) {
|
|
486
|
+
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
res.pipe(file);
|
|
490
|
+
file.on('finish', () => file.close(resolve));
|
|
491
|
+
file.on('error', reject);
|
|
492
|
+
}).on('error', reject);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
module.exports = { list, push, publish, redeploy, download };
|