create-kosh-erp 0.1.2 → 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 (3) hide show
  1. package/README.md +5 -3
  2. package/index.js +162 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,12 +3,14 @@
3
3
  Install [Kosh](https://www.npmjs.com/package/kosh-erp) and set it up in one command:
4
4
 
5
5
  ```sh
6
- npm create kosh-erp@latest my-erp
6
+ npm create kosh-erp@latest my-erp # the project my-erp
7
+ npm create kosh-erp@latest # asks for the project name first
7
8
  ```
8
9
 
9
- It creates the folder, installs `kosh-erp` (Shutro comes with it), then asks for:
10
+ It creates the project folder, installs `kosh-erp` (Shutro comes with it), then asks for:
10
11
 
11
- - the PostgreSQL database (your server, or one started in Docker for you; a missing database is created)
12
+ - the PostgreSQL database: your server (username, password, port, host, database name; a missing database is
13
+ created), or one started in Docker for you
12
14
  - the company (name, currency, language, time zone)
13
15
  - the administrator (login, name, email, password) and its authenticator app
14
16
  - the apps (roles and approvals, reports and documents, chatter and attachments)
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,43 +42,168 @@ 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);
169
+
170
+ /** A project name: a folder name and a valid package name (lower-case letters, digits, - and _). */
171
+ const validName = (name) => /^[a-z0-9][a-z0-9._-]{0,213}$/.test(name);
65
172
 
173
+ /** The project name: `npm create kosh-erp my-erp`, else asked (Enter: my-erp). */
66
174
  async function folder() {
67
- if (positional !== undefined) return positional;
175
+ if (positional !== undefined) {
176
+ if (!validName(basename(resolve(positional)))) {
177
+ process.stderr.write(
178
+ `"${positional}" is not a project name: lower-case letters, digits, - and _ (e.g. my-erp).\n`,
179
+ );
180
+ process.exit(2);
181
+ }
182
+ return positional;
183
+ }
68
184
  if (!process.stdin.isTTY) return 'my-erp';
69
185
  const rl = createInterface({ input: process.stdin, output: process.stdout });
70
- const answer = (await rl.question('Project folder [my-erp]: ')).trim();
71
- rl.close();
72
- return answer === '' ? 'my-erp' : answer;
186
+ out('\n');
187
+ for (;;) {
188
+ const answer =
189
+ (await rl.question(`${c.cyan('?')} Project name ${c.gray('[my-erp]')}: `)).trim() || 'my-erp';
190
+ if (validName(answer)) {
191
+ rl.close();
192
+ return answer;
193
+ }
194
+ out(
195
+ ` ${c.yellow(mark.warn)} Lower-case letters, digits, - and _ (e.g. my-erp, padma-traders).\n`,
196
+ );
197
+ }
73
198
  }
74
199
 
200
+ banner();
75
201
  const target = resolve(await folder());
76
202
  const name =
77
203
  basename(target)
78
204
  .toLowerCase()
79
205
  .replace(/[^a-z0-9-]+/g, '-') || 'kosh-app';
80
- process.stdout.write(`\nCreating a Kosh installation in ${target}\n\n`);
206
+ step('📦', 'Kosh packages', target);
81
207
  mkdirSync(target, { recursive: true });
82
208
  if (!existsSync(join(target, 'package.json')))
83
209
  writeFileSync(
@@ -85,8 +211,7 @@ if (!existsSync(join(target, 'package.json')))
85
211
  `${JSON.stringify({ name, version: '0.1.0', private: true, type: 'module' }, null, 2)}\n`,
86
212
  );
87
213
 
88
- process.stdout.write(`Installing ${spec} …\n`);
89
- if (add(target, [spec]) !== 0) {
214
+ if ((await task(`Installing ${spec} (Shutro comes with it)`, () => add(target, [spec]))) !== 0) {
90
215
  process.stderr.write(`\nInstalling ${spec} failed (see above).\n`);
91
216
  process.exit(1);
92
217
  }
@@ -96,9 +221,10 @@ if (!existsSync(cli)) {
96
221
  process.stderr.write(`\n${cli} is missing: the kosh-erp installation is incomplete.\n`);
97
222
  process.exit(1);
98
223
  }
99
- process.stdout.write('\n');
100
224
  // kosh init asks its questions in this terminal (stdio inherited), then sets up the database.
101
- 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
+ });
102
228
  if (init !== 0) {
103
229
  process.stderr.write(
104
230
  `\nThe setup stopped. Fix the problem above, then finish it with:\n cd ${positional ?? basename(target)}\n npx kosh init\n`,
@@ -106,21 +232,22 @@ if (init !== 0) {
106
232
  process.exit(init);
107
233
  }
108
234
 
109
- process.stdout.write('\nInstalling the back-office tools …\n');
110
- 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) {
111
237
  process.stderr.write(`\n${pm} install failed (see above); run it again in ${target}.\n`);
112
238
  process.exit(1);
113
239
  }
114
240
 
115
- const cd = target === resolve('.') ? '' : ` cd ${positional ?? basename(target)}\n`;
116
- 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`;
117
243
  const web = pm === 'npm' ? 'npm run web' : `${pm} web`;
118
- process.stdout.write(`
119
- Kosh is installed.
244
+ const arrow = unicode ? '→' : '->';
245
+ out(`
246
+ ${icon('🚀')} ${c.bold(c.green('Kosh is installed.'))} ${c.gray('Start it:')}
120
247
 
121
- ${cd} ${start} # the API (reads .env)
122
- ${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`)}
123
250
 
124
- Everything in Docker instead: docker compose up -d --build → http://localhost:8080
125
- 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
126
253
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kosh-erp",
3
- "version": "0.1.2",
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",