shutro-erp 0.2.1 → 0.3.1
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 +74 -67
- 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 +1380 -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 +172 -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/assistant/routes.js +1 -1
- package/dist/automation/engine.js +1 -1
- package/dist/automation/web/messages.js +2 -2
- package/dist/cli/apps.d.ts +1 -1
- package/dist/cli/apps.js +14 -14
- package/dist/cli/bin.js +3 -0
- package/dist/cli/index.js +11 -11
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.js +224 -78
- package/dist/cli/launch.d.ts +16 -0
- package/dist/cli/launch.js +138 -0
- package/dist/cli/project.d.ts +7 -7
- package/dist/cli/project.js +25 -25
- package/dist/cli/prompt.d.ts +35 -0
- package/dist/cli/prompt.js +48 -2
- package/dist/cli/templates.d.ts +14 -6
- package/dist/cli/templates.js +61 -34
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.d.ts +21 -8
- package/dist/cli/ui.js +65 -19
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +33 -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 +1311 -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 +1373 -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/mail/index.js +2 -2
- 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/loader.js +2 -2
- 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 +7 -3
- package/dist/notify/models.d.ts +1 -1
- package/dist/notify/models.js +2 -0
- package/dist/notify/routes.js +1 -1
- package/dist/notify/web/messages.js +5 -1
- package/dist/portal/.kosh/bindings.d.ts +22 -0
- package/dist/portal/.kosh/bindings.js +15 -0
- package/dist/portal/.kosh/schema.d.ts +376 -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 +42 -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 +33 -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 +701 -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 +45 -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 +174 -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 +852 -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 +174 -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 +766 -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 +45 -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 +45 -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/api/client.js +1 -1
- package/dist/server/api/openapi.js +17 -1
- package/dist/server/apps.js +5 -5
- package/dist/server/auth/api-keys.d.ts +13 -0
- package/dist/server/auth/api-keys.js +65 -0
- package/dist/server/auth/base.js +26 -0
- package/dist/server/auth/crypto.js +1 -1
- package/dist/server/auth/scopes.d.ts +12 -0
- package/dist/server/auth/scopes.js +51 -0
- package/dist/server/auth/service.d.ts +59 -1
- package/dist/server/auth/service.js +236 -6
- package/dist/server/demo.d.ts +1 -1
- package/dist/server/demo.js +1 -1
- package/dist/server/env-names.d.ts +2 -0
- package/dist/server/env-names.js +16 -0
- package/dist/server/host.js +5 -1
- package/dist/server/identity.d.ts +7 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/registry.js +1 -1
- package/dist/server/server.d.ts +5 -0
- package/dist/server/server.js +71 -6
- package/dist/settings/routes.js +1 -1
- package/dist/settings/web/ApiKeysPage.d.ts +8 -0
- package/dist/settings/web/ApiKeysPage.js +70 -0
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +4 -0
- package/dist/settings/web/messages.js +62 -0
- package/dist/storage/routes.js +1 -1
- package/dist/studio/build.js +1 -1
- package/dist/studio/generate.js +4 -4
- package/dist/studio/module.js +5 -5
- package/dist/studio/web/messages.js +2 -2
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.js +27 -4
- package/dist/ui/context.d.ts +3 -1
- package/dist/ui/context.js +2 -2
- package/dist/ui/i18n/messages.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/pwa/worker.js +2 -2
- package/dist/ui/registry.d.ts +15 -0
- package/dist/ui/registry.js +20 -0
- package/dist/ui/views/FormView.js +19 -6
- package/package.json +52 -4
package/dist/cli/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
2
|
+
* `shutro-erp init`: sets up an installation in a folder. It asks for (or takes as flags) the database, the first company,
|
|
3
3
|
* the administrator, the apps, file storage, Valkey and the port; checks the database (and creates it when it does
|
|
4
4
|
* not exist yet); writes the starter project and `.env`; plans and applies the first migration; installs the company,
|
|
5
5
|
* the administrator and each app's default data; and enrols the administrator's authenticator app (administrator
|
|
@@ -9,17 +9,19 @@
|
|
|
9
9
|
import { spawn, spawnSync } from 'node:child_process';
|
|
10
10
|
import { randomBytes } from 'node:crypto';
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
-
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { chmod, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
13
|
import { createServer as createNetServer } from 'node:net';
|
|
14
14
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { parseArgs } from 'node:util';
|
|
17
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';
|
|
18
19
|
import { seed } from './project.js';
|
|
19
|
-
import { defaultsPrompter, terminalPrompter } from './prompt.js';
|
|
20
|
+
import { choose, chooseMany, defaultsPrompter, terminalPrompter } from './prompt.js';
|
|
20
21
|
import * as templates from './templates.js';
|
|
21
|
-
import { APPS, bridgesFor, SELECTABLE_APPS } from './templates.js';
|
|
22
|
-
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';
|
|
23
25
|
/**
|
|
24
26
|
* The version of the `shutro-erp` package this CLI runs from: new projects depend on it. In the workspace (no such
|
|
25
27
|
* package around the CLI) the framework version stands in.
|
|
@@ -37,28 +39,33 @@ export function releaseVersion(from = dirname(fileURLToPath(import.meta.url))) {
|
|
|
37
39
|
}
|
|
38
40
|
return KOSH_VERSION;
|
|
39
41
|
}
|
|
40
|
-
export const INIT_USAGE = `Usage:
|
|
42
|
+
export const INIT_USAGE = `Usage: shutro-erp init [folder] [options]
|
|
41
43
|
|
|
42
|
-
Sets up a
|
|
44
|
+
Sets up a Shutro ERP installation: asks for the database, the first company, the administrator, the apps,
|
|
43
45
|
file storage and the port (Enter keeps the default), then writes the project, creates the database
|
|
44
46
|
tables and the administrator, and enrols the administrator's authenticator app.
|
|
45
47
|
|
|
46
48
|
Options (each also answers its question; with --yes the rest take their defaults):
|
|
47
|
-
--database-url <url> PostgreSQL, e.g. postgres://user:pass@localhost:5432/
|
|
49
|
+
--database-url <url> PostgreSQL, e.g. postgres://user:pass@localhost:5432/my_erp
|
|
48
50
|
--docker-db start PostgreSQL in Docker (compose.yaml) instead
|
|
49
51
|
--create-database create the database when it does not exist
|
|
50
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)
|
|
51
55
|
--lang <en|bn> language --tz <zone> time zone (IANA)
|
|
52
56
|
--admin-login <login> --admin-name <name> --admin-email <email>
|
|
53
|
-
--admin-password <pw> (or
|
|
57
|
+
--admin-password <pw> (or SHUTRO_ERP_ADMIN_PASSWORD; at least 12 characters)
|
|
54
58
|
--apps <list> comma-separated: ${SELECTABLE_APPS.join(', ')} (default: all; bridges come with the apps they connect)
|
|
55
59
|
--storage-dir <dir> attachments in a folder (default ./var/attachments)
|
|
56
60
|
--s3-endpoint <url> --s3-bucket <name> --s3-access-key <key> --s3-secret-key <secret> [--s3-region <r>]
|
|
57
61
|
--valkey-url <url> optional: rate limits and caches shared across servers
|
|
58
|
-
--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)
|
|
59
64
|
--demo load demo data: a sample company with contacts, products, orders, invoices and
|
|
60
65
|
projects, made by demo users (dina, rahim, karim, sumi; one shared password)
|
|
61
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)
|
|
62
69
|
--yes do not ask; use flags and defaults
|
|
63
70
|
`;
|
|
64
71
|
const dockerAvailable = () => spawnSync('docker', ['version', '--format', '{{.Server.Version}}'], { encoding: 'utf8' })
|
|
@@ -134,7 +141,7 @@ const required = (what) => (v) => (v.trim() === '' ? `${what} is required.` : nu
|
|
|
134
141
|
/** Opens the database; offers to create it when it does not exist. Returns the PostgreSQL version. */
|
|
135
142
|
async function checkDatabase(url, create) {
|
|
136
143
|
const probe = async (target) => {
|
|
137
|
-
const driver = pg.createDriver({ url: target, max: 1, applicationName: '
|
|
144
|
+
const driver = pg.createDriver({ url: target, max: 1, applicationName: 'shutro-erp init' });
|
|
138
145
|
try {
|
|
139
146
|
const [row] = await driver.query({
|
|
140
147
|
text: 'SHOW server_version',
|
|
@@ -164,7 +171,7 @@ async function checkDatabase(url, create) {
|
|
|
164
171
|
const admin = pg.createDriver({
|
|
165
172
|
url: parsed.toString(),
|
|
166
173
|
max: 1,
|
|
167
|
-
applicationName: '
|
|
174
|
+
applicationName: 'shutro-erp init',
|
|
168
175
|
});
|
|
169
176
|
try {
|
|
170
177
|
await admin.query({ text: `CREATE DATABASE "${name}"`, params: [] });
|
|
@@ -212,10 +219,10 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
212
219
|
const dir = resolve(cwd, positional ?? (await q(null, 'Project folder', '.')));
|
|
213
220
|
const name = basename(dir)
|
|
214
221
|
.toLowerCase()
|
|
215
|
-
.replace(/[^a-z0-9-]+/g, '-') || '
|
|
222
|
+
.replace(/[^a-z0-9-]+/g, '-') || 'shutro-erp-app';
|
|
216
223
|
ui.step('database', 'Database', 'PostgreSQL 16 or later: a server you have, or one in Docker');
|
|
217
224
|
const postgres = {
|
|
218
|
-
user: '
|
|
225
|
+
user: 'shutro',
|
|
219
226
|
password: randomBytes(18)
|
|
220
227
|
.toString('base64url')
|
|
221
228
|
.replace(/[^A-Za-z0-9]/g, '')
|
|
@@ -228,9 +235,19 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
228
235
|
if (flag('database-url') === undefined) {
|
|
229
236
|
const docker = flags['docker-db'] === true ||
|
|
230
237
|
(dockerAvailable() && flags['yes'] !== true
|
|
231
|
-
? (await
|
|
232
|
-
|
|
233
|
-
|
|
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'
|
|
234
251
|
: false);
|
|
235
252
|
if (docker) {
|
|
236
253
|
if (!dockerAvailable())
|
|
@@ -312,9 +329,41 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
312
329
|
const currency = (await q('currency', 'Currency (ISO code)', 'BDT', {
|
|
313
330
|
validate: (v) => (/^[A-Za-z]{3}$/.test(v) ? null : 'Three letters, e.g. BDT, USD, EUR'),
|
|
314
331
|
})).toUpperCase();
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
}));
|
|
318
367
|
const tz = await q('tz', 'Time zone', Intl.DateTimeFormat().resolvedOptions().timeZone, {
|
|
319
368
|
validate: (v) => {
|
|
320
369
|
try {
|
|
@@ -355,36 +404,43 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
355
404
|
ui.warn('The passwords differ; try again.');
|
|
356
405
|
}
|
|
357
406
|
ui.step('apps', 'Apps', 'All are selected; remove the ones you do not need');
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
.map((
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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))];
|
|
379
429
|
const demo = flags['demo'] === true ||
|
|
380
430
|
(await prompter.confirm(' Load demo data (a sample company with orders, invoices and projects; not for production)?', false));
|
|
381
431
|
ui.step('storage', 'File storage', 'Where attachments and generated documents are kept');
|
|
382
432
|
let storage;
|
|
383
433
|
const s3 = flag('s3-endpoint') !== undefined ||
|
|
384
434
|
(flag('storage-dir') === undefined &&
|
|
385
|
-
(await
|
|
386
|
-
|
|
387
|
-
|
|
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');
|
|
388
444
|
if (s3)
|
|
389
445
|
storage = {
|
|
390
446
|
kind: 's3',
|
|
@@ -407,17 +463,20 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
407
463
|
};
|
|
408
464
|
else
|
|
409
465
|
storage = { kind: 'dir', dir: await q('storage-dir', 'Folder', './var/attachments') };
|
|
410
|
-
ui.step('server', 'Server', 'The API
|
|
411
|
-
const
|
|
412
|
-
const port = Number(await q('port', 'API port',
|
|
413
|
-
|
|
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),
|
|
414
471
|
}));
|
|
472
|
+
const valkeyUrl = await q('valkey-url', 'Valkey/Redis URL (optional, Enter to skip)', '');
|
|
415
473
|
return {
|
|
416
474
|
dir,
|
|
417
475
|
name,
|
|
418
476
|
databaseUrl,
|
|
419
477
|
company,
|
|
420
478
|
currency,
|
|
479
|
+
country,
|
|
421
480
|
lang,
|
|
422
481
|
tz,
|
|
423
482
|
admin: { login, name: adminName, email, password: password },
|
|
@@ -426,6 +485,7 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
426
485
|
storage,
|
|
427
486
|
valkeyUrl,
|
|
428
487
|
port,
|
|
488
|
+
webPort,
|
|
429
489
|
postgres,
|
|
430
490
|
dockerDb,
|
|
431
491
|
vapid: (await import('../notify/index.js')).generateVapidKeys(),
|
|
@@ -444,6 +504,7 @@ async function modulesFor(apps) {
|
|
|
444
504
|
(await import('../mail/index.js')).mail,
|
|
445
505
|
(await import('../automation/index.js')).automation,
|
|
446
506
|
(await import('../studio/index.js')).studio,
|
|
507
|
+
(await import('../import/index.js')).dataImport,
|
|
447
508
|
];
|
|
448
509
|
for (const app of apps) {
|
|
449
510
|
if (app === 'permissions')
|
|
@@ -471,22 +532,26 @@ async function modulesFor(apps) {
|
|
|
471
532
|
modules.push((await import('../project/index.js')).project);
|
|
472
533
|
if (app === 'sale_project')
|
|
473
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);
|
|
474
539
|
}
|
|
475
540
|
return modules;
|
|
476
541
|
}
|
|
477
542
|
function envFile(a) {
|
|
478
543
|
const lines = [
|
|
479
|
-
'#
|
|
544
|
+
'# Shutro ERP settings (shutro-erp init). Keep this file out of version control: it holds credentials.',
|
|
480
545
|
`DATABASE_URL=${a.databaseUrl}`,
|
|
481
546
|
`PORT=${a.port}`,
|
|
482
547
|
];
|
|
483
548
|
if (a.storage.kind === 'dir')
|
|
484
|
-
lines.push(`
|
|
549
|
+
lines.push(`SHUTRO_ERP_STORAGE_DIR=${a.storage.dir}`);
|
|
485
550
|
else
|
|
486
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}`);
|
|
487
552
|
if (a.valkeyUrl !== '')
|
|
488
553
|
lines.push(`VALKEY_URL=${a.valkeyUrl}`);
|
|
489
|
-
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).', `SHUTRO_ERP_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=Shutro ERP <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', '', '# Shutro ERP Studio (RFC 0012): propose (default: changes become patches) or write (development, one server: Studio', '# writes src/modules/<name>/ and applies at once). SHUTRO_ERP_STUDIO_DIR: the project folder (default: the working one).', '# SHUTRO_ERP_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.', '# SHUTRO_ERP_STUDIO_GIT_URL=https://github.com/acme/erp', '# SHUTRO_ERP_STUDIO_GIT_TOKEN=', '# SHUTRO_ERP_STUDIO_GIT_BASE=main', '', '# The AI assistant (RFC 0013): off until an administrator switches it on in Settings → AI assistant.', '# ANTHROPIC_API_KEY=', '# SHUTRO_ERP_ASSISTANT_MODEL=claude-opus-5-5', '# SHUTRO_ERP_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 shutro-erp 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}`}`);
|
|
490
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}`);
|
|
491
556
|
return `${lines.join('\n')}\n`;
|
|
492
557
|
}
|
|
@@ -496,6 +561,7 @@ async function writeProject(a, ui) {
|
|
|
496
561
|
name: a.name,
|
|
497
562
|
apps: a.apps,
|
|
498
563
|
port: a.port,
|
|
564
|
+
webPort: a.webPort,
|
|
499
565
|
koshVersion: releaseVersion(),
|
|
500
566
|
dockerDb: a.postgres.db,
|
|
501
567
|
fileStorage: a.storage.kind === 'dir',
|
|
@@ -552,15 +618,37 @@ async function setUp(server, a, ui) {
|
|
|
552
618
|
text: "SELECT to_regclass('public.res_users')::text AS found",
|
|
553
619
|
params: [],
|
|
554
620
|
});
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
621
|
+
// A database another project set up keeps that project's migration history: this folder cannot continue it.
|
|
622
|
+
const status = await migrate.migrationStatus({ driver: server.driver, dir });
|
|
623
|
+
const here = new Set((await readdir(dir).catch(() => []))
|
|
624
|
+
.map((f) => /^(\d+)_/.exec(f)?.[1])
|
|
625
|
+
.filter(Boolean));
|
|
626
|
+
const foreign = status.applied.filter((m) => !here.has(m.version));
|
|
627
|
+
if (foreign.length > 0)
|
|
628
|
+
throw new Error(`The database "${new URL(a.databaseUrl).pathname.slice(1)}" belongs to another installation: its migrations ` +
|
|
629
|
+
`(${foreign.map((m) => `${m.version}_${m.name}`).join(', ')}) are not in this project's migrations folder. ` +
|
|
630
|
+
'Choose a new database for this project, or run shutro-erp init (or migrate) in the folder of the ' +
|
|
631
|
+
'project that created it.');
|
|
632
|
+
// Always what the database lacks: a new one gets every table; one made by an earlier version (or another
|
|
633
|
+
// project's choice of apps) gets the tables and columns these apps add, before their data migrations run.
|
|
634
|
+
const fresh = installed?.found === null || installed?.found === undefined;
|
|
635
|
+
task.update('planning');
|
|
636
|
+
const plan = await migrate.planMigration({
|
|
637
|
+
driver: server.driver,
|
|
638
|
+
registry: server.registry.registry,
|
|
639
|
+
ignore: FRAMEWORK_TABLES, // the framework's own tables, not a model's: never dropped
|
|
640
|
+
});
|
|
641
|
+
if (plan.destructive.length > 0)
|
|
642
|
+
throw new Error(`The database "${new URL(a.databaseUrl).pathname.slice(1)}" has data these apps would drop or change ` +
|
|
643
|
+
`(${plan.destructive.map((op) => migrate.renderOperation(op)).join('; ')}). Use a new database, or ` +
|
|
644
|
+
'review the change and run shutro-erp migrate --allow-destructive in the project first.');
|
|
645
|
+
if (plan.statements.length > 0) {
|
|
646
|
+
const file = await migrate.writeMigrationFile({
|
|
647
|
+
dir,
|
|
648
|
+
name: fresh ? 'init' : 'update',
|
|
649
|
+
plan,
|
|
561
650
|
});
|
|
562
|
-
|
|
563
|
-
ui.detail(`planned ${plan.statements.length} statements in migrations/${basename(file.path)}`);
|
|
651
|
+
ui.detail(`planned ${plan.statements.length} statements in migrations/${basename(file.path)}${fresh ? '' : ' (the database had an earlier version)'}`);
|
|
564
652
|
}
|
|
565
653
|
task.update('applying');
|
|
566
654
|
const result = await migrate.applyMigrations({
|
|
@@ -568,8 +656,8 @@ async function setUp(server, a, ui) {
|
|
|
568
656
|
dir,
|
|
569
657
|
registry: server.registry.registry,
|
|
570
658
|
});
|
|
571
|
-
// The installed set (RFC 0008): what `
|
|
572
|
-
await recordInstalledApps(server.driver, server.registry.loaded.modules, ['base', 'settings', 'realtime', 'notify', ...a.apps], `
|
|
659
|
+
// The installed set (RFC 0008): what `shutro-erp apps` changes later.
|
|
660
|
+
await recordInstalledApps(server.driver, server.registry.loaded.modules, ['base', 'settings', 'realtime', 'notify', ...a.apps], `shutro-erp init (${a.admin.login})`);
|
|
573
661
|
task.finish(`Database tables ready ${ui.style('gray', `(applied ${result.applied.length} migration(s))`)}`);
|
|
574
662
|
});
|
|
575
663
|
const [users] = await server.driver.query({
|
|
@@ -588,7 +676,7 @@ async function setUp(server, a, ui) {
|
|
|
588
676
|
company: { name: a.company, currency: a.currency },
|
|
589
677
|
admin: { login: a.admin.login, name: a.admin.name, password: a.admin.password },
|
|
590
678
|
});
|
|
591
|
-
await server.shutro.bootstrap({ reason: '
|
|
679
|
+
await server.shutro.bootstrap({ reason: 'shutro-erp init', company }, async (env) => {
|
|
592
680
|
const model = (name) => env.model(name);
|
|
593
681
|
const lang = a.lang === 'bn' ? 'bn_BD' : 'en_US';
|
|
594
682
|
await model('res.users')
|
|
@@ -681,10 +769,10 @@ async function enrol(server, a, prompter, ui) {
|
|
|
681
769
|
ui.fail(error instanceof Error ? error.message : String(error));
|
|
682
770
|
}
|
|
683
771
|
}
|
|
684
|
-
ui.warn('Not enrolled: run
|
|
772
|
+
ui.warn('Not enrolled: run shutro-erp init again later, or enrol from the API (POST /api/v1/auth/2fa/setup).');
|
|
685
773
|
return false;
|
|
686
774
|
}
|
|
687
|
-
/** Runs `
|
|
775
|
+
/** Runs `shutro-erp init`. Returns the exit code. */
|
|
688
776
|
export async function runInit(argv, io) {
|
|
689
777
|
const { values, positionals } = parseArgs({
|
|
690
778
|
args: [...argv],
|
|
@@ -694,6 +782,7 @@ export async function runInit(argv, io) {
|
|
|
694
782
|
'create-database': { type: 'boolean' },
|
|
695
783
|
company: { type: 'string' },
|
|
696
784
|
currency: { type: 'string' },
|
|
785
|
+
country: { type: 'string' },
|
|
697
786
|
lang: { type: 'string' },
|
|
698
787
|
tz: { type: 'string' },
|
|
699
788
|
'admin-login': { type: 'string' },
|
|
@@ -709,7 +798,9 @@ export async function runInit(argv, io) {
|
|
|
709
798
|
's3-region': { type: 'string' },
|
|
710
799
|
'valkey-url': { type: 'string' },
|
|
711
800
|
port: { type: 'string' },
|
|
801
|
+
'web-port': { type: 'string' },
|
|
712
802
|
'no-2fa': { type: 'boolean' },
|
|
803
|
+
'no-start': { type: 'boolean' },
|
|
713
804
|
demo: { type: 'boolean' },
|
|
714
805
|
'docker-db': { type: 'boolean' },
|
|
715
806
|
yes: { type: 'boolean', short: 'y' },
|
|
@@ -723,15 +814,34 @@ export async function runInit(argv, io) {
|
|
|
723
814
|
const terminal = io.terminal ?? PLAIN_TERMINAL;
|
|
724
815
|
const interactive2fa = values['no-2fa'] !== true && values.yes !== true;
|
|
725
816
|
// Six question steps, then the project files and the database, then (interactively) the second factor.
|
|
726
|
-
|
|
817
|
+
// At the end, in a terminal: install, start and open the installation (not for scripts, CI and tests).
|
|
818
|
+
const autoStart = values['no-start'] !== true && io.prompter === undefined && process.stdout.isTTY === true;
|
|
819
|
+
const ui = createUi(io.write, terminal, {
|
|
820
|
+
steps: (interactive2fa ? 9 : 8) + (autoStart ? 1 : 0),
|
|
821
|
+
});
|
|
822
|
+
const interactive = io.prompter === undefined &&
|
|
823
|
+
values.yes !== true &&
|
|
824
|
+
process.stdin.isTTY === true &&
|
|
825
|
+
railed(terminal);
|
|
727
826
|
const asked = io.prompter ??
|
|
728
827
|
(values.yes === true
|
|
729
828
|
? defaultsPrompter()
|
|
730
|
-
:
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
829
|
+
: interactive
|
|
830
|
+
? interactivePrompter(process.stdin, process.stdout, terminal)
|
|
831
|
+
: terminalPrompter(process.stdin, process.stdout, {
|
|
832
|
+
marker: ui.style('cyan', '? '),
|
|
833
|
+
fallback: (t) => ui.style('gray', t),
|
|
834
|
+
problem: (t) => ui.style('yellow', `${terminal.unicode ? '▲' : '!'} ${t}`),
|
|
835
|
+
}));
|
|
836
|
+
const paused = async (work) => {
|
|
837
|
+
ui.pause();
|
|
838
|
+
try {
|
|
839
|
+
return await work();
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
ui.resume();
|
|
843
|
+
}
|
|
844
|
+
};
|
|
735
845
|
// A question stops the spinner while it is asked (the prompter writes to the terminal itself).
|
|
736
846
|
const prompter = {
|
|
737
847
|
async ask(q) {
|
|
@@ -752,13 +862,23 @@ export async function runInit(argv, io) {
|
|
|
752
862
|
ui.resume();
|
|
753
863
|
}
|
|
754
864
|
},
|
|
865
|
+
...(asked.select === undefined
|
|
866
|
+
? {}
|
|
867
|
+
: { select: (q) => paused(() => asked.select(q)) }),
|
|
868
|
+
...(asked.multiselect === undefined
|
|
869
|
+
? {}
|
|
870
|
+
: {
|
|
871
|
+
multiselect: (q) => paused(() => asked.multiselect(q)),
|
|
872
|
+
}),
|
|
755
873
|
close: () => asked.close(),
|
|
756
874
|
};
|
|
757
875
|
let server;
|
|
758
876
|
const started = Date.now();
|
|
759
877
|
try {
|
|
760
|
-
await ui.banner(`
|
|
761
|
-
ui.
|
|
878
|
+
await ui.banner(`ERP · business software on the Shutro ORM · v${releaseVersion()}`);
|
|
879
|
+
ui.intro('Shutro ERP setup', interactive
|
|
880
|
+
? 'Enter keeps the default · ↑ ↓ choose · Space ticks · Ctrl+C quits'
|
|
881
|
+
: 'Shutro ERP setup: press Enter to keep the value in [brackets].');
|
|
762
882
|
const answers = await ask(prompter, values, io.cwd, positionals[0], ui);
|
|
763
883
|
ui.step('files', 'Project files', answers.dir);
|
|
764
884
|
await ui.task('Writing the project', async () => {
|
|
@@ -801,9 +921,12 @@ export async function runInit(argv, io) {
|
|
|
801
921
|
enrolled = await enrol(live, answers, prompter, ui);
|
|
802
922
|
}
|
|
803
923
|
else if (values.yes === true && values['no-2fa'] !== true)
|
|
804
|
-
ui.info('The authenticator app is enrolled interactively: run npx
|
|
924
|
+
ui.info('The authenticator app is enrolled interactively: run npx shutro-erp init again (without --yes).');
|
|
805
925
|
const rows = [
|
|
806
|
-
[
|
|
926
|
+
[
|
|
927
|
+
'Company',
|
|
928
|
+
`${answers.company} (${answers.currency}${answers.country === '' ? '' : `, ${answers.country}`})`,
|
|
929
|
+
],
|
|
807
930
|
[
|
|
808
931
|
'Administrator',
|
|
809
932
|
`${answers.admin.login} ${ui.style(enrolled ? 'green' : 'yellow', enrolled ? '· 2FA on' : '· 2FA pending')}`,
|
|
@@ -818,6 +941,7 @@ export async function runInit(argv, io) {
|
|
|
818
941
|
'mail',
|
|
819
942
|
'automation',
|
|
820
943
|
'studio',
|
|
944
|
+
'import',
|
|
821
945
|
...answers.apps,
|
|
822
946
|
].join(', '),
|
|
823
947
|
],
|
|
@@ -825,22 +949,44 @@ export async function runInit(argv, io) {
|
|
|
825
949
|
? [['Demo users', `dina, rahim, karim, sumi · password "${DEMO_PASSWORD}"`]]
|
|
826
950
|
: []),
|
|
827
951
|
['Database', answers.dockerDb ? `Docker (port ${answers.postgres.port})` : 'your server'],
|
|
952
|
+
['Back-office', `http://localhost:${answers.webPort}`],
|
|
828
953
|
['API', `http://localhost:${answers.port}`],
|
|
829
954
|
['Took', `${((Date.now() - started) / 1000).toFixed(1)}s`],
|
|
830
955
|
];
|
|
831
|
-
|
|
956
|
+
if (!autoStart)
|
|
957
|
+
ui.outro(ui.style('green', 'Shutro ERP is set up'));
|
|
958
|
+
ui.card(`${terminal.emoji ? '🚀 ' : ''}Shutro ERP is set up`, rows, enrolled || values['no-2fa'] === true ? 'green' : 'yellow');
|
|
959
|
+
if (autoStart) {
|
|
960
|
+
if (!enrolled)
|
|
961
|
+
ui.warn('Administrator rights need a second factor: run `shutro-erp init` again to enrol the authenticator app.');
|
|
962
|
+
ui.step('rocket', 'Starting Shutro ERP', 'Dependencies, the API, the back-office, then your browser');
|
|
963
|
+
// The installer's own connection is not needed any more: the API has its own.
|
|
964
|
+
await server?.close();
|
|
965
|
+
server = undefined;
|
|
966
|
+
await launch({
|
|
967
|
+
dir: answers.dir,
|
|
968
|
+
port: answers.port,
|
|
969
|
+
webPort: answers.webPort,
|
|
970
|
+
login: answers.admin.login,
|
|
971
|
+
}, ui);
|
|
972
|
+
return 0;
|
|
973
|
+
}
|
|
832
974
|
const relative = answers.dir === resolve(io.cwd) ? '' : `cd ${positionals[0] ?? answers.dir}\n `;
|
|
833
975
|
const cmd = (c) => ui.style('cyan', c);
|
|
834
976
|
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` +
|
|
835
|
-
` ${cmd('npm run web')} ${ui.style('gray',
|
|
977
|
+
` ${cmd('npm run web')} ${ui.style('gray', `# the back-office on http://localhost:${answers.webPort}`)}\n\n` +
|
|
836
978
|
` Sign in as "${answers.admin.login}"${enrolled ? ' with your password and a code from the authenticator app' : ''}.\n`);
|
|
837
979
|
if (!enrolled)
|
|
838
|
-
ui.warn('Administrator rights need a second factor: run `
|
|
980
|
+
ui.warn('Administrator rights need a second factor: run `shutro-erp init` again to enrol the authenticator app.');
|
|
839
981
|
return 0;
|
|
840
982
|
}
|
|
841
983
|
catch (error) {
|
|
984
|
+
if (error instanceof CancelledError) {
|
|
985
|
+
ui.outro(ui.style('red', 'Setup cancelled: nothing more was done.'));
|
|
986
|
+
return 130;
|
|
987
|
+
}
|
|
842
988
|
ui.write('\n');
|
|
843
|
-
ui.fail(`
|
|
989
|
+
ui.fail(`shutro-erp init stopped: ${error instanceof Error ? error.message : String(error)}`);
|
|
844
990
|
return 1;
|
|
845
991
|
}
|
|
846
992
|
finally {
|
|
@@ -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>;
|