shutro-erp 0.2.0 → 0.3.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 +17 -10
- package/dist/account_portal/.kosh/bindings.d.ts +22 -0
- package/dist/account_portal/.kosh/bindings.js +15 -0
- package/dist/account_portal/.kosh/schema.d.ts +1350 -0
- package/dist/account_portal/index.d.ts +3 -0
- package/dist/account_portal/index.js +73 -0
- package/dist/account_portal/module.d.ts +5983 -0
- package/dist/account_portal/module.js +18 -0
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +142 -9
- package/dist/accounting/chart.d.ts +40 -8
- package/dist/accounting/chart.js +124 -30
- package/dist/accounting/closing.d.ts +6 -1
- package/dist/accounting/closing.js +5 -1
- package/dist/accounting/demo.js +6 -5
- package/dist/accounting/index.d.ts +3 -3
- package/dist/accounting/index.js +200 -23
- package/dist/accounting/module.d.ts +3326 -1086
- package/dist/accounting/module.js +408 -66
- package/dist/accounting/payments.d.ts +25 -0
- package/dist/accounting/payments.js +96 -4
- package/dist/accounting/web/RegisterPayment.d.ts +3 -2
- package/dist/accounting/web/RegisterPayment.js +39 -6
- package/dist/accounting/web/SettingsPage.js +5 -1
- package/dist/accounting/web/messages.js +10 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +195 -53
- package/dist/cli/launch.d.ts +16 -0
- package/dist/cli/launch.js +138 -0
- package/dist/cli/prompt.d.ts +35 -0
- package/dist/cli/prompt.js +46 -0
- package/dist/cli/templates.d.ts +11 -3
- package/dist/cli/templates.js +32 -7
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.d.ts +25 -5
- package/dist/cli/ui.js +49 -9
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +3 -1
- package/dist/contacts/index.js +24 -1
- package/dist/contacts/module.d.ts +42 -0
- package/dist/contacts/module.js +3 -1
- package/dist/import/convert.d.ts +55 -0
- package/dist/import/convert.js +224 -0
- package/dist/import/csv.d.ts +28 -0
- package/dist/import/csv.js +115 -0
- package/dist/import/index.d.ts +33 -0
- package/dist/import/index.js +60 -0
- package/dist/import/mapping.d.ts +15 -0
- package/dist/import/mapping.js +33 -0
- package/dist/import/match.d.ts +24 -0
- package/dist/import/match.js +60 -0
- package/dist/import/models.d.ts +2 -0
- package/dist/import/models.js +102 -0
- package/dist/import/routes.d.ts +13 -0
- package/dist/import/routes.js +392 -0
- package/dist/import/run.d.ts +131 -0
- package/dist/import/run.js +568 -0
- package/dist/import/service.d.ts +153 -0
- package/dist/import/service.js +367 -0
- package/dist/import/web/ImportButton.d.ts +7 -0
- package/dist/import/web/ImportButton.js +31 -0
- package/dist/import/web/ImportWizard.d.ts +75 -0
- package/dist/import/web/ImportWizard.js +217 -0
- package/dist/import/web/index.d.ts +13 -0
- package/dist/import/web/index.js +18 -0
- package/dist/import/web/messages.d.ts +2 -0
- package/dist/import/web/messages.js +139 -0
- package/dist/import/worker.d.ts +3 -0
- package/dist/import/worker.js +52 -0
- package/dist/import/xlsx.d.ts +20 -0
- package/dist/import/xlsx.js +192 -0
- package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_bd/.kosh/bindings.js +15 -0
- package/dist/l10n_bd/.kosh/schema.d.ts +1281 -0
- package/dist/l10n_bd/documents.d.ts +8 -0
- package/dist/l10n_bd/documents.js +183 -0
- package/dist/l10n_bd/index.d.ts +8 -0
- package/dist/l10n_bd/index.js +156 -0
- package/dist/l10n_bd/module.d.ts +5705 -0
- package/dist/l10n_bd/module.js +163 -0
- package/dist/l10n_bd/mushak.d.ts +67 -0
- package/dist/l10n_bd/mushak.js +139 -0
- package/dist/l10n_bd/routes.d.ts +3 -0
- package/dist/l10n_bd/routes.js +205 -0
- package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
- package/dist/l10n_bd/web/MushakPage.js +53 -0
- package/dist/l10n_bd/web/index.d.ts +13 -0
- package/dist/l10n_bd/web/index.js +18 -0
- package/dist/l10n_bd/web/messages.d.ts +2 -0
- package/dist/l10n_bd/web/messages.js +101 -0
- package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_in/.kosh/bindings.js +15 -0
- package/dist/l10n_in/.kosh/schema.d.ts +1343 -0
- package/dist/l10n_in/documents.d.ts +8 -0
- package/dist/l10n_in/documents.js +114 -0
- package/dist/l10n_in/einvoice.d.ts +253 -0
- package/dist/l10n_in/einvoice.js +485 -0
- package/dist/l10n_in/gstr.d.ts +109 -0
- package/dist/l10n_in/gstr.js +430 -0
- package/dist/l10n_in/index.d.ts +20 -0
- package/dist/l10n_in/index.js +232 -0
- package/dist/l10n_in/module.d.ts +7823 -0
- package/dist/l10n_in/module.js +427 -0
- package/dist/l10n_in/routes-einvoice.d.ts +15 -0
- package/dist/l10n_in/routes-einvoice.js +124 -0
- package/dist/l10n_in/routes.d.ts +17 -0
- package/dist/l10n_in/routes.js +282 -0
- package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
- package/dist/l10n_in/web/EInvoicePanel.js +63 -0
- package/dist/l10n_in/web/GstPage.d.ts +7 -0
- package/dist/l10n_in/web/GstPage.js +57 -0
- package/dist/l10n_in/web/GstSettings.d.ts +10 -0
- package/dist/l10n_in/web/GstSettings.js +36 -0
- package/dist/l10n_in/web/index.d.ts +15 -0
- package/dist/l10n_in/web/index.js +22 -0
- package/dist/l10n_in/web/messages.d.ts +2 -0
- package/dist/l10n_in/web/messages.js +166 -0
- package/dist/modules-sdk/demo.d.ts +2 -0
- package/dist/modules-sdk/demo.js +7 -3
- package/dist/modules-sdk/index.d.ts +2 -2
- package/dist/modules-sdk/index.js +1 -1
- package/dist/modules-sdk/manifest.d.ts +132 -0
- package/dist/modules-sdk/manifest.js +22 -0
- package/dist/notify/inbox.js +21 -0
- package/dist/notify/messages.js +4 -0
- package/dist/notify/models.d.ts +1 -1
- package/dist/notify/models.js +2 -0
- package/dist/notify/web/messages.js +4 -0
- package/dist/portal/.kosh/bindings.d.ts +22 -0
- package/dist/portal/.kosh/bindings.js +15 -0
- package/dist/portal/.kosh/schema.d.ts +346 -0
- package/dist/portal/announce.d.ts +16 -0
- package/dist/portal/announce.js +117 -0
- package/dist/portal/documents.d.ts +17 -0
- package/dist/portal/documents.js +61 -0
- package/dist/portal/index.d.ts +20 -0
- package/dist/portal/index.js +65 -0
- package/dist/portal/module.d.ts +1405 -0
- package/dist/portal/module.js +84 -0
- package/dist/portal/routes-documents.d.ts +2 -0
- package/dist/portal/routes-documents.js +391 -0
- package/dist/portal/routes.d.ts +13 -0
- package/dist/portal/routes.js +313 -0
- package/dist/portal/rules.d.ts +11 -0
- package/dist/portal/rules.js +25 -0
- package/dist/portal/web/PortalAccess.d.ts +8 -0
- package/dist/portal/web/PortalAccess.js +66 -0
- package/dist/portal/web/index.d.ts +12 -0
- package/dist/portal/web/index.js +17 -0
- package/dist/portal/web/messages.d.ts +2 -0
- package/dist/portal/web/messages.js +33 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +12 -3
- package/dist/product/demo.js +6 -5
- package/dist/product/index.d.ts +1 -0
- package/dist/product/index.js +17 -4
- package/dist/product/module.d.ts +346 -14
- package/dist/product/module.js +48 -18
- package/dist/product/taxes.d.ts +59 -0
- package/dist/product/taxes.js +61 -0
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +3 -1
- package/dist/project/module.d.ts +20 -0
- package/dist/project_portal/.kosh/bindings.d.ts +22 -0
- package/dist/project_portal/.kosh/bindings.js +15 -0
- package/dist/project_portal/.kosh/schema.d.ts +671 -0
- package/dist/project_portal/index.d.ts +9 -0
- package/dist/project_portal/index.js +42 -0
- package/dist/project_portal/module.d.ts +2142 -0
- package/dist/project_portal/module.js +22 -0
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +15 -4
- package/dist/purchase/demo.js +3 -3
- package/dist/purchase/index.js +1 -1
- package/dist/purchase/module.d.ts +232 -8
- package/dist/purchase/module.js +51 -9
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +144 -10
- package/dist/purchase_account/module.d.ts +2008 -641
- package/dist/purchase_account/module.js +2 -2
- package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_portal/.kosh/bindings.js +15 -0
- package/dist/purchase_portal/.kosh/schema.d.ts +822 -0
- package/dist/purchase_portal/index.d.ts +9 -0
- package/dist/purchase_portal/index.js +53 -0
- package/dist/purchase_portal/module.d.ts +2758 -0
- package/dist/purchase_portal/module.js +18 -0
- package/dist/reporting/documents.js +20 -3
- package/dist/reporting/index.d.ts +1 -0
- package/dist/reporting/index.js +1 -0
- package/dist/reporting/routes-documents.js +9 -2
- package/dist/reporting/words.d.ts +11 -0
- package/dist/reporting/words.js +229 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +144 -10
- package/dist/sale_account/module.d.ts +2008 -641
- package/dist/sale_account/module.js +3 -3
- package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
- package/dist/sale_portal/.kosh/bindings.js +15 -0
- package/dist/sale_portal/.kosh/schema.d.ts +736 -0
- package/dist/sale_portal/index.d.ts +9 -0
- package/dist/sale_portal/index.js +71 -0
- package/dist/sale_portal/module.d.ts +2718 -0
- package/dist/sale_portal/module.js +19 -0
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +15 -4
- package/dist/sale_project/module.d.ts +390 -10
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +15 -4
- package/dist/sales/demo.js +3 -3
- package/dist/sales/index.js +67 -1
- package/dist/sales/module.d.ts +232 -8
- package/dist/sales/module.js +51 -12
- package/dist/server/auth/base.js +2 -0
- package/dist/server/auth/service.d.ts +2 -0
- package/dist/server/auth/service.js +7 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/server.js +26 -1
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.js +1 -1
- package/dist/ui/context.d.ts +3 -1
- package/dist/ui/context.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/lib/format.d.ts +2 -0
- package/dist/ui/lib/format.js +3 -1
- package/dist/ui/views/FormView.js +19 -6
- package/package.json +50 -2
package/dist/cli/init.js
CHANGED
|
@@ -8,17 +8,37 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { spawn, spawnSync } from 'node:child_process';
|
|
10
10
|
import { randomBytes } from 'node:crypto';
|
|
11
|
-
import { existsSync } from 'node:fs';
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
13
|
import { createServer as createNetServer } from 'node:net';
|
|
14
|
-
import { basename, join, resolve } from 'node:path';
|
|
14
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
15
16
|
import { parseArgs } from 'node:util';
|
|
16
17
|
import { base, createServer, DEMO_PASSWORD, FRAMEWORK_TABLES, installBase, installDemo, KOSH_VERSION, migrate, pg, recordInstalledApps, } from '../server/index.js';
|
|
18
|
+
import { launch } from './launch.js';
|
|
17
19
|
import { seed } from './project.js';
|
|
18
|
-
import { defaultsPrompter, terminalPrompter } from './prompt.js';
|
|
20
|
+
import { choose, chooseMany, defaultsPrompter, terminalPrompter } from './prompt.js';
|
|
19
21
|
import * as templates from './templates.js';
|
|
20
|
-
import { APPS, bridgesFor, SELECTABLE_APPS } from './templates.js';
|
|
21
|
-
import {
|
|
22
|
+
import { APPS, bridgesFor, COUNTRY_NAMES, LOCALIZED_COUNTRIES, SELECTABLE_APPS, } from './templates.js';
|
|
23
|
+
import { CancelledError, interactivePrompter } from './tui.js';
|
|
24
|
+
import { createUi, PLAIN_TERMINAL, railed } from './ui.js';
|
|
25
|
+
/**
|
|
26
|
+
* The version of the `shutro-erp` package this CLI runs from: new projects depend on it. In the workspace (no such
|
|
27
|
+
* package around the CLI) the framework version stands in.
|
|
28
|
+
*/
|
|
29
|
+
export function releaseVersion(from = dirname(fileURLToPath(import.meta.url))) {
|
|
30
|
+
for (let dir = from, i = 0; i < 6; i++, dir = dirname(dir)) {
|
|
31
|
+
try {
|
|
32
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
33
|
+
if (manifest.name === 'shutro-erp' && typeof manifest.version === 'string')
|
|
34
|
+
return manifest.version;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// no package.json here: look further up
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return KOSH_VERSION;
|
|
41
|
+
}
|
|
22
42
|
export const INIT_USAGE = `Usage: kosh init [folder] [options]
|
|
23
43
|
|
|
24
44
|
Sets up a Kosh installation: asks for the database, the first company, the administrator, the apps,
|
|
@@ -30,6 +50,8 @@ Options (each also answers its question; with --yes the rest take their defaults
|
|
|
30
50
|
--docker-db start PostgreSQL in Docker (compose.yaml) instead
|
|
31
51
|
--create-database create the database when it does not exist
|
|
32
52
|
--company <name> the first company --currency <code> its currency (ISO 4217)
|
|
53
|
+
--country <code> the company's country (ISO 3166): ${LOCALIZED_COUNTRIES.join(', ')} install its localization
|
|
54
|
+
with Accounting (default: BD for BDT, IN for INR, none otherwise; "none" for none)
|
|
33
55
|
--lang <en|bn> language --tz <zone> time zone (IANA)
|
|
34
56
|
--admin-login <login> --admin-name <name> --admin-email <email>
|
|
35
57
|
--admin-password <pw> (or KOSH_ADMIN_PASSWORD; at least 12 characters)
|
|
@@ -37,10 +59,13 @@ Options (each also answers its question; with --yes the rest take their defaults
|
|
|
37
59
|
--storage-dir <dir> attachments in a folder (default ./var/attachments)
|
|
38
60
|
--s3-endpoint <url> --s3-bucket <name> --s3-access-key <key> --s3-secret-key <secret> [--s3-region <r>]
|
|
39
61
|
--valkey-url <url> optional: rate limits and caches shared across servers
|
|
40
|
-
--port <port> the API's port (default 3000)
|
|
62
|
+
--port <port> the API's port (default 3000, or the next free one)
|
|
63
|
+
--web-port <port> the back-office's port in development (default 5173, or the next free one)
|
|
41
64
|
--demo load demo data: a sample company with contacts, products, orders, invoices and
|
|
42
65
|
projects, made by demo users (dina, rahim, karim, sumi; one shared password)
|
|
43
66
|
--no-2fa skip the authenticator enrolment (administrator rights wait for it)
|
|
67
|
+
--no-start do not install, start and open the installation at the end (in a terminal it
|
|
68
|
+
installs the dependencies, starts the API and the back-office, opens the browser)
|
|
44
69
|
--yes do not ask; use flags and defaults
|
|
45
70
|
`;
|
|
46
71
|
const dockerAvailable = () => spawnSync('docker', ['version', '--format', '{{.Server.Version}}'], { encoding: 'utf8' })
|
|
@@ -82,7 +107,7 @@ async function startDockerDb(dir, name, postgres, ui) {
|
|
|
82
107
|
name,
|
|
83
108
|
apps: [],
|
|
84
109
|
port: 3000,
|
|
85
|
-
koshVersion:
|
|
110
|
+
koshVersion: releaseVersion(),
|
|
86
111
|
dockerDb: postgres.db,
|
|
87
112
|
}));
|
|
88
113
|
return ui.task(`Starting PostgreSQL 18 in Docker (port ${postgres.port})`, async (task) => {
|
|
@@ -210,9 +235,19 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
210
235
|
if (flag('database-url') === undefined) {
|
|
211
236
|
const docker = flags['docker-db'] === true ||
|
|
212
237
|
(dockerAvailable() && flags['yes'] !== true
|
|
213
|
-
? (await
|
|
214
|
-
|
|
215
|
-
|
|
238
|
+
? (await choose(prompter, {
|
|
239
|
+
message: 'Which PostgreSQL?',
|
|
240
|
+
fallback: 'PostgreSQL: (1) a server I already have, or (2) start one in Docker for me',
|
|
241
|
+
choices: [
|
|
242
|
+
{ value: 'server', label: 'A server I already have', hint: 'host, port, user' },
|
|
243
|
+
{
|
|
244
|
+
value: 'docker',
|
|
245
|
+
label: 'Start one in Docker for me',
|
|
246
|
+
hint: 'PostgreSQL 18, its own volume',
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
default: 'docker',
|
|
250
|
+
})) === 'docker'
|
|
216
251
|
: false);
|
|
217
252
|
if (docker) {
|
|
218
253
|
if (!dockerAvailable())
|
|
@@ -294,9 +329,41 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
294
329
|
const currency = (await q('currency', 'Currency (ISO code)', 'BDT', {
|
|
295
330
|
validate: (v) => (/^[A-Za-z]{3}$/.test(v) ? null : 'Three letters, e.g. BDT, USD, EUR'),
|
|
296
331
|
})).toUpperCase();
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
332
|
+
const countryFlag = flag('country');
|
|
333
|
+
if (countryFlag !== undefined && !/^([A-Za-z]{2}|none)$/i.test(countryFlag))
|
|
334
|
+
throw new Error('--country: Two letters, e.g. BD, or none');
|
|
335
|
+
const countryAnswer = (countryFlag ??
|
|
336
|
+
(await choose(prompter, {
|
|
337
|
+
message: 'Country (taxes, chart of accounts, fiscal year)',
|
|
338
|
+
fallback: `Country (ISO code; ${LOCALIZED_COUNTRIES.join(', ')} set up taxes and the chart; "none" for none)`,
|
|
339
|
+
choices: [
|
|
340
|
+
...LOCALIZED_COUNTRIES.map((c) => ({
|
|
341
|
+
value: c,
|
|
342
|
+
label: COUNTRY_NAMES[c] ?? c,
|
|
343
|
+
hint: 'its taxes, chart and returns',
|
|
344
|
+
})),
|
|
345
|
+
{
|
|
346
|
+
value: 'none',
|
|
347
|
+
label: 'Another country',
|
|
348
|
+
hint: 'the generic chart; taxes are yours to add',
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
default: currency === 'BDT' ? 'BD' : currency === 'INR' ? 'IN' : 'none',
|
|
352
|
+
}))).toUpperCase();
|
|
353
|
+
const country = countryAnswer === 'NONE' ? '' : countryAnswer;
|
|
354
|
+
const langFlag = flag('lang');
|
|
355
|
+
if (langFlag !== undefined && !['en', 'bn'].includes(langFlag))
|
|
356
|
+
throw new Error('--lang: en or bn');
|
|
357
|
+
const lang = langFlag ??
|
|
358
|
+
(await choose(prompter, {
|
|
359
|
+
message: 'Language',
|
|
360
|
+
fallback: 'Language (en, bn)',
|
|
361
|
+
choices: [
|
|
362
|
+
{ value: 'en', label: 'English' },
|
|
363
|
+
{ value: 'bn', label: 'বাংলা', hint: 'Bangla' },
|
|
364
|
+
],
|
|
365
|
+
default: 'en',
|
|
366
|
+
}));
|
|
300
367
|
const tz = await q('tz', 'Time zone', Intl.DateTimeFormat().resolvedOptions().timeZone, {
|
|
301
368
|
validate: (v) => {
|
|
302
369
|
try {
|
|
@@ -337,36 +404,43 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
337
404
|
ui.warn('The passwords differ; try again.');
|
|
338
405
|
}
|
|
339
406
|
ui.step('apps', 'Apps', 'All are selected; remove the ones you do not need');
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
.map((
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
407
|
+
const appsFlag = flag('apps');
|
|
408
|
+
if (appsFlag === undefined && prompter.multiselect === undefined)
|
|
409
|
+
for (const key of SELECTABLE_APPS) {
|
|
410
|
+
const app = APPS[key];
|
|
411
|
+
ui.write(` ${ui.terminal.emoji ? app.icon : ui.style('cyan', '•')} ${ui.style('bold', key.padEnd(12))} ${ui.style('gray', app.label)}\n`);
|
|
412
|
+
}
|
|
413
|
+
const unknownApps = (appsFlag ?? '')
|
|
414
|
+
.split(',')
|
|
415
|
+
.map((s) => s.trim())
|
|
416
|
+
.filter((s) => s !== '' && !(s in APPS));
|
|
417
|
+
if (unknownApps.length > 0)
|
|
418
|
+
throw new Error(`--apps: Unknown: ${unknownApps.join(', ')}`);
|
|
419
|
+
const appList = appsFlag?.split(',').map((s) => s.trim()) ??
|
|
420
|
+
(await chooseMany(prompter, {
|
|
421
|
+
message: 'Apps to install',
|
|
422
|
+
fallback: 'Apps to install (comma-separated)',
|
|
423
|
+
choices: SELECTABLE_APPS.map((key) => ({ value: key, label: key, hint: APPS[key].label })),
|
|
424
|
+
defaults: SELECTABLE_APPS,
|
|
425
|
+
}));
|
|
426
|
+
const chosen = [...new Set(appList.filter((s) => s in APPS))];
|
|
427
|
+
// The bridges of the chosen apps come along (RFC 0008 §5.1), and the country's localization (RFC 0016).
|
|
428
|
+
const apps = [...chosen, ...bridgesFor(chosen, country).filter((b) => !chosen.includes(b))];
|
|
361
429
|
const demo = flags['demo'] === true ||
|
|
362
430
|
(await prompter.confirm(' Load demo data (a sample company with orders, invoices and projects; not for production)?', false));
|
|
363
431
|
ui.step('storage', 'File storage', 'Where attachments and generated documents are kept');
|
|
364
432
|
let storage;
|
|
365
433
|
const s3 = flag('s3-endpoint') !== undefined ||
|
|
366
434
|
(flag('storage-dir') === undefined &&
|
|
367
|
-
(await
|
|
368
|
-
|
|
369
|
-
|
|
435
|
+
(await choose(prompter, {
|
|
436
|
+
message: 'Where are files kept?',
|
|
437
|
+
fallback: 'Store files in (1) a folder or (2) S3-compatible storage',
|
|
438
|
+
choices: [
|
|
439
|
+
{ value: 'dir', label: 'A folder on this server', hint: './var/attachments' },
|
|
440
|
+
{ value: 's3', label: 'S3-compatible storage', hint: 'AWS S3, MinIO, SeaweedFS, R2' },
|
|
441
|
+
],
|
|
442
|
+
default: 'dir',
|
|
443
|
+
})) === 's3');
|
|
370
444
|
if (s3)
|
|
371
445
|
storage = {
|
|
372
446
|
kind: 's3',
|
|
@@ -389,17 +463,20 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
389
463
|
};
|
|
390
464
|
else
|
|
391
465
|
storage = { kind: 'dir', dir: await q('storage-dir', 'Folder', './var/attachments') };
|
|
392
|
-
ui.step('server', 'Server', 'The API
|
|
393
|
-
const
|
|
394
|
-
const port = Number(await q('port', 'API port',
|
|
395
|
-
|
|
466
|
+
ui.step('server', 'Server', 'The ports of the API and the back-office, and an optional shared cache');
|
|
467
|
+
const portNumber = (v) => /^\d+$/.test(v) && Number(v) > 0 && Number(v) < 65536 ? null : 'A port number (1–65535)';
|
|
468
|
+
const port = Number(await q('port', 'API port', String(await freePort(3000)), { validate: portNumber }));
|
|
469
|
+
const webPort = Number(await q('web-port', 'Back-office (web) port', String(await freePort(port === 5173 ? 5174 : 5173)), {
|
|
470
|
+
validate: (v) => portNumber(v) ?? (Number(v) === port ? 'Another port than the API' : null),
|
|
396
471
|
}));
|
|
472
|
+
const valkeyUrl = await q('valkey-url', 'Valkey/Redis URL (optional, Enter to skip)', '');
|
|
397
473
|
return {
|
|
398
474
|
dir,
|
|
399
475
|
name,
|
|
400
476
|
databaseUrl,
|
|
401
477
|
company,
|
|
402
478
|
currency,
|
|
479
|
+
country,
|
|
403
480
|
lang,
|
|
404
481
|
tz,
|
|
405
482
|
admin: { login, name: adminName, email, password: password },
|
|
@@ -408,6 +485,7 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
408
485
|
storage,
|
|
409
486
|
valkeyUrl,
|
|
410
487
|
port,
|
|
488
|
+
webPort,
|
|
411
489
|
postgres,
|
|
412
490
|
dockerDb,
|
|
413
491
|
vapid: (await import('../notify/index.js')).generateVapidKeys(),
|
|
@@ -426,6 +504,7 @@ async function modulesFor(apps) {
|
|
|
426
504
|
(await import('../mail/index.js')).mail,
|
|
427
505
|
(await import('../automation/index.js')).automation,
|
|
428
506
|
(await import('../studio/index.js')).studio,
|
|
507
|
+
(await import('../import/index.js')).dataImport,
|
|
429
508
|
];
|
|
430
509
|
for (const app of apps) {
|
|
431
510
|
if (app === 'permissions')
|
|
@@ -453,6 +532,10 @@ async function modulesFor(apps) {
|
|
|
453
532
|
modules.push((await import('../project/index.js')).project);
|
|
454
533
|
if (app === 'sale_project')
|
|
455
534
|
modules.push((await import('../sale_project/index.js')).saleProject);
|
|
535
|
+
if (app === 'l10n_bd')
|
|
536
|
+
modules.push((await import('../l10n_bd/index.js')).l10nBd);
|
|
537
|
+
if (app === 'l10n_in')
|
|
538
|
+
modules.push((await import('../l10n_in/index.js')).l10nIn);
|
|
456
539
|
}
|
|
457
540
|
return modules;
|
|
458
541
|
}
|
|
@@ -468,7 +551,7 @@ function envFile(a) {
|
|
|
468
551
|
lines.push(`S3_ENDPOINT=${a.storage.endpoint}`, `S3_BUCKET=${a.storage.bucket}`, `S3_ACCESS_KEY=${a.storage.accessKey}`, `S3_SECRET_KEY=${a.storage.secretKey}`, `S3_REGION=${a.storage.region}`);
|
|
469
552
|
if (a.valkeyUrl !== '')
|
|
470
553
|
lines.push(`VALKEY_URL=${a.valkeyUrl}`);
|
|
471
|
-
lines.push('', '# Where users open the back-office (links in emails and notifications).',
|
|
554
|
+
lines.push('', '# Where users open the back-office (links in emails and notifications).', `KOSH_BASE_URL=http://localhost:${a.webPort}`, '# Email notifications: an SMTP server (Mailpit in development: smtp://localhost:1025, inbox on :8025).', '# SMTP_URL=smtp://user:password@smtp.example.com:587', '# MAIL_FROM=Kosh <no-reply@example.com>', '# Replies by email (RFC 0006 part 7): the inbound mailbox domain, and the webhook secret your provider sends.', `MAIL_INBOUND_SECRET=${a.inboundSecret}`, '# MAIL_INBOUND_DOMAIN=mail.example.com', '# MAIL_INBOUND_ALIAS=reply', `MAIL_INBOUND_WEBHOOK_SECRET=${a.inboundWebhookSecret}`, '# Without a webhook provider, a mailbox read every 30 s: IMAP_URL=imaps://user:password@host:993/INBOX', '', '# Kosh Studio (RFC 0012): propose (default: changes become patches) or write (development, one server: Studio', '# writes src/modules/<name>/ and applies at once). KOSH_STUDIO_DIR: the project folder (default: the working one).', '# KOSH_STUDIO=write', '# Proposals as pull requests (GitHub) or merge requests (GitLab): the repository and a token with the rights', '# to push branches and open them. Without them, proposals are patches to download.', '# KOSH_STUDIO_GIT_URL=https://github.com/acme/erp', '# KOSH_STUDIO_GIT_TOKEN=', '# KOSH_STUDIO_GIT_BASE=main', '', '# The AI assistant (RFC 0013): off until an administrator switches it on in Settings → AI assistant.', '# ANTHROPIC_API_KEY=', '# KOSH_ASSISTANT_MODEL=claude-opus-5-5', '# KOSH_ASSISTANT_MONTHLY_TOKENS=2000000', '', '# SMS and WhatsApp through Twilio (opt-in per user; a phone number on the user).', '# TWILIO_ACCOUNT_SID=', '# TWILIO_AUTH_TOKEN=', '# TWILIO_SMS_FROM=+1...', '# TWILIO_WHATSAPP_FROM=+1...', '', '# Web push (generated by kosh init; keep them, browsers subscribed with the public key stay subscribed).', `VAPID_PUBLIC_KEY=${a.vapid.publicKey}`, `VAPID_PRIVATE_KEY=${a.vapid.privateKey}`, `VAPID_SUBJECT=${a.admin.email === '' ? 'mailto:admin@localhost' : `mailto:${a.admin.email}`}`);
|
|
472
555
|
lines.push('', '# PostgreSQL in Docker (compose.yaml): docker compose up -d db, or everything with docker compose up -d --build', `POSTGRES_USER=${a.postgres.user}`, `POSTGRES_PASSWORD=${a.postgres.password}`, `POSTGRES_DB=${a.postgres.db}`, `POSTGRES_PORT=${a.postgres.port}`);
|
|
473
556
|
return `${lines.join('\n')}\n`;
|
|
474
557
|
}
|
|
@@ -478,7 +561,8 @@ async function writeProject(a, ui) {
|
|
|
478
561
|
name: a.name,
|
|
479
562
|
apps: a.apps,
|
|
480
563
|
port: a.port,
|
|
481
|
-
|
|
564
|
+
webPort: a.webPort,
|
|
565
|
+
koshVersion: releaseVersion(),
|
|
482
566
|
dockerDb: a.postgres.db,
|
|
483
567
|
fileStorage: a.storage.kind === 'dir',
|
|
484
568
|
};
|
|
@@ -676,6 +760,7 @@ export async function runInit(argv, io) {
|
|
|
676
760
|
'create-database': { type: 'boolean' },
|
|
677
761
|
company: { type: 'string' },
|
|
678
762
|
currency: { type: 'string' },
|
|
763
|
+
country: { type: 'string' },
|
|
679
764
|
lang: { type: 'string' },
|
|
680
765
|
tz: { type: 'string' },
|
|
681
766
|
'admin-login': { type: 'string' },
|
|
@@ -691,7 +776,9 @@ export async function runInit(argv, io) {
|
|
|
691
776
|
's3-region': { type: 'string' },
|
|
692
777
|
'valkey-url': { type: 'string' },
|
|
693
778
|
port: { type: 'string' },
|
|
779
|
+
'web-port': { type: 'string' },
|
|
694
780
|
'no-2fa': { type: 'boolean' },
|
|
781
|
+
'no-start': { type: 'boolean' },
|
|
695
782
|
demo: { type: 'boolean' },
|
|
696
783
|
'docker-db': { type: 'boolean' },
|
|
697
784
|
yes: { type: 'boolean', short: 'y' },
|
|
@@ -705,15 +792,34 @@ export async function runInit(argv, io) {
|
|
|
705
792
|
const terminal = io.terminal ?? PLAIN_TERMINAL;
|
|
706
793
|
const interactive2fa = values['no-2fa'] !== true && values.yes !== true;
|
|
707
794
|
// Six question steps, then the project files and the database, then (interactively) the second factor.
|
|
708
|
-
|
|
795
|
+
// At the end, in a terminal: install, start and open the installation (not for scripts, CI and tests).
|
|
796
|
+
const autoStart = values['no-start'] !== true && io.prompter === undefined && process.stdout.isTTY === true;
|
|
797
|
+
const ui = createUi(io.write, terminal, {
|
|
798
|
+
steps: (interactive2fa ? 9 : 8) + (autoStart ? 1 : 0),
|
|
799
|
+
});
|
|
800
|
+
const interactive = io.prompter === undefined &&
|
|
801
|
+
values.yes !== true &&
|
|
802
|
+
process.stdin.isTTY === true &&
|
|
803
|
+
railed(terminal);
|
|
709
804
|
const asked = io.prompter ??
|
|
710
805
|
(values.yes === true
|
|
711
806
|
? defaultsPrompter()
|
|
712
|
-
:
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
807
|
+
: interactive
|
|
808
|
+
? interactivePrompter(process.stdin, process.stdout, terminal)
|
|
809
|
+
: terminalPrompter(process.stdin, process.stdout, {
|
|
810
|
+
marker: ui.style('cyan', '? '),
|
|
811
|
+
fallback: (t) => ui.style('gray', t),
|
|
812
|
+
problem: (t) => ui.style('yellow', `${terminal.unicode ? '▲' : '!'} ${t}`),
|
|
813
|
+
}));
|
|
814
|
+
const paused = async (work) => {
|
|
815
|
+
ui.pause();
|
|
816
|
+
try {
|
|
817
|
+
return await work();
|
|
818
|
+
}
|
|
819
|
+
finally {
|
|
820
|
+
ui.resume();
|
|
821
|
+
}
|
|
822
|
+
};
|
|
717
823
|
// A question stops the spinner while it is asked (the prompter writes to the terminal itself).
|
|
718
824
|
const prompter = {
|
|
719
825
|
async ask(q) {
|
|
@@ -734,13 +840,23 @@ export async function runInit(argv, io) {
|
|
|
734
840
|
ui.resume();
|
|
735
841
|
}
|
|
736
842
|
},
|
|
843
|
+
...(asked.select === undefined
|
|
844
|
+
? {}
|
|
845
|
+
: { select: (q) => paused(() => asked.select(q)) }),
|
|
846
|
+
...(asked.multiselect === undefined
|
|
847
|
+
? {}
|
|
848
|
+
: {
|
|
849
|
+
multiselect: (q) => paused(() => asked.multiselect(q)),
|
|
850
|
+
}),
|
|
737
851
|
close: () => asked.close(),
|
|
738
852
|
};
|
|
739
853
|
let server;
|
|
740
854
|
const started = Date.now();
|
|
741
855
|
try {
|
|
742
|
-
await ui.banner(`The ERP framework on Shutro · v${
|
|
743
|
-
ui.
|
|
856
|
+
await ui.banner(`The ERP framework on Shutro · v${releaseVersion()}`);
|
|
857
|
+
ui.intro('Kosh setup', interactive
|
|
858
|
+
? 'Enter keeps the default · ↑ ↓ choose · Space ticks · Ctrl+C quits'
|
|
859
|
+
: 'Kosh setup: press Enter to keep the value in [brackets].');
|
|
744
860
|
const answers = await ask(prompter, values, io.cwd, positionals[0], ui);
|
|
745
861
|
ui.step('files', 'Project files', answers.dir);
|
|
746
862
|
await ui.task('Writing the project', async () => {
|
|
@@ -785,7 +901,10 @@ export async function runInit(argv, io) {
|
|
|
785
901
|
else if (values.yes === true && values['no-2fa'] !== true)
|
|
786
902
|
ui.info('The authenticator app is enrolled interactively: run npx kosh init again (without --yes).');
|
|
787
903
|
const rows = [
|
|
788
|
-
[
|
|
904
|
+
[
|
|
905
|
+
'Company',
|
|
906
|
+
`${answers.company} (${answers.currency}${answers.country === '' ? '' : `, ${answers.country}`})`,
|
|
907
|
+
],
|
|
789
908
|
[
|
|
790
909
|
'Administrator',
|
|
791
910
|
`${answers.admin.login} ${ui.style(enrolled ? 'green' : 'yellow', enrolled ? '· 2FA on' : '· 2FA pending')}`,
|
|
@@ -800,6 +919,7 @@ export async function runInit(argv, io) {
|
|
|
800
919
|
'mail',
|
|
801
920
|
'automation',
|
|
802
921
|
'studio',
|
|
922
|
+
'import',
|
|
803
923
|
...answers.apps,
|
|
804
924
|
].join(', '),
|
|
805
925
|
],
|
|
@@ -807,20 +927,42 @@ export async function runInit(argv, io) {
|
|
|
807
927
|
? [['Demo users', `dina, rahim, karim, sumi · password "${DEMO_PASSWORD}"`]]
|
|
808
928
|
: []),
|
|
809
929
|
['Database', answers.dockerDb ? `Docker (port ${answers.postgres.port})` : 'your server'],
|
|
930
|
+
['Back-office', `http://localhost:${answers.webPort}`],
|
|
810
931
|
['API', `http://localhost:${answers.port}`],
|
|
811
932
|
['Took', `${((Date.now() - started) / 1000).toFixed(1)}s`],
|
|
812
933
|
];
|
|
934
|
+
if (!autoStart)
|
|
935
|
+
ui.outro(ui.style('green', 'Kosh is set up'));
|
|
813
936
|
ui.card(`${terminal.emoji ? '🚀 ' : ''}Kosh is set up`, rows, enrolled || values['no-2fa'] === true ? 'green' : 'yellow');
|
|
937
|
+
if (autoStart) {
|
|
938
|
+
if (!enrolled)
|
|
939
|
+
ui.warn('Administrator rights need a second factor: run `kosh init` again to enrol the authenticator app.');
|
|
940
|
+
ui.step('rocket', 'Starting Kosh', 'Dependencies, the API, the back-office, then your browser');
|
|
941
|
+
// The installer's own connection is not needed any more: the API has its own.
|
|
942
|
+
await server?.close();
|
|
943
|
+
server = undefined;
|
|
944
|
+
await launch({
|
|
945
|
+
dir: answers.dir,
|
|
946
|
+
port: answers.port,
|
|
947
|
+
webPort: answers.webPort,
|
|
948
|
+
login: answers.admin.login,
|
|
949
|
+
}, ui);
|
|
950
|
+
return 0;
|
|
951
|
+
}
|
|
814
952
|
const relative = answers.dir === resolve(io.cwd) ? '' : `cd ${positionals[0] ?? answers.dir}\n `;
|
|
815
953
|
const cmd = (c) => ui.style('cyan', c);
|
|
816
954
|
ui.write(`\n ${ui.style('bold', 'Next')}\n ${relative}${cmd('npm install')}\n ${cmd('npm start')} ${ui.style('gray', `# the API on http://localhost:${answers.port}`)}\n` +
|
|
817
|
-
` ${cmd('npm run web')} ${ui.style('gray',
|
|
955
|
+
` ${cmd('npm run web')} ${ui.style('gray', `# the back-office on http://localhost:${answers.webPort}`)}\n\n` +
|
|
818
956
|
` Sign in as "${answers.admin.login}"${enrolled ? ' with your password and a code from the authenticator app' : ''}.\n`);
|
|
819
957
|
if (!enrolled)
|
|
820
958
|
ui.warn('Administrator rights need a second factor: run `kosh init` again to enrol the authenticator app.');
|
|
821
959
|
return 0;
|
|
822
960
|
}
|
|
823
961
|
catch (error) {
|
|
962
|
+
if (error instanceof CancelledError) {
|
|
963
|
+
ui.outro(ui.style('red', 'Setup cancelled: nothing more was done.'));
|
|
964
|
+
return 130;
|
|
965
|
+
}
|
|
824
966
|
ui.write('\n');
|
|
825
967
|
ui.fail(`kosh init stopped: ${error instanceof Error ? error.message : String(error)}`);
|
|
826
968
|
return 1;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Ui } from './ui.js';
|
|
2
|
+
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
3
|
+
/** The package manager that ran the CLI (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`), from its user agent; npm otherwise. */
|
|
4
|
+
export declare function packageManager(env?: NodeJS.ProcessEnv): PackageManager;
|
|
5
|
+
/** The command that opens `url` in the default browser on `platform`. */
|
|
6
|
+
export declare function browserCommand(url: string, platform?: string): readonly [string, readonly string[]];
|
|
7
|
+
export interface LaunchOptions {
|
|
8
|
+
readonly dir: string;
|
|
9
|
+
readonly port: number;
|
|
10
|
+
readonly webPort: number;
|
|
11
|
+
readonly login: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Installs, starts and opens the installation; returns when the user stops it (Ctrl+C) or a server stops by itself.
|
|
15
|
+
*/
|
|
16
|
+
export declare function launch(o: LaunchOptions, ui: Ui): Promise<void>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* After `kosh init`: the project's dependencies installed with the package manager that ran the installer, the API
|
|
3
|
+
* and the back-office started, each waited for until it answers, and the back-office opened in the default browser.
|
|
4
|
+
* Both keep running in this terminal until Ctrl+C, which stops them together. Their output is kept, and shown when
|
|
5
|
+
* one of them fails to start.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
8
|
+
/** The package manager that ran the CLI (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`), from its user agent; npm otherwise. */
|
|
9
|
+
export function packageManager(env = process.env) {
|
|
10
|
+
const agent = env['npm_config_user_agent'] ?? '';
|
|
11
|
+
for (const pm of ['pnpm', 'yarn', 'bun'])
|
|
12
|
+
if (agent.startsWith(`${pm}/`))
|
|
13
|
+
return pm;
|
|
14
|
+
return 'npm';
|
|
15
|
+
}
|
|
16
|
+
/** The command that opens `url` in the default browser on `platform`. */
|
|
17
|
+
export function browserCommand(url, platform = process.platform) {
|
|
18
|
+
if (platform === 'win32')
|
|
19
|
+
return ['cmd', ['/c', 'start', '""', url]];
|
|
20
|
+
if (platform === 'darwin')
|
|
21
|
+
return ['open', [url]];
|
|
22
|
+
return ['xdg-open', [url]];
|
|
23
|
+
}
|
|
24
|
+
const windows = process.platform === 'win32';
|
|
25
|
+
/** A child process running `pm args` in `cwd`, its output kept (the last 200 lines). */
|
|
26
|
+
function start(pm, args, cwd) {
|
|
27
|
+
const child = spawn(pm, args, {
|
|
28
|
+
cwd,
|
|
29
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
30
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
+
// npm, pnpm… are .cmd scripts on Windows; elsewhere a process group, stopped as a whole.
|
|
32
|
+
shell: windows,
|
|
33
|
+
detached: !windows,
|
|
34
|
+
});
|
|
35
|
+
const lines = [];
|
|
36
|
+
const keep = (chunk) => {
|
|
37
|
+
lines.push(...chunk
|
|
38
|
+
.toString()
|
|
39
|
+
.split(/\r?\n/)
|
|
40
|
+
.filter((l) => l.trim() !== ''));
|
|
41
|
+
lines.splice(0, Math.max(0, lines.length - 200));
|
|
42
|
+
};
|
|
43
|
+
child.stdout?.on('data', keep);
|
|
44
|
+
child.stderr?.on('data', keep);
|
|
45
|
+
const exited = new Promise((done) => child.on('close', (code) => done(code ?? 1)));
|
|
46
|
+
return { child, lines, exited };
|
|
47
|
+
}
|
|
48
|
+
/** Stops a started process and what it started. */
|
|
49
|
+
function stop(child) {
|
|
50
|
+
if (child.pid === undefined || child.exitCode !== null)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
if (windows)
|
|
54
|
+
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
55
|
+
else
|
|
56
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// already gone
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Resolves when `url` answers (any HTTP status below 500); rejects when `exited` settles first or on timeout. */
|
|
63
|
+
async function answers(url, exited, seconds) {
|
|
64
|
+
let gone = null;
|
|
65
|
+
void exited.then((code) => {
|
|
66
|
+
gone = code;
|
|
67
|
+
});
|
|
68
|
+
for (let i = 0; i < seconds * 2; i++) {
|
|
69
|
+
if (gone !== null)
|
|
70
|
+
throw new Error(`it stopped (exit code ${gone})`);
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
73
|
+
if (res.status < 500)
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// not yet
|
|
78
|
+
}
|
|
79
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`no answer at ${url} within ${seconds}s`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Installs, starts and opens the installation; returns when the user stops it (Ctrl+C) or a server stops by itself.
|
|
85
|
+
*/
|
|
86
|
+
export async function launch(o, ui) {
|
|
87
|
+
const pm = packageManager();
|
|
88
|
+
await ui.task(`Installing the dependencies (${pm} install)`, async (task) => {
|
|
89
|
+
task.update('the first run downloads them');
|
|
90
|
+
const run = start(pm, ['install'], o.dir);
|
|
91
|
+
const code = await run.exited;
|
|
92
|
+
if (code !== 0)
|
|
93
|
+
throw new Error(`${pm} install failed:\n${run.lines.slice(-15).join('\n')}`);
|
|
94
|
+
});
|
|
95
|
+
const api = start(pm, ['run', 'start'], o.dir);
|
|
96
|
+
const web = start(pm, ['run', 'web'], o.dir);
|
|
97
|
+
const stopAll = () => {
|
|
98
|
+
stop(api.child);
|
|
99
|
+
stop(web.child);
|
|
100
|
+
};
|
|
101
|
+
process.once('exit', stopAll);
|
|
102
|
+
const failed = (name, run) => (error) => {
|
|
103
|
+
stopAll();
|
|
104
|
+
const tail = run.lines.slice(-15).join('\n');
|
|
105
|
+
throw new Error(`The ${name} did not start: ${error instanceof Error ? error.message : String(error)}${tail === '' ? '' : `\n${tail}`}`);
|
|
106
|
+
};
|
|
107
|
+
const apiUrl = `http://localhost:${o.port}`;
|
|
108
|
+
const webUrl = `http://localhost:${o.webPort}`;
|
|
109
|
+
await ui
|
|
110
|
+
.task(`Starting the API on ${apiUrl}`, () => answers(`${apiUrl}/health/ready`, api.exited, 90))
|
|
111
|
+
.catch(failed('API', api));
|
|
112
|
+
await ui
|
|
113
|
+
.task(`Starting the back-office on ${webUrl}`, () => answers(webUrl, web.exited, 90))
|
|
114
|
+
.catch(failed('back-office', web));
|
|
115
|
+
const [command, args] = browserCommand(webUrl);
|
|
116
|
+
const opened = spawn(command, args, { stdio: 'ignore', detached: true, shell: false });
|
|
117
|
+
opened.on('error', () => ui.info(`Open ${webUrl} in your browser.`));
|
|
118
|
+
opened.unref();
|
|
119
|
+
ui.ok(`Opened ${ui.style('cyan', webUrl)} in your browser · sign in as "${o.login}"`);
|
|
120
|
+
ui.outro(`${ui.style('green', 'Kosh is running')} ${ui.style('gray', `back-office ${webUrl} · API ${apiUrl} · Ctrl+C stops both`)}`);
|
|
121
|
+
// Until Ctrl+C, or until one of them stops by itself (its last lines are shown).
|
|
122
|
+
const which = await Promise.race([
|
|
123
|
+
new Promise((done) => {
|
|
124
|
+
process.once('SIGINT', () => done('stopped'));
|
|
125
|
+
process.once('SIGTERM', () => done('stopped'));
|
|
126
|
+
}),
|
|
127
|
+
api.exited.then(() => 'api'),
|
|
128
|
+
web.exited.then(() => 'web'),
|
|
129
|
+
]);
|
|
130
|
+
stopAll();
|
|
131
|
+
if (which === 'stopped') {
|
|
132
|
+
ui.write(`\n ${ui.style('gray', `Stopped. Start again with: ${pm} start and ${pm} run web`)}\n`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const run = which === 'api' ? api : web;
|
|
136
|
+
ui.fail(`The ${which === 'api' ? 'API' : 'back-office'} stopped:`);
|
|
137
|
+
ui.write(`${run.lines.slice(-15).join('\n')}\n`);
|
|
138
|
+
}
|
package/dist/cli/prompt.d.ts
CHANGED
|
@@ -7,11 +7,46 @@ export interface Question {
|
|
|
7
7
|
/** An error message for an invalid answer (asked again), or `null`. */
|
|
8
8
|
readonly validate?: (value: string) => string | null;
|
|
9
9
|
}
|
|
10
|
+
/** One option of a choice: what is answered, what is shown, an optional hint beside it. */
|
|
11
|
+
export interface Choice {
|
|
12
|
+
readonly value: string;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly hint?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SelectQuestion {
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly choices: readonly Choice[];
|
|
19
|
+
/** The value chosen when Enter is pressed at once. */
|
|
20
|
+
readonly default?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MultiQuestion {
|
|
23
|
+
readonly message: string;
|
|
24
|
+
readonly choices: readonly Choice[];
|
|
25
|
+
/** The values ticked to begin with. */
|
|
26
|
+
readonly defaults?: readonly string[];
|
|
27
|
+
/** At least one. */
|
|
28
|
+
readonly required?: boolean;
|
|
29
|
+
}
|
|
10
30
|
export interface Prompter {
|
|
11
31
|
ask(question: Question): Promise<string>;
|
|
12
32
|
confirm(message: string, fallback: boolean): Promise<boolean>;
|
|
33
|
+
/** One of several (a list chosen with the keyboard); without it, {@link choose} asks a question instead. */
|
|
34
|
+
select?(question: SelectQuestion): Promise<string>;
|
|
35
|
+
/** Several of several; without it, {@link chooseMany} asks for a comma-separated list. */
|
|
36
|
+
multiselect?(question: MultiQuestion): Promise<string[]>;
|
|
13
37
|
close(): void;
|
|
14
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* One of `choices`: the prompter's list when it has one, otherwise the question `fallback` (pipes, scripts, tests),
|
|
41
|
+
* answered with an option's number, value or label.
|
|
42
|
+
*/
|
|
43
|
+
export declare function choose(prompter: Prompter, q: SelectQuestion & {
|
|
44
|
+
readonly fallback: string;
|
|
45
|
+
}): Promise<string>;
|
|
46
|
+
/** Several of `choices`: the prompter's list, or a comma-separated answer to `fallback`. */
|
|
47
|
+
export declare function chooseMany(prompter: Prompter, q: MultiQuestion & {
|
|
48
|
+
readonly fallback: string;
|
|
49
|
+
}): Promise<string[]>;
|
|
15
50
|
/** Thrown when the input ends before every question is answered (piped answers ran out, Ctrl+D). */
|
|
16
51
|
export declare class InputEndedError extends Error {
|
|
17
52
|
constructor(question: string);
|