create-kosh-erp 0.1.3 → 0.2.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/index.js +143 -33
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* 4. installs what the project needs next (the back-office's build tools),
|
|
10
10
|
* 5. prints how to start it.
|
|
11
11
|
*
|
|
12
|
-
* Plain JavaScript without dependencies: it runs before anything is installed.
|
|
12
|
+
* Plain JavaScript without dependencies: it runs before anything is installed. On a terminal it shows a logo, step
|
|
13
|
+
* icons and spinners (the package manager's output is shown only when it fails); in logs and CI, plain lines.
|
|
13
14
|
*/
|
|
14
|
-
import {
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
15
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
17
|
import { basename, join, resolve } from 'node:path';
|
|
17
18
|
import { createInterface } from 'node:readline/promises';
|
|
@@ -41,27 +42,130 @@ Examples:
|
|
|
41
42
|
process.exit(0);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// The look: colour, icons and spinners on a terminal; plain lines in logs and CI (the same rules as kosh init).
|
|
46
|
+
const env = process.env;
|
|
47
|
+
const tty = process.stdout.isTTY === true && env.TERM !== 'dumb' && env.CI === undefined;
|
|
48
|
+
const color = env.NO_COLOR === undefined && ((env.FORCE_COLOR ?? '0') !== '0' || tty);
|
|
49
|
+
const unicode =
|
|
50
|
+
env.KOSH_ASCII !== '1' &&
|
|
51
|
+
(process.platform === 'win32'
|
|
52
|
+
? Boolean(
|
|
53
|
+
env.WT_SESSION ||
|
|
54
|
+
env.TERM_PROGRAM === 'vscode' ||
|
|
55
|
+
env.TERMINAL_EMULATOR === 'JetBrains-JediTerm' ||
|
|
56
|
+
env.TERM === 'xterm-256color',
|
|
57
|
+
)
|
|
58
|
+
: env.TERM !== 'linux');
|
|
59
|
+
const emoji = unicode && tty;
|
|
60
|
+
const ESC = '\u001b[';
|
|
61
|
+
const paint = (open, close) => (text) => (color ? `${ESC}${open}m${text}${ESC}${close}m` : text);
|
|
62
|
+
const c = {
|
|
63
|
+
bold: paint('1', '22'),
|
|
64
|
+
gray: paint('90', '39'),
|
|
65
|
+
cyan: paint('36', '39'),
|
|
66
|
+
green: paint('32', '39'),
|
|
67
|
+
red: paint('31', '39'),
|
|
68
|
+
yellow: paint('33', '39'),
|
|
69
|
+
};
|
|
70
|
+
const mark = unicode ? { ok: '✔', fail: '✖', warn: '▲' } : { ok: '[ok]', fail: '[x]', warn: '[!]' };
|
|
71
|
+
const out = (text) => process.stdout.write(text);
|
|
72
|
+
const icon = (pictograph) => (emoji ? pictograph : c.cyan(unicode ? '◆' : '*'));
|
|
73
|
+
const step = (pictograph, title, hint) => {
|
|
74
|
+
out(`\n${icon(pictograph)} ${c.bold(title)}\n`);
|
|
75
|
+
if (hint) out(` ${c.gray(hint)}\n`);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function banner() {
|
|
79
|
+
if (!unicode) {
|
|
80
|
+
out(`\n ${c.bold('K O S H')} npm create kosh-erp ${own.version}\n`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const logo = [
|
|
84
|
+
'██╗ ██╗ ██████╗ ███████╗██╗ ██╗',
|
|
85
|
+
'██║ ██╔╝██╔═══██╗██╔════╝██║ ██║',
|
|
86
|
+
'█████╔╝ ██║ ██║███████╗███████║',
|
|
87
|
+
'██╔═██╗ ██║ ██║╚════██║██╔══██║',
|
|
88
|
+
'██║ ██╗╚██████╔╝███████║██║ ██║',
|
|
89
|
+
'╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝',
|
|
90
|
+
];
|
|
91
|
+
const gradient = [51, 45, 39, 33, 63, 99]; // cyan to violet
|
|
92
|
+
out('\n');
|
|
93
|
+
for (const [i, row] of logo.entries())
|
|
94
|
+
out(` ${color ? `${ESC}38;5;${gradient[i]}m${row}${ESC}39m` : row}\n`);
|
|
95
|
+
out(` ${c.bold('The ERP framework on Shutro')} ${c.gray(`· create-kosh-erp ${own.version}`)}\n`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Runs `work` (resolving to an exit code) under a spinner, then prints ✔ with the time taken, or ✖. */
|
|
99
|
+
async function task(label, work) {
|
|
100
|
+
const frames = unicode
|
|
101
|
+
? ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
102
|
+
: ['|', '/', '-', '\\'];
|
|
103
|
+
const started = Date.now();
|
|
104
|
+
let frame = 0;
|
|
105
|
+
const draw = () => {
|
|
106
|
+
const seconds = Math.floor((Date.now() - started) / 1000);
|
|
107
|
+
out(`\r${ESC}2K ${c.cyan(frames[frame++ % frames.length])} ${label} ${c.gray(`${seconds}s`)}`);
|
|
108
|
+
};
|
|
109
|
+
const timer = tty ? setInterval(draw, 80) : null;
|
|
110
|
+
if (tty) draw();
|
|
111
|
+
else out(` … ${label}\n`);
|
|
112
|
+
const status = await work();
|
|
113
|
+
if (timer !== null) {
|
|
114
|
+
clearInterval(timer);
|
|
115
|
+
out(`\r${ESC}2K`);
|
|
116
|
+
}
|
|
117
|
+
const took = c.gray(`(${((Date.now() - started) / 1000).toFixed(1)}s)`);
|
|
118
|
+
out(
|
|
119
|
+
status === 0 ? ` ${c.green(mark.ok)} ${label} ${took}\n` : ` ${c.red(mark.fail)} ${label}\n`,
|
|
120
|
+
);
|
|
121
|
+
return status;
|
|
122
|
+
}
|
|
123
|
+
|
|
44
124
|
/** The package manager that ran us (npm create, pnpm create, yarn create). */
|
|
45
125
|
const agent = process.env.npm_config_user_agent ?? '';
|
|
46
126
|
const pm = agent.startsWith('pnpm') ? 'pnpm' : agent.startsWith('yarn') ? 'yarn' : 'npm';
|
|
127
|
+
/** Quotes an argument for the Windows shell (the package manager is a .cmd script there and needs one). */
|
|
128
|
+
const quote = (a) => (/[\s"&|<>^]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a);
|
|
47
129
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
130
|
+
* Starts a command. Node itself runs without a shell (its path may contain spaces: C:\Program Files\nodejs); the
|
|
131
|
+
* package manager on Windows gets one command line, each part quoted when it needs it.
|
|
50
132
|
*/
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
133
|
+
const start = (command, args, options) =>
|
|
134
|
+
command === process.execPath || process.platform !== 'win32'
|
|
135
|
+
? spawn(command, args, options)
|
|
136
|
+
: spawn([command, ...args].map(quote).join(' '), { ...options, shell: true });
|
|
137
|
+
|
|
138
|
+
/** Runs a command in this terminal (kosh init asks its questions here). */
|
|
139
|
+
const run = (command, args, cwd, extra = {}) =>
|
|
140
|
+
new Promise((done) => {
|
|
141
|
+
const child = start(command, args, { cwd, stdio: 'inherit', env: { ...env, ...extra } });
|
|
142
|
+
child.on('error', () => done(1));
|
|
143
|
+
child.on('close', (status) => done(status ?? 1));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/** Runs the package manager quietly (the spinner turns meanwhile); its output is printed only when it fails. */
|
|
147
|
+
const quiet = (args, cwd) =>
|
|
148
|
+
new Promise((done) => {
|
|
149
|
+
const child = start(pm, args, { cwd, env });
|
|
150
|
+
let output = '';
|
|
151
|
+
child.stdout.on('data', (chunk) => {
|
|
152
|
+
output += chunk;
|
|
153
|
+
});
|
|
154
|
+
child.stderr.on('data', (chunk) => {
|
|
155
|
+
output += chunk;
|
|
156
|
+
});
|
|
157
|
+
child.on('error', (error) => {
|
|
158
|
+
output += error.message;
|
|
159
|
+
});
|
|
160
|
+
child.on('close', (status) => {
|
|
161
|
+
if (status !== 0) process.stderr.write(`\n${output.trim()}\n`);
|
|
162
|
+
done(status ?? 1);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
61
165
|
const add = (cwd, what) =>
|
|
62
|
-
|
|
166
|
+
quiet(pm === 'npm' ? ['install', '--no-audit', '--no-fund', ...what] : ['add', ...what], cwd);
|
|
63
167
|
const installAll = (cwd) =>
|
|
64
|
-
|
|
168
|
+
quiet(pm === 'npm' ? ['install', '--no-audit', '--no-fund'] : ['install'], cwd);
|
|
65
169
|
|
|
66
170
|
/** A project name: a folder name and a valid package name (lower-case letters, digits, - and _). */
|
|
67
171
|
const validName = (name) => /^[a-z0-9][a-z0-9._-]{0,213}$/.test(name);
|
|
@@ -79,22 +183,27 @@ async function folder() {
|
|
|
79
183
|
}
|
|
80
184
|
if (!process.stdin.isTTY) return 'my-erp';
|
|
81
185
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
186
|
+
out('\n');
|
|
82
187
|
for (;;) {
|
|
83
|
-
const answer =
|
|
188
|
+
const answer =
|
|
189
|
+
(await rl.question(`${c.cyan('?')} Project name ${c.gray('[my-erp]')}: `)).trim() || 'my-erp';
|
|
84
190
|
if (validName(answer)) {
|
|
85
191
|
rl.close();
|
|
86
192
|
return answer;
|
|
87
193
|
}
|
|
88
|
-
|
|
194
|
+
out(
|
|
195
|
+
` ${c.yellow(mark.warn)} Lower-case letters, digits, - and _ (e.g. my-erp, padma-traders).\n`,
|
|
196
|
+
);
|
|
89
197
|
}
|
|
90
198
|
}
|
|
91
199
|
|
|
200
|
+
banner();
|
|
92
201
|
const target = resolve(await folder());
|
|
93
202
|
const name =
|
|
94
203
|
basename(target)
|
|
95
204
|
.toLowerCase()
|
|
96
205
|
.replace(/[^a-z0-9-]+/g, '-') || 'kosh-app';
|
|
97
|
-
|
|
206
|
+
step('📦', 'Kosh packages', target);
|
|
98
207
|
mkdirSync(target, { recursive: true });
|
|
99
208
|
if (!existsSync(join(target, 'package.json')))
|
|
100
209
|
writeFileSync(
|
|
@@ -102,8 +211,7 @@ if (!existsSync(join(target, 'package.json')))
|
|
|
102
211
|
`${JSON.stringify({ name, version: '0.1.0', private: true, type: 'module' }, null, 2)}\n`,
|
|
103
212
|
);
|
|
104
213
|
|
|
105
|
-
|
|
106
|
-
if (add(target, [spec]) !== 0) {
|
|
214
|
+
if ((await task(`Installing ${spec} (Shutro comes with it)`, () => add(target, [spec]))) !== 0) {
|
|
107
215
|
process.stderr.write(`\nInstalling ${spec} failed (see above).\n`);
|
|
108
216
|
process.exit(1);
|
|
109
217
|
}
|
|
@@ -113,9 +221,10 @@ if (!existsSync(cli)) {
|
|
|
113
221
|
process.stderr.write(`\n${cli} is missing: the kosh-erp installation is incomplete.\n`);
|
|
114
222
|
process.exit(1);
|
|
115
223
|
}
|
|
116
|
-
process.stdout.write('\n');
|
|
117
224
|
// kosh init asks its questions in this terminal (stdio inherited), then sets up the database.
|
|
118
|
-
const init = run(process.execPath, [cli, 'init', '.', ...forInit], target, {
|
|
225
|
+
const init = await run(process.execPath, [cli, 'init', '.', ...forInit], target, {
|
|
226
|
+
KOSH_CREATE: '1',
|
|
227
|
+
});
|
|
119
228
|
if (init !== 0) {
|
|
120
229
|
process.stderr.write(
|
|
121
230
|
`\nThe setup stopped. Fix the problem above, then finish it with:\n cd ${positional ?? basename(target)}\n npx kosh init\n`,
|
|
@@ -123,21 +232,22 @@ if (init !== 0) {
|
|
|
123
232
|
process.exit(init);
|
|
124
233
|
}
|
|
125
234
|
|
|
126
|
-
|
|
127
|
-
if (installAll(target) !== 0) {
|
|
235
|
+
step('🎨', 'Back-office', 'React, Vite and Tailwind for the web app');
|
|
236
|
+
if ((await task(`Installing the back-office tools with ${pm}`, () => installAll(target))) !== 0) {
|
|
128
237
|
process.stderr.write(`\n${pm} install failed (see above); run it again in ${target}.\n`);
|
|
129
238
|
process.exit(1);
|
|
130
239
|
}
|
|
131
240
|
|
|
132
|
-
const cd = target === resolve('.') ? '' : ` cd ${positional ?? basename(target)}\n`;
|
|
133
|
-
const
|
|
241
|
+
const cd = target === resolve('.') ? '' : ` ${c.cyan(`cd ${positional ?? basename(target)}`)}\n`;
|
|
242
|
+
const api = pm === 'npm' ? 'npm start' : `${pm} start`;
|
|
134
243
|
const web = pm === 'npm' ? 'npm run web' : `${pm} web`;
|
|
135
|
-
|
|
136
|
-
|
|
244
|
+
const arrow = unicode ? '→' : '->';
|
|
245
|
+
out(`
|
|
246
|
+
${icon('🚀')} ${c.bold(c.green('Kosh is installed.'))} ${c.gray('Start it:')}
|
|
137
247
|
|
|
138
|
-
${cd} ${
|
|
139
|
-
${web}
|
|
248
|
+
${cd} ${c.cyan(api.padEnd(14))} ${c.gray('# the API (reads .env)')}
|
|
249
|
+
${c.cyan(web.padEnd(14))} ${c.gray(`# the back-office, in a second terminal ${arrow} http://localhost:5173`)}
|
|
140
250
|
|
|
141
|
-
Everything in Docker instead:
|
|
142
|
-
Guide: https://www.npmjs.com/package/kosh-erp
|
|
251
|
+
${c.gray('Everything in Docker instead:')} ${c.cyan('docker compose up -d --build')} ${c.gray(`${arrow} http://localhost:8080`)}
|
|
252
|
+
${c.gray('Guide:')} https://www.npmjs.com/package/kosh-erp
|
|
143
253
|
`);
|