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.
Files changed (137) hide show
  1. package/README.md +58 -58
  2. package/dist/account_portal/.kosh/bindings.js +1 -1
  3. package/dist/account_portal/.kosh/schema.d.ts +31 -1
  4. package/dist/account_portal/index.js +3 -2
  5. package/dist/accounting/.kosh/bindings.js +1 -1
  6. package/dist/accounting/.kosh/schema.d.ts +31 -1
  7. package/dist/accounting/index.js +4 -0
  8. package/dist/assistant/routes.js +1 -1
  9. package/dist/automation/engine.js +1 -1
  10. package/dist/automation/web/messages.js +2 -2
  11. package/dist/cli/apps.d.ts +1 -1
  12. package/dist/cli/apps.js +14 -14
  13. package/dist/cli/bin.js +3 -0
  14. package/dist/cli/index.js +11 -11
  15. package/dist/cli/init.d.ts +1 -1
  16. package/dist/cli/init.js +60 -36
  17. package/dist/cli/launch.js +10 -7
  18. package/dist/cli/project.d.ts +7 -7
  19. package/dist/cli/project.js +25 -25
  20. package/dist/cli/prompt.js +2 -2
  21. package/dist/cli/templates.d.ts +3 -3
  22. package/dist/cli/templates.js +33 -28
  23. package/dist/cli/ui.d.ts +1 -8
  24. package/dist/cli/ui.js +36 -12
  25. package/dist/contacts/.kosh/bindings.js +1 -1
  26. package/dist/contacts/.kosh/schema.d.ts +31 -1
  27. package/dist/integrations/inbound.d.ts +36 -0
  28. package/dist/integrations/inbound.js +171 -0
  29. package/dist/integrations/index.d.ts +18 -0
  30. package/dist/integrations/index.js +400 -0
  31. package/dist/integrations/models.d.ts +6 -0
  32. package/dist/integrations/models.js +119 -0
  33. package/dist/integrations/web/DeliveryTools.d.ts +4 -0
  34. package/dist/integrations/web/DeliveryTools.js +15 -0
  35. package/dist/integrations/web/EndpointTools.d.ts +7 -0
  36. package/dist/integrations/web/EndpointTools.js +25 -0
  37. package/dist/integrations/web/InboundTools.d.ts +4 -0
  38. package/dist/integrations/web/InboundTools.js +15 -0
  39. package/dist/integrations/web/WebhookTools.d.ts +7 -0
  40. package/dist/integrations/web/WebhookTools.js +25 -0
  41. package/dist/integrations/web/index.d.ts +16 -0
  42. package/dist/integrations/web/index.js +27 -0
  43. package/dist/integrations/web/messages.d.ts +2 -0
  44. package/dist/integrations/web/messages.js +53 -0
  45. package/dist/integrations/webhooks.d.ts +63 -0
  46. package/dist/integrations/webhooks.js +217 -0
  47. package/dist/l10n_bd/.kosh/bindings.js +1 -1
  48. package/dist/l10n_bd/.kosh/schema.d.ts +31 -1
  49. package/dist/l10n_in/.kosh/bindings.js +1 -1
  50. package/dist/l10n_in/.kosh/schema.d.ts +31 -1
  51. package/dist/l10n_in/einvoice.js +1 -1
  52. package/dist/l10n_in/web/messages.js +2 -2
  53. package/dist/mail/index.js +2 -2
  54. package/dist/modules-sdk/index.d.ts +1 -1
  55. package/dist/modules-sdk/loader.js +2 -2
  56. package/dist/modules-sdk/manifest.d.ts +48 -0
  57. package/dist/notify/messages.js +3 -3
  58. package/dist/notify/routes.js +1 -1
  59. package/dist/notify/web/messages.js +1 -1
  60. package/dist/payment/.kosh/bindings.d.ts +22 -0
  61. package/dist/payment/.kosh/bindings.js +15 -0
  62. package/dist/payment/.kosh/schema.d.ts +1553 -0
  63. package/dist/payment/index.d.ts +34 -0
  64. package/dist/payment/index.js +375 -0
  65. package/dist/payment/module.d.ts +6853 -0
  66. package/dist/payment/module.js +74 -0
  67. package/dist/payment/providers.d.ts +64 -0
  68. package/dist/payment/providers.js +77 -0
  69. package/dist/payment/settle.d.ts +11 -0
  70. package/dist/payment/settle.js +59 -0
  71. package/dist/payment/sslcommerz.d.ts +12 -0
  72. package/dist/payment/sslcommerz.js +185 -0
  73. package/dist/portal/.kosh/bindings.js +1 -1
  74. package/dist/portal/.kosh/schema.d.ts +31 -1
  75. package/dist/portal/routes-documents.js +5 -0
  76. package/dist/product/.kosh/bindings.js +1 -1
  77. package/dist/product/.kosh/schema.d.ts +31 -1
  78. package/dist/project/.kosh/bindings.js +1 -1
  79. package/dist/project/.kosh/schema.d.ts +31 -1
  80. package/dist/project_portal/.kosh/bindings.js +1 -1
  81. package/dist/project_portal/.kosh/schema.d.ts +31 -1
  82. package/dist/purchase/.kosh/bindings.js +1 -1
  83. package/dist/purchase/.kosh/schema.d.ts +31 -1
  84. package/dist/purchase_account/.kosh/bindings.js +1 -1
  85. package/dist/purchase_account/.kosh/schema.d.ts +31 -1
  86. package/dist/purchase_portal/.kosh/bindings.js +1 -1
  87. package/dist/purchase_portal/.kosh/schema.d.ts +31 -1
  88. package/dist/sale_account/.kosh/bindings.js +1 -1
  89. package/dist/sale_account/.kosh/schema.d.ts +31 -1
  90. package/dist/sale_portal/.kosh/bindings.js +1 -1
  91. package/dist/sale_portal/.kosh/schema.d.ts +31 -1
  92. package/dist/sale_project/.kosh/bindings.js +1 -1
  93. package/dist/sale_project/.kosh/schema.d.ts +31 -1
  94. package/dist/sales/.kosh/bindings.js +1 -1
  95. package/dist/sales/.kosh/schema.d.ts +31 -1
  96. package/dist/sales/index.js +2 -0
  97. package/dist/server/api/client.js +1 -1
  98. package/dist/server/api/openapi.js +17 -1
  99. package/dist/server/api/rest.d.ts +6 -1
  100. package/dist/server/api/rest.js +11 -0
  101. package/dist/server/apps.js +5 -5
  102. package/dist/server/auth/api-keys.d.ts +13 -0
  103. package/dist/server/auth/api-keys.js +65 -0
  104. package/dist/server/auth/base.js +24 -0
  105. package/dist/server/auth/crypto.js +1 -1
  106. package/dist/server/auth/scopes.d.ts +12 -0
  107. package/dist/server/auth/scopes.js +51 -0
  108. package/dist/server/auth/service.d.ts +57 -1
  109. package/dist/server/auth/service.js +229 -5
  110. package/dist/server/demo.d.ts +1 -1
  111. package/dist/server/demo.js +1 -1
  112. package/dist/server/env-names.d.ts +2 -0
  113. package/dist/server/env-names.js +16 -0
  114. package/dist/server/host.js +5 -1
  115. package/dist/server/identity.d.ts +7 -0
  116. package/dist/server/index.d.ts +2 -1
  117. package/dist/server/index.js +2 -1
  118. package/dist/server/registry.js +1 -1
  119. package/dist/server/server.d.ts +5 -0
  120. package/dist/server/server.js +45 -5
  121. package/dist/settings/routes.js +1 -1
  122. package/dist/settings/web/ApiKeysPage.d.ts +8 -0
  123. package/dist/settings/web/ApiKeysPage.js +70 -0
  124. package/dist/settings/web/index.d.ts +1 -0
  125. package/dist/settings/web/index.js +4 -0
  126. package/dist/settings/web/messages.js +62 -0
  127. package/dist/storage/routes.js +1 -1
  128. package/dist/studio/build.js +1 -1
  129. package/dist/studio/generate.js +4 -4
  130. package/dist/studio/module.js +5 -5
  131. package/dist/studio/web/messages.js +2 -2
  132. package/dist/ui/app.js +26 -3
  133. package/dist/ui/i18n/messages.js +2 -2
  134. package/dist/ui/pwa/worker.js +2 -2
  135. package/dist/ui/registry.d.ts +15 -0
  136. package/dist/ui/registry.js +20 -0
  137. 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: kosh <command>
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 (kosh init --help)
28
- apps The apps: installed, disabled, available (kosh apps --help)
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 Kosh modules (\`modules\`, default, or each export)
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/kosh.gen.ts)
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('kosh generate client: pass exactly one of --modules or --from.\n');
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/kosh.gen.ts');
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 kosh generate client.\n`);
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(`kosh ${argv[0]}: ${error instanceof Error ? error.message : String(error)}\n`);
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(`kosh generate client: ${error instanceof Error ? error.message : String(error)}\n`);
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 kosh --help.\n`);
143
+ write(`Unknown command '${argv.join(' ')}'. Run shutro-erp --help.\n`);
144
144
  return 2;
145
145
  }
@@ -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 `kosh init`. Returns the exit code. */
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
- * `kosh init`: sets up an installation in a folder. It asks for (or takes as flags) the database, the first company,
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: kosh init [folder] [options]
42
+ export const INIT_USAGE = `Usage: shutro-erp init [folder] [options]
43
43
 
44
- Sets up a Kosh installation: asks for the database, the first company, the administrator, the apps,
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/kosh
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 KOSH_ADMIN_PASSWORD; at least 12 characters)
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: 'kosh init' });
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: 'kosh init',
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, '-') || 'kosh-app';
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: 'kosh',
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
- '# Kosh settings (kosh init). Keep this file out of version control: it holds credentials.',
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(`KOSH_STORAGE_DIR=${a.storage.dir}`);
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).', `KOSH_BASE_URL=http://localhost:${a.webPort}`, '# Email notifications: an SMTP server (Mailpit in development: smtp://localhost:1025, inbox on :8025).', '# SMTP_URL=smtp://user:password@smtp.example.com:587', '# MAIL_FROM=Kosh <no-reply@example.com>', '# Replies by email (RFC 0006 part 7): the inbound mailbox domain, and the webhook secret your provider sends.', `MAIL_INBOUND_SECRET=${a.inboundSecret}`, '# MAIL_INBOUND_DOMAIN=mail.example.com', '# MAIL_INBOUND_ALIAS=reply', `MAIL_INBOUND_WEBHOOK_SECRET=${a.inboundWebhookSecret}`, '# Without a webhook provider, a mailbox read every 30 s: IMAP_URL=imaps://user:password@host:993/INBOX', '', '# Kosh Studio (RFC 0012): propose (default: changes become patches) or write (development, one server: Studio', '# writes src/modules/<name>/ and applies at once). KOSH_STUDIO_DIR: the project folder (default: the working one).', '# KOSH_STUDIO=write', '# Proposals as pull requests (GitHub) or merge requests (GitLab): the repository and a token with the rights', '# to push branches and open them. Without them, proposals are patches to download.', '# KOSH_STUDIO_GIT_URL=https://github.com/acme/erp', '# KOSH_STUDIO_GIT_TOKEN=', '# KOSH_STUDIO_GIT_BASE=main', '', '# The AI assistant (RFC 0013): off until an administrator switches it on in Settings → AI assistant.', '# ANTHROPIC_API_KEY=', '# KOSH_ASSISTANT_MODEL=claude-opus-5-5', '# KOSH_ASSISTANT_MONTHLY_TOKENS=2000000', '', '# SMS and WhatsApp through Twilio (opt-in per user; a phone number on the user).', '# TWILIO_ACCOUNT_SID=', '# TWILIO_AUTH_TOKEN=', '# TWILIO_SMS_FROM=+1...', '# TWILIO_WHATSAPP_FROM=+1...', '', '# Web push (generated by kosh init; keep them, browsers subscribed with the public key stay subscribed).', `VAPID_PUBLIC_KEY=${a.vapid.publicKey}`, `VAPID_PRIVATE_KEY=${a.vapid.privateKey}`, `VAPID_SUBJECT=${a.admin.email === '' ? 'mailto:admin@localhost' : `mailto:${a.admin.email}`}`);
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
- if (installed?.found === null || installed?.found === undefined) {
622
- task.update('planning');
623
- const plan = await migrate.planMigration({
624
- driver: server.driver,
625
- registry: server.registry.registry,
626
- ignore: FRAMEWORK_TABLES, // the framework's own tables, not a model's: never dropped
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
- const file = await migrate.writeMigrationFile({ dir, name: 'init', plan });
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 `kosh apps` changes later.
638
- await recordInstalledApps(server.driver, server.registry.loaded.modules, ['base', 'settings', 'realtime', 'notify', ...a.apps], `kosh init (${a.admin.login})`);
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: 'kosh init', company }, async (env) => {
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 kosh init again later, or enrol from the API (POST /api/v1/auth/2fa/setup).');
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 `kosh init`. Returns the exit code. */
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(`The ERP framework on Shutro · v${releaseVersion()}`);
857
- ui.intro('Kosh setup', interactive
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
- : 'Kosh setup: press Enter to keep the value in [brackets].');
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 kosh init again (without --yes).');
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', 'Kosh is set up'));
936
- ui.card(`${terminal.emoji ? '🚀 ' : ''}Kosh is set up`, rows, enrolled || values['no-2fa'] === true ? 'green' : 'yellow');
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 `kosh init` again to enrol the authenticator app.');
940
- ui.step('rocket', 'Starting Kosh', 'Dependencies, the API, the back-office, then your browser');
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 `kosh init` again to enrol the authenticator app.');
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(`kosh init stopped: ${error instanceof Error ? error.message : String(error)}`);
991
+ ui.fail(`shutro-erp init stopped: ${error instanceof Error ? error.message : String(error)}`);
968
992
  return 1;
969
993
  }
970
994
  finally {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * After `kosh init`: the project's dependencies installed with the package manager that ran the installer, the API
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 child = spawn(pm, args, {
27
+ const options = {
28
28
  cwd,
29
29
  env: { ...process.env, FORCE_COLOR: '0' },
30
30
  stdio: ['ignore', 'pipe', 'pipe'],
31
- // npm, pnpm… are .cmd scripts on Windows; elsewhere a process group, stopped as a whole.
32
- shell: windows,
33
- detached: !windows,
34
- });
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', 'Kosh is running')} ${ui.style('gray', `back-office ${webUrl} · API ${apiUrl} · Ctrl+C stops both`)}`);
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) => {
@@ -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 Kosh modules a file or package exports: `modules`, the default export, or every module export. */
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 (`kosh apps`). */
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
- /** `kosh migrate [status] …` */
27
+ /** `shutro-erp migrate [status] …` */
28
28
  export declare function runMigrate(argv: readonly string[], write: (t: string) => void, cwd: string): Promise<number>;
29
- /** `kosh seed [module …]` */
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 (`kosh module create <name>`). */
31
+ /** The files of a new module (`shutro-erp module create <name>`). */
32
32
  export declare function moduleFiles(name: string): Record<string, string>;
33
- /** `kosh module create <name>`: scaffolds src/modules/<name>/ and lists it in src/modules.ts. */
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
- /** `kosh user create <login> …` and `kosh user password <login>`. */
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>;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Commands that work on an installation (a folder made by `kosh init`): `kosh migrate`, `kosh seed`,
3
- * `kosh module create`. They read `.env` (DATABASE_URL, storage, ...) and the module list in `src/modules.ts`.
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 Kosh modules a file or package exports: `modules`, the default export, or every module export. */
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 Kosh modules (defineModule from shutro-erp/modules-sdk).`);
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 kosh init, or add it to .env.');
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 kosh init, or pass --modules <file>.`);
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: 'kosh seed',
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 kosh migrate --allow-destructive.\n`);
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 kosh migrate --allow-destructive.\n');
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 kosh init.\n');
176
+ write('No company yet: seed data waits for shutro-erp init.\n');
177
177
  }
178
178
  return 0;
179
179
  }
180
- /** `kosh migrate [status] …` */
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
- /** `kosh seed [module …]` */
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 (`kosh module create <name>`). */
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: \`kosh seed\` (and \`kosh migrate\`) create these records, update them when they
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 \`kosh migrate\`). Schema changes need none: \`kosh migrate\` plans them.
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 \`kosh migrate\`.
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
- /** `kosh module create <name>`: scaffolds src/modules/<name>/ and lists it in src/modules.ts. */
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: kosh module create <name> (lower-case letters, digits and -, e.g. library)\n');
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: kosh apps install ${name} (creates its tables and loads its seed data, in one transaction)\n`);
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
- /** `kosh user create <login> …` and `kosh user password <login>`. */
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: kosh user create <login> [--name <name>] [--email <email>] [--roles a.user,b.manager] [--password <pw>]\n' +
415
- ' kosh user password <login> [--password <pw>]\n');
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 kosh init first.');
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: `kosh user password ${login}` }, (env) => auth.setPassword(env, Number(user.id), password));
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: `kosh user create ${login}` }, async (env) => {
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 });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Terminal questions for `kosh init`, on `node:readline` (no dependency). Answers can be scripted for tests and
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 (kosh init --help).`);
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}`);