shutro-erp 0.3.0 → 0.3.2
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 +58 -58
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +31 -1
- package/dist/account_portal/index.js +3 -2
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +31 -1
- package/dist/accounting/index.js +4 -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 +60 -36
- package/dist/cli/launch.js +10 -7
- package/dist/cli/project.d.ts +7 -7
- package/dist/cli/project.js +25 -25
- package/dist/cli/prompt.js +2 -2
- package/dist/cli/templates.d.ts +3 -3
- package/dist/cli/templates.js +33 -28
- package/dist/cli/ui.d.ts +1 -8
- package/dist/cli/ui.js +36 -12
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +31 -1
- package/dist/integrations/inbound.d.ts +36 -0
- package/dist/integrations/inbound.js +171 -0
- package/dist/integrations/index.d.ts +18 -0
- package/dist/integrations/index.js +400 -0
- package/dist/integrations/models.d.ts +6 -0
- package/dist/integrations/models.js +119 -0
- package/dist/integrations/web/DeliveryTools.d.ts +4 -0
- package/dist/integrations/web/DeliveryTools.js +15 -0
- package/dist/integrations/web/EndpointTools.d.ts +7 -0
- package/dist/integrations/web/EndpointTools.js +25 -0
- package/dist/integrations/web/InboundTools.d.ts +4 -0
- package/dist/integrations/web/InboundTools.js +15 -0
- package/dist/integrations/web/WebhookTools.d.ts +7 -0
- package/dist/integrations/web/WebhookTools.js +25 -0
- package/dist/integrations/web/index.d.ts +16 -0
- package/dist/integrations/web/index.js +27 -0
- package/dist/integrations/web/messages.d.ts +2 -0
- package/dist/integrations/web/messages.js +53 -0
- package/dist/integrations/webhooks.d.ts +63 -0
- package/dist/integrations/webhooks.js +217 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +31 -1
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +31 -1
- package/dist/l10n_in/einvoice.js +1 -1
- package/dist/l10n_in/web/messages.js +2 -2
- package/dist/mail/index.js +2 -2
- package/dist/modules-sdk/index.d.ts +1 -1
- package/dist/modules-sdk/loader.js +2 -2
- package/dist/modules-sdk/manifest.d.ts +48 -0
- package/dist/notify/messages.js +3 -3
- package/dist/notify/routes.js +1 -1
- package/dist/notify/web/messages.js +1 -1
- package/dist/payment/.kosh/bindings.d.ts +22 -0
- package/dist/payment/.kosh/bindings.js +15 -0
- package/dist/payment/.kosh/schema.d.ts +1553 -0
- package/dist/payment/index.d.ts +34 -0
- package/dist/payment/index.js +375 -0
- package/dist/payment/module.d.ts +6853 -0
- package/dist/payment/module.js +74 -0
- package/dist/payment/providers.d.ts +64 -0
- package/dist/payment/providers.js +77 -0
- package/dist/payment/settle.d.ts +11 -0
- package/dist/payment/settle.js +59 -0
- package/dist/payment/sslcommerz.d.ts +12 -0
- package/dist/payment/sslcommerz.js +185 -0
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +31 -1
- package/dist/portal/routes-documents.js +5 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +31 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +31 -1
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +31 -1
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +31 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +31 -1
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +31 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +31 -1
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +31 -1
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +31 -1
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +31 -1
- package/dist/sales/index.js +2 -0
- package/dist/server/api/client.js +1 -1
- package/dist/server/api/openapi.js +17 -1
- package/dist/server/api/rest.d.ts +6 -1
- package/dist/server/api/rest.js +11 -0
- 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 +24 -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 +57 -1
- package/dist/server/auth/service.js +229 -5
- 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 +45 -5
- 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/app.js +26 -3
- package/dist/ui/i18n/messages.js +2 -2
- package/dist/ui/pwa/worker.js +2 -2
- package/dist/ui/registry.d.ts +15 -0
- package/dist/ui/registry.js +20 -0
- package/package.json +15 -3
package/dist/cli/index.js
CHANGED
|
@@ -21,11 +21,11 @@ export const PACKAGE = Object.freeze({
|
|
|
21
21
|
name: 'shutro-erp/cli',
|
|
22
22
|
rfc: 'RFC 0001',
|
|
23
23
|
});
|
|
24
|
-
const USAGE = `Usage:
|
|
24
|
+
const USAGE = `Usage: shutro-erp <command>
|
|
25
25
|
|
|
26
26
|
Commands (run in the installation's folder; they read .env and src/modules.ts):
|
|
27
|
-
init [folder] Set up an installation: database, company, administrator, apps (
|
|
28
|
-
apps The apps: installed, disabled, available (
|
|
27
|
+
init [folder] Set up an installation: database, company, administrator, apps (shutro-erp init --help)
|
|
28
|
+
apps The apps: installed, disabled, available (shutro-erp apps --help)
|
|
29
29
|
apps install <app …> Install apps and what they need, in one transaction
|
|
30
30
|
apps disable <app …> Switch apps off (their data stays); apps enable <app …> brings them back
|
|
31
31
|
migrate Plan schema changes into migrations/NNNN_*.sql, apply pending files, run the modules'
|
|
@@ -41,10 +41,10 @@ Commands (run in the installation's folder; they read .env and src/modules.ts):
|
|
|
41
41
|
user create <login> Create a user: --name, --email, --roles reporting.user,messaging.user, --password
|
|
42
42
|
user password <login> Set a new password
|
|
43
43
|
generate client Typed API client from the module set or an OpenAPI document
|
|
44
|
-
--modules <file|package> a module (or file) exporting
|
|
44
|
+
--modules <file|package> a module (or file) exporting Shutro ERP modules (\`modules\`, default, or each export)
|
|
45
45
|
--from <file|url> an OpenAPI document instead (a URL needs --token)
|
|
46
46
|
--token <token> Bearer token for --from <url>
|
|
47
|
-
--out <file> where to write (default: src/api/
|
|
47
|
+
--out <file> where to write (default: src/api/shutro-erp.gen.ts)
|
|
48
48
|
--check fail if the file is out of date (CI)
|
|
49
49
|
|
|
50
50
|
Every command takes --modules <file> (default src/modules.ts).
|
|
@@ -74,21 +74,21 @@ async function generate(argv, write, options) {
|
|
|
74
74
|
});
|
|
75
75
|
const cwd = options.cwd ?? process.cwd();
|
|
76
76
|
if ((values.modules === undefined) === (values.from === undefined)) {
|
|
77
|
-
write('
|
|
77
|
+
write('shutro-erp generate client: pass exactly one of --modules or --from.\n');
|
|
78
78
|
return 2;
|
|
79
79
|
}
|
|
80
80
|
const document = values.modules === undefined
|
|
81
81
|
? await documentFrom(values.from, values.token, cwd, options.fetch ?? globalThis.fetch)
|
|
82
82
|
: openApiOf(await modulesOf(values.modules, cwd));
|
|
83
83
|
const code = generateClient(document);
|
|
84
|
-
const out = resolve(cwd, values.out ?? 'src/api/
|
|
84
|
+
const out = resolve(cwd, values.out ?? 'src/api/shutro-erp.gen.ts');
|
|
85
85
|
if (values.check === true) {
|
|
86
86
|
const current = await readFile(out, 'utf8').catch(() => '');
|
|
87
87
|
if (current.replaceAll('\r\n', '\n') === code) {
|
|
88
88
|
write(`${out} is up to date.\n`);
|
|
89
89
|
return 0;
|
|
90
90
|
}
|
|
91
|
-
write(`${out} is out of date: run
|
|
91
|
+
write(`${out} is out of date: run shutro-erp generate client.\n`);
|
|
92
92
|
return 1;
|
|
93
93
|
}
|
|
94
94
|
await mkdir(dirname(out), { recursive: true });
|
|
@@ -128,7 +128,7 @@ export async function runCli(argv, write, options = {}) {
|
|
|
128
128
|
return await command(argv.slice(1), write, cwd);
|
|
129
129
|
}
|
|
130
130
|
catch (error) {
|
|
131
|
-
write(`
|
|
131
|
+
write(`shutro-erp ${argv[0]}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
132
132
|
return 1;
|
|
133
133
|
}
|
|
134
134
|
if (argv[0] === 'generate' && argv[1] === 'client') {
|
|
@@ -136,10 +136,10 @@ export async function runCli(argv, write, options = {}) {
|
|
|
136
136
|
return await generate(argv.slice(2), write, options);
|
|
137
137
|
}
|
|
138
138
|
catch (error) {
|
|
139
|
-
write(`
|
|
139
|
+
write(`shutro-erp generate client: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
140
140
|
return 1;
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
write(`Unknown command '${argv.join(' ')}'. Run
|
|
143
|
+
write(`Unknown command '${argv.join(' ')}'. Run shutro-erp --help.\n`);
|
|
144
144
|
return 2;
|
|
145
145
|
}
|
package/dist/cli/init.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ export interface InitIo {
|
|
|
14
14
|
/** What the output can show (colour, icons, spinners). Default: plain lines. */
|
|
15
15
|
readonly terminal?: Terminal;
|
|
16
16
|
}
|
|
17
|
-
/** Runs `
|
|
17
|
+
/** Runs `shutro-erp init`. Returns the exit code. */
|
|
18
18
|
export declare function runInit(argv: readonly string[], io: InitIo): Promise<number>;
|
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,7 +9,7 @@
|
|
|
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';
|
|
@@ -39,14 +39,14 @@ export function releaseVersion(from = dirname(fileURLToPath(import.meta.url))) {
|
|
|
39
39
|
}
|
|
40
40
|
return KOSH_VERSION;
|
|
41
41
|
}
|
|
42
|
-
export const INIT_USAGE = `Usage:
|
|
42
|
+
export const INIT_USAGE = `Usage: shutro-erp init [folder] [options]
|
|
43
43
|
|
|
44
|
-
Sets up a
|
|
44
|
+
Sets up a Shutro ERP installation: asks for the database, the first company, the administrator, the apps,
|
|
45
45
|
file storage and the port (Enter keeps the default), then writes the project, creates the database
|
|
46
46
|
tables and the administrator, and enrols the administrator's authenticator app.
|
|
47
47
|
|
|
48
48
|
Options (each also answers its question; with --yes the rest take their defaults):
|
|
49
|
-
--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
|
|
50
50
|
--docker-db start PostgreSQL in Docker (compose.yaml) instead
|
|
51
51
|
--create-database create the database when it does not exist
|
|
52
52
|
--company <name> the first company --currency <code> its currency (ISO 4217)
|
|
@@ -54,7 +54,7 @@ Options (each also answers its question; with --yes the rest take their defaults
|
|
|
54
54
|
with Accounting (default: BD for BDT, IN for INR, none otherwise; "none" for none)
|
|
55
55
|
--lang <en|bn> language --tz <zone> time zone (IANA)
|
|
56
56
|
--admin-login <login> --admin-name <name> --admin-email <email>
|
|
57
|
-
--admin-password <pw> (or
|
|
57
|
+
--admin-password <pw> (or SHUTRO_ERP_ADMIN_PASSWORD; at least 12 characters)
|
|
58
58
|
--apps <list> comma-separated: ${SELECTABLE_APPS.join(', ')} (default: all; bridges come with the apps they connect)
|
|
59
59
|
--storage-dir <dir> attachments in a folder (default ./var/attachments)
|
|
60
60
|
--s3-endpoint <url> --s3-bucket <name> --s3-access-key <key> --s3-secret-key <secret> [--s3-region <r>]
|
|
@@ -141,7 +141,7 @@ const required = (what) => (v) => (v.trim() === '' ? `${what} is required.` : nu
|
|
|
141
141
|
/** Opens the database; offers to create it when it does not exist. Returns the PostgreSQL version. */
|
|
142
142
|
async function checkDatabase(url, create) {
|
|
143
143
|
const probe = async (target) => {
|
|
144
|
-
const driver = pg.createDriver({ url: target, max: 1, applicationName: '
|
|
144
|
+
const driver = pg.createDriver({ url: target, max: 1, applicationName: 'shutro-erp init' });
|
|
145
145
|
try {
|
|
146
146
|
const [row] = await driver.query({
|
|
147
147
|
text: 'SHOW server_version',
|
|
@@ -171,7 +171,7 @@ async function checkDatabase(url, create) {
|
|
|
171
171
|
const admin = pg.createDriver({
|
|
172
172
|
url: parsed.toString(),
|
|
173
173
|
max: 1,
|
|
174
|
-
applicationName: '
|
|
174
|
+
applicationName: 'shutro-erp init',
|
|
175
175
|
});
|
|
176
176
|
try {
|
|
177
177
|
await admin.query({ text: `CREATE DATABASE "${name}"`, params: [] });
|
|
@@ -219,10 +219,10 @@ async function ask(prompter, flags, cwd, positional, ui) {
|
|
|
219
219
|
const dir = resolve(cwd, positional ?? (await q(null, 'Project folder', '.')));
|
|
220
220
|
const name = basename(dir)
|
|
221
221
|
.toLowerCase()
|
|
222
|
-
.replace(/[^a-z0-9-]+/g, '-') || '
|
|
222
|
+
.replace(/[^a-z0-9-]+/g, '-') || 'shutro-erp-app';
|
|
223
223
|
ui.step('database', 'Database', 'PostgreSQL 16 or later: a server you have, or one in Docker');
|
|
224
224
|
const postgres = {
|
|
225
|
-
user: '
|
|
225
|
+
user: 'shutro',
|
|
226
226
|
password: randomBytes(18)
|
|
227
227
|
.toString('base64url')
|
|
228
228
|
.replace(/[^A-Za-z0-9]/g, '')
|
|
@@ -503,6 +503,7 @@ async function modulesFor(apps) {
|
|
|
503
503
|
(await import('../notify/index.js')).notify,
|
|
504
504
|
(await import('../mail/index.js')).mail,
|
|
505
505
|
(await import('../automation/index.js')).automation,
|
|
506
|
+
(await import('../integrations/index.js')).integrations,
|
|
506
507
|
(await import('../studio/index.js')).studio,
|
|
507
508
|
(await import('../import/index.js')).dataImport,
|
|
508
509
|
];
|
|
@@ -541,17 +542,17 @@ async function modulesFor(apps) {
|
|
|
541
542
|
}
|
|
542
543
|
function envFile(a) {
|
|
543
544
|
const lines = [
|
|
544
|
-
'#
|
|
545
|
+
'# Shutro ERP settings (shutro-erp init). Keep this file out of version control: it holds credentials.',
|
|
545
546
|
`DATABASE_URL=${a.databaseUrl}`,
|
|
546
547
|
`PORT=${a.port}`,
|
|
547
548
|
];
|
|
548
549
|
if (a.storage.kind === 'dir')
|
|
549
|
-
lines.push(`
|
|
550
|
+
lines.push(`SHUTRO_ERP_STORAGE_DIR=${a.storage.dir}`);
|
|
550
551
|
else
|
|
551
552
|
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}`);
|
|
552
553
|
if (a.valkeyUrl !== '')
|
|
553
554
|
lines.push(`VALKEY_URL=${a.valkeyUrl}`);
|
|
554
|
-
lines.push('', '# Where users open the back-office (links in emails and notifications).', `
|
|
555
|
+
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}`}`);
|
|
555
556
|
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}`);
|
|
556
557
|
return `${lines.join('\n')}\n`;
|
|
557
558
|
}
|
|
@@ -618,15 +619,37 @@ async function setUp(server, a, ui) {
|
|
|
618
619
|
text: "SELECT to_regclass('public.res_users')::text AS found",
|
|
619
620
|
params: [],
|
|
620
621
|
});
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
622
|
+
// A database another project set up keeps that project's migration history: this folder cannot continue it.
|
|
623
|
+
const status = await migrate.migrationStatus({ driver: server.driver, dir });
|
|
624
|
+
const here = new Set((await readdir(dir).catch(() => []))
|
|
625
|
+
.map((f) => /^(\d+)_/.exec(f)?.[1])
|
|
626
|
+
.filter(Boolean));
|
|
627
|
+
const foreign = status.applied.filter((m) => !here.has(m.version));
|
|
628
|
+
if (foreign.length > 0)
|
|
629
|
+
throw new Error(`The database "${new URL(a.databaseUrl).pathname.slice(1)}" belongs to another installation: its migrations ` +
|
|
630
|
+
`(${foreign.map((m) => `${m.version}_${m.name}`).join(', ')}) are not in this project's migrations folder. ` +
|
|
631
|
+
'Choose a new database for this project, or run shutro-erp init (or migrate) in the folder of the ' +
|
|
632
|
+
'project that created it.');
|
|
633
|
+
// Always what the database lacks: a new one gets every table; one made by an earlier version (or another
|
|
634
|
+
// project's choice of apps) gets the tables and columns these apps add, before their data migrations run.
|
|
635
|
+
const fresh = installed?.found === null || installed?.found === undefined;
|
|
636
|
+
task.update('planning');
|
|
637
|
+
const plan = await migrate.planMigration({
|
|
638
|
+
driver: server.driver,
|
|
639
|
+
registry: server.registry.registry,
|
|
640
|
+
ignore: FRAMEWORK_TABLES, // the framework's own tables, not a model's: never dropped
|
|
641
|
+
});
|
|
642
|
+
if (plan.destructive.length > 0)
|
|
643
|
+
throw new Error(`The database "${new URL(a.databaseUrl).pathname.slice(1)}" has data these apps would drop or change ` +
|
|
644
|
+
`(${plan.destructive.map((op) => migrate.renderOperation(op)).join('; ')}). Use a new database, or ` +
|
|
645
|
+
'review the change and run shutro-erp migrate --allow-destructive in the project first.');
|
|
646
|
+
if (plan.statements.length > 0) {
|
|
647
|
+
const file = await migrate.writeMigrationFile({
|
|
648
|
+
dir,
|
|
649
|
+
name: fresh ? 'init' : 'update',
|
|
650
|
+
plan,
|
|
627
651
|
});
|
|
628
|
-
|
|
629
|
-
ui.detail(`planned ${plan.statements.length} statements in migrations/${basename(file.path)}`);
|
|
652
|
+
ui.detail(`planned ${plan.statements.length} statements in migrations/${basename(file.path)}${fresh ? '' : ' (the database had an earlier version)'}`);
|
|
630
653
|
}
|
|
631
654
|
task.update('applying');
|
|
632
655
|
const result = await migrate.applyMigrations({
|
|
@@ -634,8 +657,8 @@ async function setUp(server, a, ui) {
|
|
|
634
657
|
dir,
|
|
635
658
|
registry: server.registry.registry,
|
|
636
659
|
});
|
|
637
|
-
// The installed set (RFC 0008): what `
|
|
638
|
-
await recordInstalledApps(server.driver, server.registry.loaded.modules, ['base', 'settings', 'realtime', 'notify', ...a.apps], `
|
|
660
|
+
// The installed set (RFC 0008): what `shutro-erp apps` changes later.
|
|
661
|
+
await recordInstalledApps(server.driver, server.registry.loaded.modules, ['base', 'settings', 'realtime', 'notify', ...a.apps], `shutro-erp init (${a.admin.login})`);
|
|
639
662
|
task.finish(`Database tables ready ${ui.style('gray', `(applied ${result.applied.length} migration(s))`)}`);
|
|
640
663
|
});
|
|
641
664
|
const [users] = await server.driver.query({
|
|
@@ -654,7 +677,7 @@ async function setUp(server, a, ui) {
|
|
|
654
677
|
company: { name: a.company, currency: a.currency },
|
|
655
678
|
admin: { login: a.admin.login, name: a.admin.name, password: a.admin.password },
|
|
656
679
|
});
|
|
657
|
-
await server.shutro.bootstrap({ reason: '
|
|
680
|
+
await server.shutro.bootstrap({ reason: 'shutro-erp init', company }, async (env) => {
|
|
658
681
|
const model = (name) => env.model(name);
|
|
659
682
|
const lang = a.lang === 'bn' ? 'bn_BD' : 'en_US';
|
|
660
683
|
await model('res.users')
|
|
@@ -747,10 +770,10 @@ async function enrol(server, a, prompter, ui) {
|
|
|
747
770
|
ui.fail(error instanceof Error ? error.message : String(error));
|
|
748
771
|
}
|
|
749
772
|
}
|
|
750
|
-
ui.warn('Not enrolled: run
|
|
773
|
+
ui.warn('Not enrolled: run shutro-erp init again later, or enrol from the API (POST /api/v1/auth/2fa/setup).');
|
|
751
774
|
return false;
|
|
752
775
|
}
|
|
753
|
-
/** Runs `
|
|
776
|
+
/** Runs `shutro-erp init`. Returns the exit code. */
|
|
754
777
|
export async function runInit(argv, io) {
|
|
755
778
|
const { values, positionals } = parseArgs({
|
|
756
779
|
args: [...argv],
|
|
@@ -853,10 +876,10 @@ export async function runInit(argv, io) {
|
|
|
853
876
|
let server;
|
|
854
877
|
const started = Date.now();
|
|
855
878
|
try {
|
|
856
|
-
await ui.banner(`
|
|
857
|
-
ui.intro('
|
|
879
|
+
await ui.banner(`ERP · business software on the Shutro ORM · v${releaseVersion()}`);
|
|
880
|
+
ui.intro('Shutro ERP setup', interactive
|
|
858
881
|
? 'Enter keeps the default · ↑ ↓ choose · Space ticks · Ctrl+C quits'
|
|
859
|
-
: '
|
|
882
|
+
: 'Shutro ERP setup: press Enter to keep the value in [brackets].');
|
|
860
883
|
const answers = await ask(prompter, values, io.cwd, positionals[0], ui);
|
|
861
884
|
ui.step('files', 'Project files', answers.dir);
|
|
862
885
|
await ui.task('Writing the project', async () => {
|
|
@@ -899,7 +922,7 @@ export async function runInit(argv, io) {
|
|
|
899
922
|
enrolled = await enrol(live, answers, prompter, ui);
|
|
900
923
|
}
|
|
901
924
|
else if (values.yes === true && values['no-2fa'] !== true)
|
|
902
|
-
ui.info('The authenticator app is enrolled interactively: run npx
|
|
925
|
+
ui.info('The authenticator app is enrolled interactively: run npx shutro-erp init again (without --yes).');
|
|
903
926
|
const rows = [
|
|
904
927
|
[
|
|
905
928
|
'Company',
|
|
@@ -918,6 +941,7 @@ export async function runInit(argv, io) {
|
|
|
918
941
|
'notify',
|
|
919
942
|
'mail',
|
|
920
943
|
'automation',
|
|
944
|
+
'integrations',
|
|
921
945
|
'studio',
|
|
922
946
|
'import',
|
|
923
947
|
...answers.apps,
|
|
@@ -932,12 +956,12 @@ export async function runInit(argv, io) {
|
|
|
932
956
|
['Took', `${((Date.now() - started) / 1000).toFixed(1)}s`],
|
|
933
957
|
];
|
|
934
958
|
if (!autoStart)
|
|
935
|
-
ui.outro(ui.style('green', '
|
|
936
|
-
ui.card(`${terminal.emoji ? '🚀 ' : ''}
|
|
959
|
+
ui.outro(ui.style('green', 'Shutro ERP is set up'));
|
|
960
|
+
ui.card(`${terminal.emoji ? '🚀 ' : ''}Shutro ERP is set up`, rows, enrolled || values['no-2fa'] === true ? 'green' : 'yellow');
|
|
937
961
|
if (autoStart) {
|
|
938
962
|
if (!enrolled)
|
|
939
|
-
ui.warn('Administrator rights need a second factor: run `
|
|
940
|
-
ui.step('rocket', 'Starting
|
|
963
|
+
ui.warn('Administrator rights need a second factor: run `shutro-erp init` again to enrol the authenticator app.');
|
|
964
|
+
ui.step('rocket', 'Starting Shutro ERP', 'Dependencies, the API, the back-office, then your browser');
|
|
941
965
|
// The installer's own connection is not needed any more: the API has its own.
|
|
942
966
|
await server?.close();
|
|
943
967
|
server = undefined;
|
|
@@ -955,7 +979,7 @@ export async function runInit(argv, io) {
|
|
|
955
979
|
` ${cmd('npm run web')} ${ui.style('gray', `# the back-office on http://localhost:${answers.webPort}`)}\n\n` +
|
|
956
980
|
` Sign in as "${answers.admin.login}"${enrolled ? ' with your password and a code from the authenticator app' : ''}.\n`);
|
|
957
981
|
if (!enrolled)
|
|
958
|
-
ui.warn('Administrator rights need a second factor: run `
|
|
982
|
+
ui.warn('Administrator rights need a second factor: run `shutro-erp init` again to enrol the authenticator app.');
|
|
959
983
|
return 0;
|
|
960
984
|
}
|
|
961
985
|
catch (error) {
|
|
@@ -964,7 +988,7 @@ export async function runInit(argv, io) {
|
|
|
964
988
|
return 130;
|
|
965
989
|
}
|
|
966
990
|
ui.write('\n');
|
|
967
|
-
ui.fail(`
|
|
991
|
+
ui.fail(`shutro-erp init stopped: ${error instanceof Error ? error.message : String(error)}`);
|
|
968
992
|
return 1;
|
|
969
993
|
}
|
|
970
994
|
finally {
|
package/dist/cli/launch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* After `
|
|
2
|
+
* After `shutro-erp init`: the project's dependencies installed with the package manager that ran the installer, the API
|
|
3
3
|
* and the back-office started, each waited for until it answers, and the back-office opened in the default browser.
|
|
4
4
|
* Both keep running in this terminal until Ctrl+C, which stops them together. Their output is kept, and shown when
|
|
5
5
|
* one of them fails to start.
|
|
@@ -24,14 +24,17 @@ export function browserCommand(url, platform = process.platform) {
|
|
|
24
24
|
const windows = process.platform === 'win32';
|
|
25
25
|
/** A child process running `pm args` in `cwd`, its output kept (the last 200 lines). */
|
|
26
26
|
function start(pm, args, cwd) {
|
|
27
|
-
const
|
|
27
|
+
const options = {
|
|
28
28
|
cwd,
|
|
29
29
|
env: { ...process.env, FORCE_COLOR: '0' },
|
|
30
30
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
detached: !windows, // elsewhere a process group, stopped as a whole
|
|
32
|
+
};
|
|
33
|
+
// npm, pnpm… are .cmd scripts on Windows: run through the shell as one command line (the arguments are this
|
|
34
|
+
// module's own words, never input), which Node prefers to separate arguments with `shell: true`.
|
|
35
|
+
const child = windows
|
|
36
|
+
? spawn([pm, ...args].join(' '), { ...options, shell: true })
|
|
37
|
+
: spawn(pm, args, options);
|
|
35
38
|
const lines = [];
|
|
36
39
|
const keep = (chunk) => {
|
|
37
40
|
lines.push(...chunk
|
|
@@ -117,7 +120,7 @@ export async function launch(o, ui) {
|
|
|
117
120
|
opened.on('error', () => ui.info(`Open ${webUrl} in your browser.`));
|
|
118
121
|
opened.unref();
|
|
119
122
|
ui.ok(`Opened ${ui.style('cyan', webUrl)} in your browser · sign in as "${o.login}"`);
|
|
120
|
-
ui.outro(`${ui.style('green', '
|
|
123
|
+
ui.outro(`${ui.style('green', 'Shutro ERP is running')} ${ui.style('gray', `back-office ${webUrl} · API ${apiUrl} · Ctrl+C stops both`)}`);
|
|
121
124
|
// Until Ctrl+C, or until one of them stops by itself (its last lines are shown).
|
|
122
125
|
const which = await Promise.race([
|
|
123
126
|
new Promise((done) => {
|
package/dist/cli/project.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { KoshModule } from '../modules-sdk/index.js';
|
|
2
2
|
import { type KoshServer } from '../server/index.js';
|
|
3
|
-
/** The
|
|
3
|
+
/** The Shutro ERP modules a file or package exports: `modules`, the default export, or every module export. */
|
|
4
4
|
export declare function modulesOf(specifier: string, cwd: string): Promise<KoshModule[]>;
|
|
5
5
|
/** Opens the installation in `cwd`: `.env`, the modules, a server (not listening). */
|
|
6
6
|
export declare function openProject(cwd: string, modulesFile: string, options?: {
|
|
7
7
|
/** `schema`: installed and disabled modules (what migrations keep); default: the installed ones (what is served). */
|
|
8
8
|
readonly set?: 'served' | 'schema';
|
|
9
9
|
}): Promise<KoshServer>;
|
|
10
|
-
/** What seed data is loaded with: a server's, or an app change's transaction (`
|
|
10
|
+
/** What seed data is loaded with: a server's, or an app change's transaction (`shutro-erp apps`). */
|
|
11
11
|
export interface SeedContext {
|
|
12
12
|
readonly shutro: KoshServer['shutro'];
|
|
13
13
|
readonly driver: Pick<KoshServer['driver'], 'query'>;
|
|
@@ -24,13 +24,13 @@ export declare function migrateProject(server: KoshServer, dir: string, write: (
|
|
|
24
24
|
allowDestructive?: boolean;
|
|
25
25
|
seed?: boolean;
|
|
26
26
|
}): Promise<number>;
|
|
27
|
-
/** `
|
|
27
|
+
/** `shutro-erp migrate [status] …` */
|
|
28
28
|
export declare function runMigrate(argv: readonly string[], write: (t: string) => void, cwd: string): Promise<number>;
|
|
29
|
-
/** `
|
|
29
|
+
/** `shutro-erp seed [module …]` */
|
|
30
30
|
export declare function runSeed(argv: readonly string[], write: (t: string) => void, cwd: string): Promise<number>;
|
|
31
|
-
/** The files of a new module (`
|
|
31
|
+
/** The files of a new module (`shutro-erp module create <name>`). */
|
|
32
32
|
export declare function moduleFiles(name: string): Record<string, string>;
|
|
33
|
-
/** `
|
|
33
|
+
/** `shutro-erp module create <name>`: scaffolds src/modules/<name>/ and lists it in src/modules.ts. */
|
|
34
34
|
export declare function runModule(argv: readonly string[], write: (t: string) => void, cwd: string): Promise<number>;
|
|
35
|
-
/** `
|
|
35
|
+
/** `shutro-erp user create <login> …` and `shutro-erp user password <login>`. */
|
|
36
36
|
export declare function runUser(argv: readonly string[], write: (t: string) => void, cwd: string, prompter?: import('./prompt.js').Prompter): Promise<number>;
|
package/dist/cli/project.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Commands that work on an installation (a folder made by `
|
|
3
|
-
* `
|
|
2
|
+
* Commands that work on an installation (a folder made by `shutro-erp init`): `shutro-erp migrate`, `shutro-erp seed`,
|
|
3
|
+
* `shutro-erp module create`. They read `.env` (DATABASE_URL, storage, ...) and the module list in `src/modules.ts`.
|
|
4
4
|
*
|
|
5
5
|
* - Schema migrations are versioned SQL files in `migrations/`, planned from the modules against the live database
|
|
6
6
|
* (Shutro migrate) and applied in order; each module's data migrations (`migrations` in its Shutro module) run
|
|
@@ -21,7 +21,7 @@ function isModule(value) {
|
|
|
21
21
|
'accessLevels' in value &&
|
|
22
22
|
'name' in value);
|
|
23
23
|
}
|
|
24
|
-
/** The
|
|
24
|
+
/** The Shutro ERP modules a file or package exports: `modules`, the default export, or every module export. */
|
|
25
25
|
export async function modulesOf(specifier, cwd) {
|
|
26
26
|
const target = specifier.startsWith('.') ||
|
|
27
27
|
specifier.startsWith('/') ||
|
|
@@ -39,7 +39,7 @@ export async function modulesOf(specifier, cwd) {
|
|
|
39
39
|
}
|
|
40
40
|
const all = Object.values(exported).filter(isModule);
|
|
41
41
|
if (all.length === 0)
|
|
42
|
-
throw new Error(`${specifier} exports no
|
|
42
|
+
throw new Error(`${specifier} exports no Shutro ERP modules (defineModule from shutro-erp/modules-sdk).`);
|
|
43
43
|
return all;
|
|
44
44
|
}
|
|
45
45
|
/** Opens the installation in `cwd`: `.env`, the modules, a server (not listening). */
|
|
@@ -49,9 +49,9 @@ export async function openProject(cwd, modulesFile, options = {}) {
|
|
|
49
49
|
process.loadEnvFile(envFile);
|
|
50
50
|
const databaseUrl = process.env['DATABASE_URL'];
|
|
51
51
|
if (databaseUrl === undefined || databaseUrl === '')
|
|
52
|
-
throw new Error('DATABASE_URL is not set: run
|
|
52
|
+
throw new Error('DATABASE_URL is not set: run shutro-erp init, or add it to .env.');
|
|
53
53
|
if (!existsSync(resolve(cwd, modulesFile)))
|
|
54
|
-
throw new Error(`${modulesFile} does not exist: run
|
|
54
|
+
throw new Error(`${modulesFile} does not exist: run shutro-erp init, or pass --modules <file>.`);
|
|
55
55
|
process.env['KOSH_SCHEDULES'] = 'off';
|
|
56
56
|
// The module list is the catalogue; the database says which of them are installed (RFC 0008).
|
|
57
57
|
const modules = await installedModules(databaseUrl, await modulesOf(modulesFile, cwd), options.set === 'schema' ? ['installed', 'disabled'] : ['installed']);
|
|
@@ -93,7 +93,7 @@ export async function seedWith(context, write, only) {
|
|
|
93
93
|
: context.modules.filter((m) => only.includes(m.name));
|
|
94
94
|
for (const d of await installDefaults(context.shutro, context.modules, {
|
|
95
95
|
company: companyId,
|
|
96
|
-
reason: '
|
|
96
|
+
reason: 'shutro-erp seed',
|
|
97
97
|
}))
|
|
98
98
|
if (wanted.some((m) => m.name === d.module))
|
|
99
99
|
write(` ${d.module.padEnd(16)} ${d.summary}
|
|
@@ -117,7 +117,7 @@ async function apply(server, dir, write, allowDestructive) {
|
|
|
117
117
|
}
|
|
118
118
|
catch (error) {
|
|
119
119
|
if (error instanceof Error && error.name === 'DestructiveMigrationError') {
|
|
120
|
-
write(`${error.message}\nReview it, then run
|
|
120
|
+
write(`${error.message}\nReview it, then run shutro-erp migrate --allow-destructive.\n`);
|
|
121
121
|
return 1;
|
|
122
122
|
}
|
|
123
123
|
throw error;
|
|
@@ -152,7 +152,7 @@ export async function migrateProject(server, dir, write, options = {}) {
|
|
|
152
152
|
if (options.planOnly === true)
|
|
153
153
|
return 0;
|
|
154
154
|
if (plan.destructive.length > 0 && options.allowDestructive !== true) {
|
|
155
|
-
write('It drops or changes existing data: review the file, then run
|
|
155
|
+
write('It drops or changes existing data: review the file, then run shutro-erp migrate --allow-destructive.\n');
|
|
156
156
|
return 1;
|
|
157
157
|
}
|
|
158
158
|
if (plan.statements.length > 0 || pending.length === 0) {
|
|
@@ -173,11 +173,11 @@ export async function migrateProject(server, dir, write, options = {}) {
|
|
|
173
173
|
await seed(server, write, served !== undefined && served.length > 0 ? served : undefined);
|
|
174
174
|
}
|
|
175
175
|
else
|
|
176
|
-
write('No company yet: seed data waits for
|
|
176
|
+
write('No company yet: seed data waits for shutro-erp init.\n');
|
|
177
177
|
}
|
|
178
178
|
return 0;
|
|
179
179
|
}
|
|
180
|
-
/** `
|
|
180
|
+
/** `shutro-erp migrate [status] …` */
|
|
181
181
|
export async function runMigrate(argv, write, cwd) {
|
|
182
182
|
const { values, positionals } = parseArgs({
|
|
183
183
|
args: [...argv],
|
|
@@ -217,7 +217,7 @@ export async function runMigrate(argv, write, cwd) {
|
|
|
217
217
|
await server.close();
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
-
/** `
|
|
220
|
+
/** `shutro-erp seed [module …]` */
|
|
221
221
|
export async function runSeed(argv, write, cwd) {
|
|
222
222
|
const { values, positionals } = parseArgs({
|
|
223
223
|
args: [...argv],
|
|
@@ -239,7 +239,7 @@ const title = (name) => name
|
|
|
239
239
|
.split(/[-_]+/)
|
|
240
240
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
241
241
|
.join(' ');
|
|
242
|
-
/** The files of a new module (`
|
|
242
|
+
/** The files of a new module (`shutro-erp module create <name>`). */
|
|
243
243
|
export function moduleFiles(name) {
|
|
244
244
|
const id = camel(name);
|
|
245
245
|
const label = title(name);
|
|
@@ -265,7 +265,7 @@ export const Item = defineModel('${model}', {
|
|
|
265
265
|
});
|
|
266
266
|
`,
|
|
267
267
|
'data.ts': `/**
|
|
268
|
-
* Seed data of the ${label} module: \`
|
|
268
|
+
* Seed data of the ${label} module: \`shutro-erp seed\` (and \`shutro-erp migrate\`) create these records, update them when they
|
|
269
269
|
* change here, and delete records removed from this list. \`noupdate\` records are created once, then left to the users.
|
|
270
270
|
* Refer to other records by external id: orm.ref('res.currency', 'base.currency_bdt').
|
|
271
271
|
*/
|
|
@@ -277,7 +277,7 @@ export const data = [
|
|
|
277
277
|
`,
|
|
278
278
|
'migrations.ts': `/**
|
|
279
279
|
* Data migrations of the ${label} module: each runs once, when an installed module is upgraded past its version
|
|
280
|
-
* (raise \`version\` in index.ts, then \`
|
|
280
|
+
* (raise \`version\` in index.ts, then \`shutro-erp migrate\`). Schema changes need none: \`shutro-erp migrate\` plans them.
|
|
281
281
|
*/
|
|
282
282
|
import type { DataMigration } from 'shutro-erp/orm';
|
|
283
283
|
|
|
@@ -293,7 +293,7 @@ export const migrations: DataMigration[] = [
|
|
|
293
293
|
`,
|
|
294
294
|
'index.ts': `/**
|
|
295
295
|
* The ${label} module: its Shutro models, groups, seed data and data migrations, plus access levels, an app,
|
|
296
|
-
* menus, actions and views. Installed by listing it in src/modules.ts, then \`
|
|
296
|
+
* menus, actions and views. Installed by listing it in src/modules.ts, then \`shutro-erp migrate\`.
|
|
297
297
|
*/
|
|
298
298
|
import { base, baseShutro } from 'shutro-erp';
|
|
299
299
|
import { defineModule } from 'shutro-erp/modules-sdk';
|
|
@@ -340,11 +340,11 @@ export const ${id} = defineModule({
|
|
|
340
340
|
`,
|
|
341
341
|
};
|
|
342
342
|
}
|
|
343
|
-
/** `
|
|
343
|
+
/** `shutro-erp module create <name>`: scaffolds src/modules/<name>/ and lists it in src/modules.ts. */
|
|
344
344
|
export async function runModule(argv, write, cwd) {
|
|
345
345
|
const [action, name] = argv;
|
|
346
346
|
if (action !== 'create' || name === undefined) {
|
|
347
|
-
write('Usage:
|
|
347
|
+
write('Usage: shutro-erp module create <name> (lower-case letters, digits and -, e.g. library)\n');
|
|
348
348
|
return 2;
|
|
349
349
|
}
|
|
350
350
|
if (!/^[a-z][a-z0-9-]{1,40}$/.test(name) ||
|
|
@@ -379,7 +379,7 @@ export async function runModule(argv, write, cwd) {
|
|
|
379
379
|
else
|
|
380
380
|
write(` add ${id} to the modules list in src/modules.ts\n`);
|
|
381
381
|
}
|
|
382
|
-
write(`\nNext:
|
|
382
|
+
write(`\nNext: shutro-erp apps install ${name} (creates its tables and loads its seed data, in one transaction)\n`);
|
|
383
383
|
return 0;
|
|
384
384
|
}
|
|
385
385
|
/** The access levels a role list names: `reporting.user`, `messaging.user`, … (groups of installed modules). */
|
|
@@ -396,7 +396,7 @@ function groupsOf(server, roles) {
|
|
|
396
396
|
throw new Error(`Unknown role(s): ${unknown.join(', ')}. Known: ${[...known].sort().join(', ')}.`);
|
|
397
397
|
return list;
|
|
398
398
|
}
|
|
399
|
-
/** `
|
|
399
|
+
/** `shutro-erp user create <login> …` and `shutro-erp user password <login>`. */
|
|
400
400
|
export async function runUser(argv, write, cwd, prompter) {
|
|
401
401
|
const { values, positionals } = parseArgs({
|
|
402
402
|
args: [...argv],
|
|
@@ -411,8 +411,8 @@ export async function runUser(argv, write, cwd, prompter) {
|
|
|
411
411
|
});
|
|
412
412
|
const [action, login] = positionals;
|
|
413
413
|
if ((action !== 'create' && action !== 'password') || login === undefined) {
|
|
414
|
-
write('Usage:
|
|
415
|
-
'
|
|
414
|
+
write('Usage: shutro-erp user create <login> [--name <name>] [--email <email>] [--roles a.user,b.manager] [--password <pw>]\n' +
|
|
415
|
+
' shutro-erp user password <login> [--password <pw>]\n');
|
|
416
416
|
return 2;
|
|
417
417
|
}
|
|
418
418
|
const { terminalPrompter } = await import('./prompt.js');
|
|
@@ -436,7 +436,7 @@ export async function runUser(argv, write, cwd, prompter) {
|
|
|
436
436
|
params: [],
|
|
437
437
|
});
|
|
438
438
|
if (company === undefined)
|
|
439
|
-
throw new Error('No company yet: run
|
|
439
|
+
throw new Error('No company yet: run shutro-erp init first.');
|
|
440
440
|
const [user] = await server.driver.query({
|
|
441
441
|
text: 'SELECT id::text AS id FROM res_users WHERE login = $1',
|
|
442
442
|
params: [login],
|
|
@@ -444,7 +444,7 @@ export async function runUser(argv, write, cwd, prompter) {
|
|
|
444
444
|
if (action === 'password') {
|
|
445
445
|
if (user === undefined)
|
|
446
446
|
throw new Error(`No user "${login}".`);
|
|
447
|
-
await server.shutro.bootstrap({ reason: `
|
|
447
|
+
await server.shutro.bootstrap({ reason: `shutro-erp user password ${login}` }, (env) => auth.setPassword(env, Number(user.id), password));
|
|
448
448
|
write(`Password of "${login}" changed.\n`);
|
|
449
449
|
return 0;
|
|
450
450
|
}
|
|
@@ -459,7 +459,7 @@ export async function runUser(argv, write, cwd, prompter) {
|
|
|
459
459
|
groups: groupsOf(server, values.roles),
|
|
460
460
|
});
|
|
461
461
|
if (values.email !== undefined)
|
|
462
|
-
await server.shutro.bootstrap({ reason: `
|
|
462
|
+
await server.shutro.bootstrap({ reason: `shutro-erp user create ${login}` }, async (env) => {
|
|
463
463
|
await env.model('res.users')
|
|
464
464
|
.browse([id])
|
|
465
465
|
.write({ email: values.email });
|
package/dist/cli/prompt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Terminal questions for `
|
|
2
|
+
* Terminal questions for `shutro-erp init`, on `node:readline` (no dependency). Answers can be scripted for tests and
|
|
3
3
|
* non-interactive installs (`--yes` with flags): every question goes through {@link Prompter}.
|
|
4
4
|
*/
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
@@ -150,7 +150,7 @@ export function defaultsPrompter() {
|
|
|
150
150
|
return {
|
|
151
151
|
async ask(q) {
|
|
152
152
|
if (q.default === undefined)
|
|
153
|
-
throw new Error(`"${q.message}" has no default: pass it as a flag (
|
|
153
|
+
throw new Error(`"${q.message}" has no default: pass it as a flag (shutro-erp init --help).`);
|
|
154
154
|
const problem = q.validate?.(q.default) ?? null;
|
|
155
155
|
if (problem !== null)
|
|
156
156
|
throw new Error(`${q.message}: ${problem}`);
|