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.
- package/README.md +5 -3
- package/index.js +162 -35
- 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
|
|
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 {
|
|
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
|
-
*
|
|
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);
|
|
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)
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
244
|
+
const arrow = unicode ? '→' : '->';
|
|
245
|
+
out(`
|
|
246
|
+
${icon('🚀')} ${c.bold(c.green('Kosh is installed.'))} ${c.gray('Start it:')}
|
|
120
247
|
|
|
121
|
-
${cd} ${
|
|
122
|
-
${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`)}
|
|
123
250
|
|
|
124
|
-
Everything in Docker instead:
|
|
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
|
`);
|