@ilanalimanjs/open_pack 2.4.3 → 2.4.5
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/index.js +143 -126
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -8,34 +8,37 @@ import { parseArgs } from 'node:util';
|
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
9
|
import { randomBytes } from 'node:crypto';
|
|
10
10
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
11
|
-
import { downloadTemplate } from 'giget';
|
|
12
11
|
import * as prompts from '@clack/prompts';
|
|
13
12
|
import fse from 'fs-extra';
|
|
14
13
|
import chalk from 'chalk';
|
|
15
14
|
import ora from 'ora';
|
|
16
15
|
|
|
16
|
+
process.noDeprecation = true;
|
|
17
|
+
|
|
17
18
|
const { version: CLI_VERSION } = createRequire(import.meta.url)('../package.json');
|
|
18
19
|
|
|
19
20
|
const TEMPLATE_SOURCE = 'github:DevIlannn/open_pack/Source';
|
|
20
21
|
const REGISTRY_URL = 'https://registry.npmjs.org/@ilanalimanjs%2Fopen_pack/latest';
|
|
21
22
|
const PACKAGE_COMMAND = 'npx @ilanalimanjs/open_pack@latest';
|
|
22
23
|
const NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
23
|
-
const DATABASE_URL_PATTERN = /^postgres(ql)?:\/\/.+/;
|
|
24
24
|
const ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
|
|
25
25
|
const ENV_EXAMPLE_FILE = '.env.examples';
|
|
26
26
|
const ENV_FILE = '.env';
|
|
27
|
+
const LINUX_CHASSIS_FILE = '/sys/class/dmi/id/chassis_type';
|
|
28
|
+
const LINUX_POWER_SUPPLY_DIRECTORY = '/sys/class/power_supply';
|
|
29
|
+
const WINDOWS_CHASSIS_SCRIPT = '(Get-CimInstance -ClassName Win32_SystemEnclosure).ChassisTypes';
|
|
27
30
|
const DEFAULT_PORT = 3000;
|
|
28
|
-
const DEFAULT_DATABASE_PORT = 5432;
|
|
29
31
|
const MAX_PORT = 65535;
|
|
30
32
|
const PORT_SEARCH_RANGE = 50;
|
|
31
33
|
const MIN_NODE_VERSION = { major: 20, minor: 11 };
|
|
32
34
|
const BANNER_DELAY_MS = 70;
|
|
33
35
|
const STEP_MIN_DURATION_MS = 450;
|
|
34
36
|
const DETECT_TIMEOUT_MS = 4000;
|
|
35
|
-
const DATABASE_TIMEOUT_MS = 2500;
|
|
36
37
|
const UPDATE_TIMEOUT_MS = 2500;
|
|
37
38
|
const EXIT_INTERRUPTED = 130;
|
|
38
39
|
const GRADIENT = ['#22d3ee', '#38bdf8', '#60a5fa', '#818cf8', '#a78bfa'];
|
|
40
|
+
const CHASSIS_LAPTOP = new Set([8, 9, 10, 14, 31, 32]);
|
|
41
|
+
const CHASSIS_PC = new Set([3, 4, 5, 6, 7, 13, 15, 16, 24, 35, 36]);
|
|
39
42
|
|
|
40
43
|
const GLYPHS = [
|
|
41
44
|
[String.raw` ___ `, String.raw` / _ \ `, String.raw`| | | |`, String.raw`| |_| |`, String.raw` \___/ `],
|
|
@@ -60,7 +63,15 @@ const PACKAGE_MANAGERS = {
|
|
|
60
63
|
const PLATFORM_LABELS = {
|
|
61
64
|
darwin: 'macOS',
|
|
62
65
|
win32: 'Windows',
|
|
63
|
-
linux: 'Linux'
|
|
66
|
+
linux: 'Linux',
|
|
67
|
+
android: 'Android'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const DEVICE_TYPES = {
|
|
71
|
+
phone: 'Phone',
|
|
72
|
+
laptop: 'Laptop',
|
|
73
|
+
pc: 'PC',
|
|
74
|
+
other: 'Other'
|
|
64
75
|
};
|
|
65
76
|
|
|
66
77
|
const MODES = {
|
|
@@ -72,11 +83,9 @@ const MODES = {
|
|
|
72
83
|
const CLI_OPTIONS = {
|
|
73
84
|
yes: { type: 'boolean', short: 'y' },
|
|
74
85
|
pm: { type: 'string' },
|
|
75
|
-
'database-url': { type: 'string' },
|
|
76
86
|
port: { type: 'string' },
|
|
77
87
|
'no-install': { type: 'boolean' },
|
|
78
88
|
'no-run': { type: 'boolean' },
|
|
79
|
-
'no-db-check': { type: 'boolean' },
|
|
80
89
|
'no-update-check': { type: 'boolean' },
|
|
81
90
|
help: { type: 'boolean', short: 'h' },
|
|
82
91
|
version: { type: 'boolean', short: 'v' }
|
|
@@ -85,11 +94,9 @@ const CLI_OPTIONS = {
|
|
|
85
94
|
const HELP_OPTIONS = [
|
|
86
95
|
['-y, --yes', 'Skip prompts and use defaults'],
|
|
87
96
|
['--pm <name>', 'Package manager: npm, pnpm, yarn, bun'],
|
|
88
|
-
['--database-url <url>', 'PostgreSQL connection string'],
|
|
89
97
|
['--port <number>', `Development server port (default ${DEFAULT_PORT})`],
|
|
90
98
|
['--no-install', 'Create the project without installing dependencies'],
|
|
91
99
|
['--no-run', 'Install dependencies without starting the server'],
|
|
92
|
-
['--no-db-check', 'Skip the database reachability check'],
|
|
93
100
|
['--no-update-check', 'Skip the CLI update check'],
|
|
94
101
|
['-h, --help', 'Show this help'],
|
|
95
102
|
['-v, --version', 'Show the CLI version']
|
|
@@ -99,20 +106,33 @@ const accent = chalk.hex('#38bdf8');
|
|
|
99
106
|
const muted = chalk.gray;
|
|
100
107
|
const isWindows = process.platform === 'win32';
|
|
101
108
|
const isTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
109
|
+
const SHELL_TOOLS = new Set(Object.keys(PACKAGE_MANAGERS));
|
|
102
110
|
|
|
103
111
|
const STATE_ICONS = {
|
|
104
|
-
done: chalk.green('
|
|
105
|
-
warning: chalk.yellow('
|
|
106
|
-
failed: chalk.red('
|
|
112
|
+
done: chalk.green('●'),
|
|
113
|
+
warning: chalk.yellow('●'),
|
|
114
|
+
failed: chalk.red('●'),
|
|
107
115
|
pending: muted('○')
|
|
108
116
|
};
|
|
109
117
|
|
|
118
|
+
const STATUS_VALUES = {
|
|
119
|
+
done: (result) => muted(`${(result.duration / 1000).toFixed(1)}s`),
|
|
120
|
+
warning: (result) => chalk.yellow(`${(result.duration / 1000).toFixed(1)}s`),
|
|
121
|
+
failed: () => chalk.red('failed'),
|
|
122
|
+
pending: () => muted('pending')
|
|
123
|
+
};
|
|
124
|
+
|
|
110
125
|
class UsageError extends Error {}
|
|
111
126
|
|
|
112
127
|
const pause = (ms) => (isTTY ? sleep(ms) : Promise.resolve());
|
|
113
128
|
|
|
114
129
|
const visibleLength = (value) => value.replace(ANSI_PATTERN, '').length;
|
|
115
130
|
|
|
131
|
+
const spawnTool = (command, args, options) =>
|
|
132
|
+
isWindows && SHELL_TOOLS.has(command)
|
|
133
|
+
? spawn([command, ...args].join(' '), { ...options, shell: true })
|
|
134
|
+
: spawn(command, args, options);
|
|
135
|
+
|
|
116
136
|
const renderCard = (title, rows) => {
|
|
117
137
|
const labelWidth = Math.max(...rows.map((row) => visibleLength(row.label)));
|
|
118
138
|
const lines = rows.map((row) => `${row.label}${' '.repeat(labelWidth - visibleLength(row.label))} ${row.value}`);
|
|
@@ -141,9 +161,6 @@ const validateProjectName = (value) => {
|
|
|
141
161
|
return undefined;
|
|
142
162
|
};
|
|
143
163
|
|
|
144
|
-
const validateDatabaseUrl = (value) =>
|
|
145
|
-
DATABASE_URL_PATTERN.test(value ?? '') ? undefined : 'Use the format postgresql://user:password@host:port/db';
|
|
146
|
-
|
|
147
164
|
const validatePort = (value) => {
|
|
148
165
|
const port = Number(value);
|
|
149
166
|
return /^\d+$/.test(String(value ?? '')) && port >= 1 && port <= MAX_PORT
|
|
@@ -155,8 +172,7 @@ const validateFlags = ({ values, positionals }) => {
|
|
|
155
172
|
const checks = [
|
|
156
173
|
positionals[0] === undefined ? undefined : validateProjectName(positionals[0]),
|
|
157
174
|
values.pm === undefined || PACKAGE_MANAGERS[values.pm] ? undefined : '--pm must be one of npm, pnpm, yarn, bun',
|
|
158
|
-
values.port === undefined ? undefined : validatePort(values.port)
|
|
159
|
-
values['database-url'] === undefined ? undefined : validateDatabaseUrl(values['database-url'])
|
|
175
|
+
values.port === undefined ? undefined : validatePort(values.port)
|
|
160
176
|
];
|
|
161
177
|
return checks.find(Boolean);
|
|
162
178
|
};
|
|
@@ -170,26 +186,23 @@ const resolveFixedMode = (values) => {
|
|
|
170
186
|
const readFixedOptions = ({ values, positionals }) => ({
|
|
171
187
|
name: positionals[0],
|
|
172
188
|
packageManager: values.pm,
|
|
173
|
-
databaseUrl: values['database-url'],
|
|
174
189
|
port: values.port === undefined ? undefined : Number(values.port),
|
|
175
190
|
mode: resolveFixedMode(values)
|
|
176
191
|
});
|
|
177
192
|
|
|
178
193
|
const captureCommand = (command, args) =>
|
|
179
194
|
new Promise((resolve) => {
|
|
180
|
-
const child =
|
|
181
|
-
shell: isWindows,
|
|
182
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
183
|
-
timeout: DETECT_TIMEOUT_MS
|
|
184
|
-
});
|
|
195
|
+
const child = spawnTool(command, args, { stdio: ['ignore', 'pipe', 'ignore'], timeout: DETECT_TIMEOUT_MS });
|
|
185
196
|
let output = '';
|
|
186
197
|
child.stdout.on('data', (chunk) => {
|
|
187
198
|
output += chunk.toString();
|
|
188
199
|
});
|
|
189
200
|
child.on('error', () => resolve(null));
|
|
190
|
-
child.on('close', (code) => resolve(code === 0 ?
|
|
201
|
+
child.on('close', (code) => resolve(code === 0 ? output.trim() : null));
|
|
191
202
|
});
|
|
192
203
|
|
|
204
|
+
const captureFirstLine = async (command, args) => (await captureCommand(command, args))?.split('\n')[0].trim() ?? null;
|
|
205
|
+
|
|
193
206
|
const isPortFree = (port) =>
|
|
194
207
|
new Promise((resolve) => {
|
|
195
208
|
const server = net.createServer();
|
|
@@ -205,20 +218,6 @@ const findFreePort = async (start) => {
|
|
|
205
218
|
throw new Error(`No free port found from ${start}`);
|
|
206
219
|
};
|
|
207
220
|
|
|
208
|
-
const isTcpReachable = (host, port) =>
|
|
209
|
-
new Promise((resolve) => {
|
|
210
|
-
const socket = net.connect({ host, port, timeout: DATABASE_TIMEOUT_MS });
|
|
211
|
-
socket.once('connect', () => {
|
|
212
|
-
socket.destroy();
|
|
213
|
-
resolve(true);
|
|
214
|
-
});
|
|
215
|
-
socket.once('timeout', () => {
|
|
216
|
-
socket.destroy();
|
|
217
|
-
resolve(false);
|
|
218
|
-
});
|
|
219
|
-
socket.once('error', () => resolve(false));
|
|
220
|
-
});
|
|
221
|
-
|
|
222
221
|
const fetchLatestVersion = async () => {
|
|
223
222
|
try {
|
|
224
223
|
const response = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS) });
|
|
@@ -241,27 +240,82 @@ const isNodeSupported = () => {
|
|
|
241
240
|
return major > MIN_NODE_VERSION.major || (major === MIN_NODE_VERSION.major && minor >= MIN_NODE_VERSION.minor);
|
|
242
241
|
};
|
|
243
242
|
|
|
243
|
+
const classifyChassis = (code) => {
|
|
244
|
+
if (CHASSIS_LAPTOP.has(code)) return DEVICE_TYPES.laptop;
|
|
245
|
+
if (CHASSIS_PC.has(code)) return DEVICE_TYPES.pc;
|
|
246
|
+
return DEVICE_TYPES.other;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const isAndroid = () =>
|
|
250
|
+
process.platform === 'android' || Boolean(process.env.TERMUX_VERSION) || (process.env.PREFIX ?? '').includes('com.termux');
|
|
251
|
+
|
|
252
|
+
const readLinuxChassis = async () => {
|
|
253
|
+
try {
|
|
254
|
+
return Number((await fse.readFile(LINUX_CHASSIS_FILE, 'utf8')).trim());
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const hasLinuxBattery = async () => {
|
|
261
|
+
try {
|
|
262
|
+
return (await fse.readdir(LINUX_POWER_SUPPLY_DIRECTORY)).some((entry) => /^BAT/i.test(entry));
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const detectDeviceType = async () => {
|
|
269
|
+
if (isAndroid()) return DEVICE_TYPES.phone;
|
|
270
|
+
|
|
271
|
+
if (process.platform === 'win32') {
|
|
272
|
+
const output = await captureCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', WINDOWS_CHASSIS_SCRIPT]);
|
|
273
|
+
return classifyChassis(Number((output ?? '').split(/\s+/).find((token) => /^\d+$/.test(token))));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (process.platform === 'darwin') {
|
|
277
|
+
const output = await captureCommand('pmset', ['-g', 'batt']);
|
|
278
|
+
return output?.includes('InternalBattery') ? DEVICE_TYPES.laptop : DEVICE_TYPES.pc;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (process.platform === 'linux') {
|
|
282
|
+
const code = await readLinuxChassis();
|
|
283
|
+
if (code !== null) return classifyChassis(code);
|
|
284
|
+
return (await hasLinuxBattery()) ? DEVICE_TYPES.laptop : DEVICE_TYPES.other;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return DEVICE_TYPES.other;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const detectIpAddress = () => {
|
|
291
|
+
const [entry] = Object.values(os.networkInterfaces())
|
|
292
|
+
.flat()
|
|
293
|
+
.filter((item) => item && item.family === 'IPv4' && !item.internal);
|
|
294
|
+
return entry?.address ?? null;
|
|
295
|
+
};
|
|
296
|
+
|
|
244
297
|
const detectEnvironment = async ({ port, checkUpdate }) => {
|
|
245
298
|
const managerNames = Object.keys(PACKAGE_MANAGERS);
|
|
246
|
-
const [
|
|
247
|
-
|
|
299
|
+
const [gitVersion, portFree, latestVersion, device, ...managerVersions] = await Promise.all([
|
|
300
|
+
captureFirstLine('git', ['--version']),
|
|
248
301
|
isPortFree(port),
|
|
249
302
|
checkUpdate ? fetchLatestVersion() : Promise.resolve(null),
|
|
250
|
-
|
|
303
|
+
detectDeviceType(),
|
|
304
|
+
...managerNames.map((name) => captureFirstLine(name, ['--version']))
|
|
251
305
|
]);
|
|
252
306
|
const managers = Object.fromEntries(
|
|
253
307
|
managerNames.map((name, index) => [name, managerVersions[index]]).filter(([, version]) => version)
|
|
254
308
|
);
|
|
255
|
-
const cpus = os.cpus();
|
|
256
309
|
|
|
257
310
|
return {
|
|
258
311
|
system: `${PLATFORM_LABELS[process.platform] ?? process.platform} ${os.release()} (${os.arch()})`,
|
|
259
|
-
device
|
|
312
|
+
device,
|
|
313
|
+
ipAddress: detectIpAddress(),
|
|
260
314
|
node: process.versions.node,
|
|
261
315
|
nodeSupported: isNodeSupported(),
|
|
262
316
|
launcher: parseLauncher(),
|
|
263
317
|
managers,
|
|
264
|
-
git:
|
|
318
|
+
git: gitVersion ? gitVersion.replace(/^git version\s*/, '') : null,
|
|
265
319
|
port,
|
|
266
320
|
portFree,
|
|
267
321
|
latestVersion
|
|
@@ -269,36 +323,25 @@ const detectEnvironment = async ({ port, checkUpdate }) => {
|
|
|
269
323
|
};
|
|
270
324
|
|
|
271
325
|
const printEnvironmentCard = (environment) => {
|
|
272
|
-
const managerList = Object.entries(environment.managers).map(([name, version]) => `${name} ${version}`);
|
|
273
|
-
const nodeStatus = environment.nodeSupported
|
|
274
|
-
? chalk.green('✔')
|
|
275
|
-
: chalk.red(`✖ requires >=${MIN_NODE_VERSION.major}.${MIN_NODE_VERSION.minor}`);
|
|
276
326
|
const hasUpdate = environment.latestVersion && compareVersions(environment.latestVersion, CLI_VERSION) > 0;
|
|
327
|
+
const nodeValue = environment.nodeSupported
|
|
328
|
+
? `v${environment.node}`
|
|
329
|
+
: chalk.red(`v${environment.node} (requires >=${MIN_NODE_VERSION.major}.${MIN_NODE_VERSION.minor})`);
|
|
330
|
+
const portValue = environment.portFree
|
|
331
|
+
? `${environment.port} available`
|
|
332
|
+
: chalk.yellow(`${environment.port} in use`);
|
|
277
333
|
const rows = [
|
|
278
334
|
{ label: muted('System'), value: environment.system },
|
|
279
335
|
{ label: muted('Device'), value: environment.device },
|
|
280
|
-
{ label: muted('
|
|
281
|
-
{
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
label: muted('Package managers'),
|
|
287
|
-
value: managerList.length ? managerList.join(', ') : chalk.yellow('none detected')
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
label: muted('Git'),
|
|
291
|
-
value: environment.git ? `${environment.git} ${chalk.green('✔')}` : muted('not found')
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
label: muted('Port'),
|
|
295
|
-
value: `${environment.port} ${environment.portFree ? `${chalk.green('✔')} free` : chalk.yellow('in use')}`
|
|
296
|
-
},
|
|
336
|
+
{ label: muted('IP address'), value: environment.ipAddress ?? muted('unavailable') },
|
|
337
|
+
{ label: muted('Node.js'), value: nodeValue },
|
|
338
|
+
{ label: muted('Git'), value: environment.git ?? muted('not found') },
|
|
339
|
+
{ label: muted('Port'), value: portValue },
|
|
297
340
|
{
|
|
298
341
|
label: muted('Openpack'),
|
|
299
342
|
value: hasUpdate
|
|
300
|
-
? `v${CLI_VERSION} ${chalk.yellow(`update available: ${environment.latestVersion}`)}`
|
|
301
|
-
: `v${CLI_VERSION}
|
|
343
|
+
? `v${CLI_VERSION} ${chalk.yellow(`(update available: ${environment.latestVersion})`)}`
|
|
344
|
+
: `v${CLI_VERSION}`
|
|
302
345
|
}
|
|
303
346
|
];
|
|
304
347
|
console.log(renderCard('Environment', rows));
|
|
@@ -308,7 +351,7 @@ const printEnvironmentCard = (environment) => {
|
|
|
308
351
|
const printStatusCard = (results) => {
|
|
309
352
|
const rows = results.map((result) => ({
|
|
310
353
|
label: `${STATE_ICONS[result.state]} ${result.label}`,
|
|
311
|
-
value: result.
|
|
354
|
+
value: STATUS_VALUES[result.state](result)
|
|
312
355
|
}));
|
|
313
356
|
console.log();
|
|
314
357
|
console.log(renderCard('Build status', rows));
|
|
@@ -339,13 +382,13 @@ const printHelp = () => {
|
|
|
339
382
|
});
|
|
340
383
|
console.log();
|
|
341
384
|
console.log(` ${muted('Example')}`);
|
|
342
|
-
console.log(` ${PACKAGE_COMMAND} ${accent('my-project --yes --pm pnpm --
|
|
385
|
+
console.log(` ${PACKAGE_COMMAND} ${accent('my-project --yes --pm pnpm --no-run')}`);
|
|
343
386
|
console.log();
|
|
344
387
|
};
|
|
345
388
|
|
|
346
389
|
const printFailure = (title, detail) => {
|
|
347
390
|
console.log();
|
|
348
|
-
console.log(` ${chalk.red('
|
|
391
|
+
console.log(` ${chalk.red('●')} ${chalk.bold(title)}`);
|
|
349
392
|
if (detail) console.log(` ${muted(detail)}`);
|
|
350
393
|
console.log();
|
|
351
394
|
process.exitCode = 1;
|
|
@@ -353,7 +396,7 @@ const printFailure = (title, detail) => {
|
|
|
353
396
|
|
|
354
397
|
const runCommand = (command, args, cwd) =>
|
|
355
398
|
new Promise((resolve, reject) => {
|
|
356
|
-
const child =
|
|
399
|
+
const child = spawnTool(command, args, { cwd, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
357
400
|
const errors = [];
|
|
358
401
|
child.stderr.on('data', (chunk) => errors.push(chunk.toString()));
|
|
359
402
|
child.on('error', reject);
|
|
@@ -366,7 +409,7 @@ const runCommand = (command, args, cwd) =>
|
|
|
366
409
|
|
|
367
410
|
const runInteractive = (command, args, cwd) =>
|
|
368
411
|
new Promise((resolve, reject) => {
|
|
369
|
-
const child =
|
|
412
|
+
const child = spawnTool(command, args, { cwd, stdio: 'inherit' });
|
|
370
413
|
child.on('error', reject);
|
|
371
414
|
child.on('close', () => resolve());
|
|
372
415
|
});
|
|
@@ -421,26 +464,19 @@ const askOptions = async (cli, environment) => {
|
|
|
421
464
|
placeholder: 'my-project',
|
|
422
465
|
validate: validateProjectName
|
|
423
466
|
}),
|
|
424
|
-
packageManager: () =>
|
|
425
|
-
fixed.packageManager !== undefined
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
fixed.databaseUrl !== undefined
|
|
438
|
-
? Promise.resolve(fixed.databaseUrl)
|
|
439
|
-
: prompts.text({
|
|
440
|
-
message: 'PostgreSQL DATABASE_URL',
|
|
441
|
-
placeholder: 'postgresql://user:password@localhost:5432/mydb',
|
|
442
|
-
validate: validateDatabaseUrl
|
|
443
|
-
}),
|
|
467
|
+
packageManager: () => {
|
|
468
|
+
if (fixed.packageManager !== undefined) return Promise.resolve(fixed.packageManager);
|
|
469
|
+
if (managers.names.length === 1) return Promise.resolve(managers.names[0]);
|
|
470
|
+
return prompts.select({
|
|
471
|
+
message: 'Choose a package manager',
|
|
472
|
+
initialValue: managers.initial,
|
|
473
|
+
options: managers.names.map((value) => ({
|
|
474
|
+
value,
|
|
475
|
+
label: PACKAGE_MANAGERS[value].label,
|
|
476
|
+
hint: environment.managers[value] ? `v${environment.managers[value]}` : undefined
|
|
477
|
+
}))
|
|
478
|
+
});
|
|
479
|
+
},
|
|
444
480
|
mode: () =>
|
|
445
481
|
fixed.mode !== undefined
|
|
446
482
|
? Promise.resolve(fixed.mode)
|
|
@@ -477,7 +513,6 @@ const askOptions = async (cli, environment) => {
|
|
|
477
513
|
const resolveDefaultOptions = async (cli, environment) => {
|
|
478
514
|
const fixed = readFixedOptions(cli);
|
|
479
515
|
if (fixed.name === undefined) throw new UsageError('Project name is required in non-interactive mode');
|
|
480
|
-
if (fixed.databaseUrl === undefined) throw new UsageError('--database-url is required in non-interactive mode');
|
|
481
516
|
|
|
482
517
|
const mode = fixed.mode ?? (process.stdout.isTTY ? MODES.run : MODES.install);
|
|
483
518
|
const desired = fixed.port ?? DEFAULT_PORT;
|
|
@@ -491,23 +526,12 @@ const resolveDefaultOptions = async (cli, environment) => {
|
|
|
491
526
|
return {
|
|
492
527
|
name: fixed.name,
|
|
493
528
|
packageManager: fixed.packageManager ?? resolveManagerChoices(environment).initial,
|
|
494
|
-
databaseUrl: fixed.databaseUrl,
|
|
495
529
|
mode,
|
|
496
530
|
port
|
|
497
531
|
};
|
|
498
532
|
};
|
|
499
533
|
|
|
500
|
-
const
|
|
501
|
-
const url = new URL(databaseUrl);
|
|
502
|
-
const host = url.hostname.replace(/^\[|\]$/g, '');
|
|
503
|
-
const port = Number(url.port) || DEFAULT_DATABASE_PORT;
|
|
504
|
-
const target = `${host}:${port}`;
|
|
505
|
-
return (await isTcpReachable(host, port))
|
|
506
|
-
? { label: `Database reachable at ${target}` }
|
|
507
|
-
: { state: 'warning', label: `Database unreachable at ${target}` };
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
const buildSteps = ({ name, packageManager, databaseUrl, mode, port }, { dbCheck }) => {
|
|
534
|
+
const buildSteps = ({ name, packageManager, mode, port }) => {
|
|
511
535
|
const targetDirectory = path.resolve(process.cwd(), name);
|
|
512
536
|
const manager = PACKAGE_MANAGERS[packageManager];
|
|
513
537
|
const customPort = port !== DEFAULT_PORT;
|
|
@@ -515,7 +539,10 @@ const buildSteps = ({ name, packageManager, databaseUrl, mode, port }, { dbCheck
|
|
|
515
539
|
{
|
|
516
540
|
active: 'Fetching template',
|
|
517
541
|
done: 'Template fetched',
|
|
518
|
-
task: () =>
|
|
542
|
+
task: async () => {
|
|
543
|
+
const { downloadTemplate } = await import('giget');
|
|
544
|
+
await downloadTemplate(TEMPLATE_SOURCE, { dir: targetDirectory });
|
|
545
|
+
}
|
|
519
546
|
},
|
|
520
547
|
{
|
|
521
548
|
active: 'Preparing environment',
|
|
@@ -528,31 +555,21 @@ const buildSteps = ({ name, packageManager, databaseUrl, mode, port }, { dbCheck
|
|
|
528
555
|
},
|
|
529
556
|
{
|
|
530
557
|
active: 'Injecting template configuration',
|
|
531
|
-
done: customPort ? '
|
|
558
|
+
done: customPort ? 'SESSION_SECRET and PORT injected' : 'SESSION_SECRET injected',
|
|
532
559
|
task: async () => {
|
|
533
560
|
const envPath = path.join(targetDirectory, ENV_FILE);
|
|
534
561
|
const current = await fse.readFile(envPath, 'utf8');
|
|
535
|
-
const
|
|
536
|
-
const withSecret = setEnvValue(withDatabase, 'SESSION_SECRET', randomBytes(32).toString('hex'));
|
|
562
|
+
const withSecret = setEnvValue(current, 'SESSION_SECRET', randomBytes(32).toString('hex'));
|
|
537
563
|
await fse.writeFile(envPath, customPort ? setEnvValue(withSecret, 'PORT', String(port)) : withSecret);
|
|
538
564
|
}
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
active: 'Entering project directory',
|
|
568
|
+
done: `Entered ${name}`,
|
|
569
|
+
task: async () => process.chdir(targetDirectory)
|
|
539
570
|
}
|
|
540
571
|
];
|
|
541
572
|
|
|
542
|
-
if (dbCheck) {
|
|
543
|
-
steps.push({
|
|
544
|
-
active: 'Checking database connection',
|
|
545
|
-
done: 'Database reachable',
|
|
546
|
-
task: () => checkDatabase(databaseUrl)
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
steps.push({
|
|
551
|
-
active: 'Entering project directory',
|
|
552
|
-
done: `Entered ${name}`,
|
|
553
|
-
task: async () => process.chdir(targetDirectory)
|
|
554
|
-
});
|
|
555
|
-
|
|
556
573
|
if (mode !== MODES.none) {
|
|
557
574
|
steps.push({
|
|
558
575
|
active: `Installing dependencies with ${manager.label}`,
|
|
@@ -589,7 +606,7 @@ const runSteps = async (steps, results) => {
|
|
|
589
606
|
spinner.stopAndPersist({ symbol: STATE_ICONS[state], text: `${prefix} ${label}` });
|
|
590
607
|
} catch (error) {
|
|
591
608
|
results[index] = { label: step.active, state: 'failed', duration: Date.now() - startedAt };
|
|
592
|
-
spinner.stopAndPersist({ symbol: STATE_ICONS.failed, text: `${prefix} ${step.active}` });
|
|
609
|
+
spinner.stopAndPersist({ symbol: STATE_ICONS.failed, text: `${prefix} ${step.active} ${chalk.red('failed')}` });
|
|
593
610
|
throw error;
|
|
594
611
|
}
|
|
595
612
|
}
|
|
@@ -604,7 +621,7 @@ const printSummary = ({ name, packageManager, mode, port, startedAt }) => {
|
|
|
604
621
|
if (mode !== MODES.run) pending.push(devCommand);
|
|
605
622
|
|
|
606
623
|
console.log();
|
|
607
|
-
console.log(` ${chalk.green('
|
|
624
|
+
console.log(` ${chalk.green('●')} ${chalk.bold(name)} is ready ${muted(`in ${duration}s`)}`);
|
|
608
625
|
console.log();
|
|
609
626
|
if (mode === MODES.run) {
|
|
610
627
|
console.log(` ${muted('Server')} ${accent(devUrl)}`);
|
|
@@ -663,7 +680,7 @@ async function main() {
|
|
|
663
680
|
|
|
664
681
|
const options = interactive ? await askOptions(cli, environment) : await resolveDefaultOptions(cli, environment);
|
|
665
682
|
const originalCwd = process.cwd();
|
|
666
|
-
const { steps, targetDirectory, manager } = buildSteps(options
|
|
683
|
+
const { steps, targetDirectory, manager } = buildSteps(options);
|
|
667
684
|
const rollback = createRollback(targetDirectory, originalCwd, !(await fse.pathExists(targetDirectory)));
|
|
668
685
|
const results = steps.map((step) => ({ label: step.done, state: 'pending', duration: null }));
|
|
669
686
|
const startedAt = performance.now();
|