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.
Files changed (2) hide show
  1. package/index.js +143 -33
  2. 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 { spawnSync } from 'node:child_process';
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
- * Runs a command in this terminal. The package manager is a .cmd script on Windows and needs a shell there, so its
49
- * arguments are quoted; Node itself runs without one (its path may contain spaces: C:\Program Files\nodejs).
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 run = (command, args, cwd, env = {}) => {
52
- const options = { cwd, stdio: 'inherit', env: { ...process.env, ...env } };
53
- if (command === process.execPath || process.platform !== 'win32')
54
- return spawnSync(command, args, options).status ?? 1;
55
- // One command line, each part quoted when it needs it (the shell gets no separate argument list).
56
- const quote = (a) => (/[\s"&|<>^]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a);
57
- return (
58
- spawnSync([command, ...args].map(quote).join(' '), { ...options, shell: true }).status ?? 1
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
- run(pm, pm === 'npm' ? ['install', '--no-audit', '--no-fund', ...what] : ['add', ...what], cwd);
166
+ quiet(pm === 'npm' ? ['install', '--no-audit', '--no-fund', ...what] : ['add', ...what], cwd);
63
167
  const installAll = (cwd) =>
64
- run(pm, pm === 'npm' ? ['install', '--no-audit', '--no-fund'] : ['install'], cwd);
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 = (await rl.question('Project name [my-erp]: ')).trim() || 'my-erp';
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
- process.stdout.write(' Lower-case letters, digits, - and _ (e.g. my-erp, padma-traders).\n');
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
- process.stdout.write(`\nCreating a Kosh installation in ${target}\n\n`);
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
- process.stdout.write(`Installing ${spec} …\n`);
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, { KOSH_CREATE: '1' });
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
- process.stdout.write('\nInstalling the back-office tools …\n');
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 start = pm === 'npm' ? 'npm start' : `${pm} start`;
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
- process.stdout.write(`
136
- Kosh is installed.
244
+ const arrow = unicode ? '→' : '->';
245
+ out(`
246
+ ${icon('🚀')} ${c.bold(c.green('Kosh is installed.'))} ${c.gray('Start it:')}
137
247
 
138
- ${cd} ${start} # the API (reads .env)
139
- ${web} # the back-office, in a second terminal → http://localhost:5173
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: docker compose up -d --build → http://localhost:8080
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
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kosh-erp",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "npm create kosh-erp: install Kosh and set it up in one command (database, company, administrator, apps, port)",
5
5
  "keywords": [
6
6
  "kosh",