shutro-erp 0.3.0 → 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 +58 -58
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +31 -1
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +31 -1
- 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 +58 -36
- package/dist/cli/launch.js +2 -2
- 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 +29 -27
- package/dist/cli/ui.d.ts +1 -8
- package/dist/cli/ui.js +17 -11
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +31 -1
- 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/loader.js +2 -2
- package/dist/notify/messages.js +3 -3
- package/dist/notify/routes.js +1 -1
- package/dist/notify/web/messages.js +1 -1
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +31 -1
- 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/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 +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 +1 -0
- package/dist/server/index.js +1 -0
- 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 +3 -3
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, '')
|
|
@@ -541,17 +541,17 @@ async function modulesFor(apps) {
|
|
|
541
541
|
}
|
|
542
542
|
function envFile(a) {
|
|
543
543
|
const lines = [
|
|
544
|
-
'#
|
|
544
|
+
'# Shutro ERP settings (shutro-erp init). Keep this file out of version control: it holds credentials.',
|
|
545
545
|
`DATABASE_URL=${a.databaseUrl}`,
|
|
546
546
|
`PORT=${a.port}`,
|
|
547
547
|
];
|
|
548
548
|
if (a.storage.kind === 'dir')
|
|
549
|
-
lines.push(`
|
|
549
|
+
lines.push(`SHUTRO_ERP_STORAGE_DIR=${a.storage.dir}`);
|
|
550
550
|
else
|
|
551
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}`);
|
|
552
552
|
if (a.valkeyUrl !== '')
|
|
553
553
|
lines.push(`VALKEY_URL=${a.valkeyUrl}`);
|
|
554
|
-
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}`}`);
|
|
555
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}`);
|
|
556
556
|
return `${lines.join('\n')}\n`;
|
|
557
557
|
}
|
|
@@ -618,15 +618,37 @@ async function setUp(server, a, ui) {
|
|
|
618
618
|
text: "SELECT to_regclass('public.res_users')::text AS found",
|
|
619
619
|
params: [],
|
|
620
620
|
});
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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,
|
|
627
650
|
});
|
|
628
|
-
|
|
629
|
-
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)'}`);
|
|
630
652
|
}
|
|
631
653
|
task.update('applying');
|
|
632
654
|
const result = await migrate.applyMigrations({
|
|
@@ -634,8 +656,8 @@ async function setUp(server, a, ui) {
|
|
|
634
656
|
dir,
|
|
635
657
|
registry: server.registry.registry,
|
|
636
658
|
});
|
|
637
|
-
// The installed set (RFC 0008): what `
|
|
638
|
-
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})`);
|
|
639
661
|
task.finish(`Database tables ready ${ui.style('gray', `(applied ${result.applied.length} migration(s))`)}`);
|
|
640
662
|
});
|
|
641
663
|
const [users] = await server.driver.query({
|
|
@@ -654,7 +676,7 @@ async function setUp(server, a, ui) {
|
|
|
654
676
|
company: { name: a.company, currency: a.currency },
|
|
655
677
|
admin: { login: a.admin.login, name: a.admin.name, password: a.admin.password },
|
|
656
678
|
});
|
|
657
|
-
await server.shutro.bootstrap({ reason: '
|
|
679
|
+
await server.shutro.bootstrap({ reason: 'shutro-erp init', company }, async (env) => {
|
|
658
680
|
const model = (name) => env.model(name);
|
|
659
681
|
const lang = a.lang === 'bn' ? 'bn_BD' : 'en_US';
|
|
660
682
|
await model('res.users')
|
|
@@ -747,10 +769,10 @@ async function enrol(server, a, prompter, ui) {
|
|
|
747
769
|
ui.fail(error instanceof Error ? error.message : String(error));
|
|
748
770
|
}
|
|
749
771
|
}
|
|
750
|
-
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).');
|
|
751
773
|
return false;
|
|
752
774
|
}
|
|
753
|
-
/** Runs `
|
|
775
|
+
/** Runs `shutro-erp init`. Returns the exit code. */
|
|
754
776
|
export async function runInit(argv, io) {
|
|
755
777
|
const { values, positionals } = parseArgs({
|
|
756
778
|
args: [...argv],
|
|
@@ -853,10 +875,10 @@ export async function runInit(argv, io) {
|
|
|
853
875
|
let server;
|
|
854
876
|
const started = Date.now();
|
|
855
877
|
try {
|
|
856
|
-
await ui.banner(`
|
|
857
|
-
ui.intro('
|
|
878
|
+
await ui.banner(`ERP · business software on the Shutro ORM · v${releaseVersion()}`);
|
|
879
|
+
ui.intro('Shutro ERP setup', interactive
|
|
858
880
|
? 'Enter keeps the default · ↑ ↓ choose · Space ticks · Ctrl+C quits'
|
|
859
|
-
: '
|
|
881
|
+
: 'Shutro ERP setup: press Enter to keep the value in [brackets].');
|
|
860
882
|
const answers = await ask(prompter, values, io.cwd, positionals[0], ui);
|
|
861
883
|
ui.step('files', 'Project files', answers.dir);
|
|
862
884
|
await ui.task('Writing the project', async () => {
|
|
@@ -899,7 +921,7 @@ export async function runInit(argv, io) {
|
|
|
899
921
|
enrolled = await enrol(live, answers, prompter, ui);
|
|
900
922
|
}
|
|
901
923
|
else if (values.yes === true && values['no-2fa'] !== true)
|
|
902
|
-
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).');
|
|
903
925
|
const rows = [
|
|
904
926
|
[
|
|
905
927
|
'Company',
|
|
@@ -932,12 +954,12 @@ export async function runInit(argv, io) {
|
|
|
932
954
|
['Took', `${((Date.now() - started) / 1000).toFixed(1)}s`],
|
|
933
955
|
];
|
|
934
956
|
if (!autoStart)
|
|
935
|
-
ui.outro(ui.style('green', '
|
|
936
|
-
ui.card(`${terminal.emoji ? '🚀 ' : ''}
|
|
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');
|
|
937
959
|
if (autoStart) {
|
|
938
960
|
if (!enrolled)
|
|
939
|
-
ui.warn('Administrator rights need a second factor: run `
|
|
940
|
-
ui.step('rocket', 'Starting
|
|
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');
|
|
941
963
|
// The installer's own connection is not needed any more: the API has its own.
|
|
942
964
|
await server?.close();
|
|
943
965
|
server = undefined;
|
|
@@ -955,7 +977,7 @@ export async function runInit(argv, io) {
|
|
|
955
977
|
` ${cmd('npm run web')} ${ui.style('gray', `# the back-office on http://localhost:${answers.webPort}`)}\n\n` +
|
|
956
978
|
` Sign in as "${answers.admin.login}"${enrolled ? ' with your password and a code from the authenticator app' : ''}.\n`);
|
|
957
979
|
if (!enrolled)
|
|
958
|
-
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.');
|
|
959
981
|
return 0;
|
|
960
982
|
}
|
|
961
983
|
catch (error) {
|
|
@@ -964,7 +986,7 @@ export async function runInit(argv, io) {
|
|
|
964
986
|
return 130;
|
|
965
987
|
}
|
|
966
988
|
ui.write('\n');
|
|
967
|
-
ui.fail(`
|
|
989
|
+
ui.fail(`shutro-erp init stopped: ${error instanceof Error ? error.message : String(error)}`);
|
|
968
990
|
return 1;
|
|
969
991
|
}
|
|
970
992
|
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.
|
|
@@ -117,7 +117,7 @@ export async function launch(o, ui) {
|
|
|
117
117
|
opened.on('error', () => ui.info(`Open ${webUrl} in your browser.`));
|
|
118
118
|
opened.unref();
|
|
119
119
|
ui.ok(`Opened ${ui.style('cyan', webUrl)} in your browser · sign in as "${o.login}"`);
|
|
120
|
-
ui.outro(`${ui.style('green', '
|
|
120
|
+
ui.outro(`${ui.style('green', 'Shutro ERP is running')} ${ui.style('gray', `back-office ${webUrl} · API ${apiUrl} · Ctrl+C stops both`)}`);
|
|
121
121
|
// Until Ctrl+C, or until one of them stops by itself (its last lines are shown).
|
|
122
122
|
const which = await Promise.race([
|
|
123
123
|
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}`);
|
package/dist/cli/templates.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The starter project `
|
|
2
|
+
* The starter project `shutro-erp init` writes: an API server and the back-office app, both importing `shutro-erp`.
|
|
3
3
|
* Server files run on Node.js directly (type stripping); the back-office is a Vite app proxying `/api` to the server.
|
|
4
4
|
*/
|
|
5
5
|
export type AppName = 'permissions' | 'reporting' | 'messaging' | 'contacts' | 'product' | 'sales' | 'purchase' | 'accounting' | 'sale_account' | 'purchase_account' | 'project' | 'sale_project' | 'l10n_bd' | 'l10n_in';
|
|
@@ -16,7 +16,7 @@ export interface AppSpec {
|
|
|
16
16
|
/** A localization (RFC 0016): installed, with its bridged apps, for a company in this country (ISO 3166). */
|
|
17
17
|
readonly country?: string;
|
|
18
18
|
}
|
|
19
|
-
/** The apps `
|
|
19
|
+
/** The apps `shutro-erp init` can install (base and Settings are always there), in the order the installer lists them. */
|
|
20
20
|
export declare const APPS: Readonly<Record<AppName, AppSpec>>;
|
|
21
21
|
export interface StarterOptions {
|
|
22
22
|
readonly name: string;
|
|
@@ -27,7 +27,7 @@ export interface StarterOptions {
|
|
|
27
27
|
readonly koshVersion: string;
|
|
28
28
|
/** The database name of the Docker PostgreSQL service (compose.yaml). */
|
|
29
29
|
readonly dockerDb?: string;
|
|
30
|
-
/** Attachments in a folder (
|
|
30
|
+
/** Attachments in a folder (SHUTRO_ERP_STORAGE_DIR) rather than S3. */
|
|
31
31
|
readonly fileStorage?: boolean;
|
|
32
32
|
}
|
|
33
33
|
/** The project's package.json (merged into an existing one: missing scripts and dependencies are added). */
|