shutro-erp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +17 -10
  2. package/dist/account_portal/.kosh/bindings.d.ts +22 -0
  3. package/dist/account_portal/.kosh/bindings.js +15 -0
  4. package/dist/account_portal/.kosh/schema.d.ts +1350 -0
  5. package/dist/account_portal/index.d.ts +3 -0
  6. package/dist/account_portal/index.js +73 -0
  7. package/dist/account_portal/module.d.ts +5983 -0
  8. package/dist/account_portal/module.js +18 -0
  9. package/dist/accounting/.kosh/bindings.js +1 -1
  10. package/dist/accounting/.kosh/schema.d.ts +142 -9
  11. package/dist/accounting/chart.d.ts +40 -8
  12. package/dist/accounting/chart.js +124 -30
  13. package/dist/accounting/closing.d.ts +6 -1
  14. package/dist/accounting/closing.js +5 -1
  15. package/dist/accounting/demo.js +6 -5
  16. package/dist/accounting/index.d.ts +3 -3
  17. package/dist/accounting/index.js +200 -23
  18. package/dist/accounting/module.d.ts +3326 -1086
  19. package/dist/accounting/module.js +408 -66
  20. package/dist/accounting/payments.d.ts +25 -0
  21. package/dist/accounting/payments.js +96 -4
  22. package/dist/accounting/web/RegisterPayment.d.ts +3 -2
  23. package/dist/accounting/web/RegisterPayment.js +39 -6
  24. package/dist/accounting/web/SettingsPage.js +5 -1
  25. package/dist/accounting/web/messages.js +10 -0
  26. package/dist/cli/index.d.ts +1 -1
  27. package/dist/cli/index.js +1 -1
  28. package/dist/cli/init.d.ts +5 -0
  29. package/dist/cli/init.js +195 -53
  30. package/dist/cli/launch.d.ts +16 -0
  31. package/dist/cli/launch.js +138 -0
  32. package/dist/cli/prompt.d.ts +35 -0
  33. package/dist/cli/prompt.js +46 -0
  34. package/dist/cli/templates.d.ts +11 -3
  35. package/dist/cli/templates.js +32 -7
  36. package/dist/cli/tui.d.ts +7 -0
  37. package/dist/cli/tui.js +254 -0
  38. package/dist/cli/ui.d.ts +25 -5
  39. package/dist/cli/ui.js +49 -9
  40. package/dist/contacts/.kosh/bindings.js +1 -1
  41. package/dist/contacts/.kosh/schema.d.ts +3 -1
  42. package/dist/contacts/index.js +24 -1
  43. package/dist/contacts/module.d.ts +42 -0
  44. package/dist/contacts/module.js +3 -1
  45. package/dist/import/convert.d.ts +55 -0
  46. package/dist/import/convert.js +224 -0
  47. package/dist/import/csv.d.ts +28 -0
  48. package/dist/import/csv.js +115 -0
  49. package/dist/import/index.d.ts +33 -0
  50. package/dist/import/index.js +60 -0
  51. package/dist/import/mapping.d.ts +15 -0
  52. package/dist/import/mapping.js +33 -0
  53. package/dist/import/match.d.ts +24 -0
  54. package/dist/import/match.js +60 -0
  55. package/dist/import/models.d.ts +2 -0
  56. package/dist/import/models.js +102 -0
  57. package/dist/import/routes.d.ts +13 -0
  58. package/dist/import/routes.js +392 -0
  59. package/dist/import/run.d.ts +131 -0
  60. package/dist/import/run.js +568 -0
  61. package/dist/import/service.d.ts +153 -0
  62. package/dist/import/service.js +367 -0
  63. package/dist/import/web/ImportButton.d.ts +7 -0
  64. package/dist/import/web/ImportButton.js +31 -0
  65. package/dist/import/web/ImportWizard.d.ts +75 -0
  66. package/dist/import/web/ImportWizard.js +217 -0
  67. package/dist/import/web/index.d.ts +13 -0
  68. package/dist/import/web/index.js +18 -0
  69. package/dist/import/web/messages.d.ts +2 -0
  70. package/dist/import/web/messages.js +139 -0
  71. package/dist/import/worker.d.ts +3 -0
  72. package/dist/import/worker.js +52 -0
  73. package/dist/import/xlsx.d.ts +20 -0
  74. package/dist/import/xlsx.js +192 -0
  75. package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
  76. package/dist/l10n_bd/.kosh/bindings.js +15 -0
  77. package/dist/l10n_bd/.kosh/schema.d.ts +1281 -0
  78. package/dist/l10n_bd/documents.d.ts +8 -0
  79. package/dist/l10n_bd/documents.js +183 -0
  80. package/dist/l10n_bd/index.d.ts +8 -0
  81. package/dist/l10n_bd/index.js +156 -0
  82. package/dist/l10n_bd/module.d.ts +5705 -0
  83. package/dist/l10n_bd/module.js +163 -0
  84. package/dist/l10n_bd/mushak.d.ts +67 -0
  85. package/dist/l10n_bd/mushak.js +139 -0
  86. package/dist/l10n_bd/routes.d.ts +3 -0
  87. package/dist/l10n_bd/routes.js +205 -0
  88. package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
  89. package/dist/l10n_bd/web/MushakPage.js +53 -0
  90. package/dist/l10n_bd/web/index.d.ts +13 -0
  91. package/dist/l10n_bd/web/index.js +18 -0
  92. package/dist/l10n_bd/web/messages.d.ts +2 -0
  93. package/dist/l10n_bd/web/messages.js +101 -0
  94. package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
  95. package/dist/l10n_in/.kosh/bindings.js +15 -0
  96. package/dist/l10n_in/.kosh/schema.d.ts +1343 -0
  97. package/dist/l10n_in/documents.d.ts +8 -0
  98. package/dist/l10n_in/documents.js +114 -0
  99. package/dist/l10n_in/einvoice.d.ts +253 -0
  100. package/dist/l10n_in/einvoice.js +485 -0
  101. package/dist/l10n_in/gstr.d.ts +109 -0
  102. package/dist/l10n_in/gstr.js +430 -0
  103. package/dist/l10n_in/index.d.ts +20 -0
  104. package/dist/l10n_in/index.js +232 -0
  105. package/dist/l10n_in/module.d.ts +7823 -0
  106. package/dist/l10n_in/module.js +427 -0
  107. package/dist/l10n_in/routes-einvoice.d.ts +15 -0
  108. package/dist/l10n_in/routes-einvoice.js +124 -0
  109. package/dist/l10n_in/routes.d.ts +17 -0
  110. package/dist/l10n_in/routes.js +282 -0
  111. package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
  112. package/dist/l10n_in/web/EInvoicePanel.js +63 -0
  113. package/dist/l10n_in/web/GstPage.d.ts +7 -0
  114. package/dist/l10n_in/web/GstPage.js +57 -0
  115. package/dist/l10n_in/web/GstSettings.d.ts +10 -0
  116. package/dist/l10n_in/web/GstSettings.js +36 -0
  117. package/dist/l10n_in/web/index.d.ts +15 -0
  118. package/dist/l10n_in/web/index.js +22 -0
  119. package/dist/l10n_in/web/messages.d.ts +2 -0
  120. package/dist/l10n_in/web/messages.js +166 -0
  121. package/dist/modules-sdk/demo.d.ts +2 -0
  122. package/dist/modules-sdk/demo.js +7 -3
  123. package/dist/modules-sdk/index.d.ts +2 -2
  124. package/dist/modules-sdk/index.js +1 -1
  125. package/dist/modules-sdk/manifest.d.ts +132 -0
  126. package/dist/modules-sdk/manifest.js +22 -0
  127. package/dist/notify/inbox.js +21 -0
  128. package/dist/notify/messages.js +4 -0
  129. package/dist/notify/models.d.ts +1 -1
  130. package/dist/notify/models.js +2 -0
  131. package/dist/notify/web/messages.js +4 -0
  132. package/dist/portal/.kosh/bindings.d.ts +22 -0
  133. package/dist/portal/.kosh/bindings.js +15 -0
  134. package/dist/portal/.kosh/schema.d.ts +346 -0
  135. package/dist/portal/announce.d.ts +16 -0
  136. package/dist/portal/announce.js +117 -0
  137. package/dist/portal/documents.d.ts +17 -0
  138. package/dist/portal/documents.js +61 -0
  139. package/dist/portal/index.d.ts +20 -0
  140. package/dist/portal/index.js +65 -0
  141. package/dist/portal/module.d.ts +1405 -0
  142. package/dist/portal/module.js +84 -0
  143. package/dist/portal/routes-documents.d.ts +2 -0
  144. package/dist/portal/routes-documents.js +391 -0
  145. package/dist/portal/routes.d.ts +13 -0
  146. package/dist/portal/routes.js +313 -0
  147. package/dist/portal/rules.d.ts +11 -0
  148. package/dist/portal/rules.js +25 -0
  149. package/dist/portal/web/PortalAccess.d.ts +8 -0
  150. package/dist/portal/web/PortalAccess.js +66 -0
  151. package/dist/portal/web/index.d.ts +12 -0
  152. package/dist/portal/web/index.js +17 -0
  153. package/dist/portal/web/messages.d.ts +2 -0
  154. package/dist/portal/web/messages.js +33 -0
  155. package/dist/product/.kosh/bindings.js +1 -1
  156. package/dist/product/.kosh/schema.d.ts +12 -3
  157. package/dist/product/demo.js +6 -5
  158. package/dist/product/index.d.ts +1 -0
  159. package/dist/product/index.js +17 -4
  160. package/dist/product/module.d.ts +346 -14
  161. package/dist/product/module.js +48 -18
  162. package/dist/product/taxes.d.ts +59 -0
  163. package/dist/product/taxes.js +61 -0
  164. package/dist/project/.kosh/bindings.js +1 -1
  165. package/dist/project/.kosh/schema.d.ts +3 -1
  166. package/dist/project/module.d.ts +20 -0
  167. package/dist/project_portal/.kosh/bindings.d.ts +22 -0
  168. package/dist/project_portal/.kosh/bindings.js +15 -0
  169. package/dist/project_portal/.kosh/schema.d.ts +671 -0
  170. package/dist/project_portal/index.d.ts +9 -0
  171. package/dist/project_portal/index.js +42 -0
  172. package/dist/project_portal/module.d.ts +2142 -0
  173. package/dist/project_portal/module.js +22 -0
  174. package/dist/purchase/.kosh/bindings.js +1 -1
  175. package/dist/purchase/.kosh/schema.d.ts +15 -4
  176. package/dist/purchase/demo.js +3 -3
  177. package/dist/purchase/index.js +1 -1
  178. package/dist/purchase/module.d.ts +232 -8
  179. package/dist/purchase/module.js +51 -9
  180. package/dist/purchase_account/.kosh/bindings.js +1 -1
  181. package/dist/purchase_account/.kosh/schema.d.ts +144 -10
  182. package/dist/purchase_account/module.d.ts +2008 -641
  183. package/dist/purchase_account/module.js +2 -2
  184. package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
  185. package/dist/purchase_portal/.kosh/bindings.js +15 -0
  186. package/dist/purchase_portal/.kosh/schema.d.ts +822 -0
  187. package/dist/purchase_portal/index.d.ts +9 -0
  188. package/dist/purchase_portal/index.js +53 -0
  189. package/dist/purchase_portal/module.d.ts +2758 -0
  190. package/dist/purchase_portal/module.js +18 -0
  191. package/dist/reporting/documents.js +20 -3
  192. package/dist/reporting/index.d.ts +1 -0
  193. package/dist/reporting/index.js +1 -0
  194. package/dist/reporting/routes-documents.js +9 -2
  195. package/dist/reporting/words.d.ts +11 -0
  196. package/dist/reporting/words.js +229 -0
  197. package/dist/sale_account/.kosh/bindings.js +1 -1
  198. package/dist/sale_account/.kosh/schema.d.ts +144 -10
  199. package/dist/sale_account/module.d.ts +2008 -641
  200. package/dist/sale_account/module.js +3 -3
  201. package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
  202. package/dist/sale_portal/.kosh/bindings.js +15 -0
  203. package/dist/sale_portal/.kosh/schema.d.ts +736 -0
  204. package/dist/sale_portal/index.d.ts +9 -0
  205. package/dist/sale_portal/index.js +71 -0
  206. package/dist/sale_portal/module.d.ts +2718 -0
  207. package/dist/sale_portal/module.js +19 -0
  208. package/dist/sale_project/.kosh/bindings.js +1 -1
  209. package/dist/sale_project/.kosh/schema.d.ts +15 -4
  210. package/dist/sale_project/module.d.ts +390 -10
  211. package/dist/sales/.kosh/bindings.js +1 -1
  212. package/dist/sales/.kosh/schema.d.ts +15 -4
  213. package/dist/sales/demo.js +3 -3
  214. package/dist/sales/index.js +67 -1
  215. package/dist/sales/module.d.ts +232 -8
  216. package/dist/sales/module.js +51 -12
  217. package/dist/server/auth/base.js +2 -0
  218. package/dist/server/auth/service.d.ts +2 -0
  219. package/dist/server/auth/service.js +7 -1
  220. package/dist/server/index.d.ts +1 -1
  221. package/dist/server/index.js +1 -1
  222. package/dist/server/server.js +26 -1
  223. package/dist/ui/api.d.ts +2 -0
  224. package/dist/ui/app.js +1 -1
  225. package/dist/ui/context.d.ts +3 -1
  226. package/dist/ui/context.js +2 -2
  227. package/dist/ui/index.d.ts +1 -1
  228. package/dist/ui/index.js +1 -1
  229. package/dist/ui/lib/format.d.ts +2 -0
  230. package/dist/ui/lib/format.js +3 -1
  231. package/dist/ui/views/FormView.js +19 -6
  232. package/package.json +50 -2
@@ -3,6 +3,52 @@
3
3
  * non-interactive installs (`--yes` with flags): every question goes through {@link Prompter}.
4
4
  */
5
5
  import { createInterface } from 'node:readline';
6
+ /**
7
+ * One of `choices`: the prompter's list when it has one, otherwise the question `fallback` (pipes, scripts, tests),
8
+ * answered with an option's number, value or label.
9
+ */
10
+ export async function choose(prompter, q) {
11
+ if (prompter.select !== undefined)
12
+ return prompter.select(q);
13
+ const find = (answer) => {
14
+ const a = answer.trim().toLowerCase();
15
+ if (/^\d+$/.test(a))
16
+ return q.choices[Number(a) - 1];
17
+ return q.choices.find((c) => c.value.toLowerCase() === a || c.label.toLowerCase() === a);
18
+ };
19
+ const answer = await prompter.ask({
20
+ message: q.fallback,
21
+ ...(q.default === undefined ? {} : { default: q.default }),
22
+ validate: (v) => find(v) === undefined ? `One of ${q.choices.map((c) => c.value).join(', ')}` : null,
23
+ });
24
+ return find(answer).value;
25
+ }
26
+ /** Several of `choices`: the prompter's list, or a comma-separated answer to `fallback`. */
27
+ export async function chooseMany(prompter, q) {
28
+ if (prompter.multiselect !== undefined)
29
+ return prompter.multiselect(q);
30
+ const known = new Set(q.choices.map((c) => c.value));
31
+ const answer = await prompter.ask({
32
+ message: q.fallback,
33
+ default: (q.defaults ?? []).join(','),
34
+ validate: (v) => {
35
+ const items = v
36
+ .split(',')
37
+ .map((x) => x.trim())
38
+ .filter((x) => x !== '');
39
+ const unknown = items.filter((x) => !known.has(x));
40
+ if (unknown.length > 0)
41
+ return `Unknown: ${unknown.join(', ')}`;
42
+ return q.required === true && items.length === 0 ? 'Choose at least one.' : null;
43
+ },
44
+ });
45
+ return [
46
+ ...new Set(answer
47
+ .split(',')
48
+ .map((x) => x.trim())
49
+ .filter((x) => x !== '')),
50
+ ];
51
+ }
6
52
  /** Thrown when the input ends before every question is answered (piped answers ran out, Ctrl+D). */
7
53
  export class InputEndedError extends Error {
8
54
  constructor(question) {
@@ -2,7 +2,7 @@
2
2
  * The starter project `kosh 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
- export type AppName = 'permissions' | 'reporting' | 'messaging' | 'contacts' | 'product' | 'sales' | 'purchase' | 'accounting' | 'sale_account' | 'purchase_account' | 'project' | 'sale_project';
5
+ export type AppName = 'permissions' | 'reporting' | 'messaging' | 'contacts' | 'product' | 'sales' | 'purchase' | 'accounting' | 'sale_account' | 'purchase_account' | 'project' | 'sale_project' | 'l10n_bd' | 'l10n_in';
6
6
  export interface AppSpec {
7
7
  readonly label: string;
8
8
  /** Shown next to the app in the installer (where the terminal shows pictographs). */
@@ -13,6 +13,8 @@ export interface AppSpec {
13
13
  readonly web?: string;
14
14
  /** A bridge (RFC 0008 §5.1): not offered in the installer; proposed when the apps it connects are installed. */
15
15
  readonly bridge?: readonly AppName[];
16
+ /** A localization (RFC 0016): installed, with its bridged apps, for a company in this country (ISO 3166). */
17
+ readonly country?: string;
16
18
  }
17
19
  /** The apps `kosh init` can install (base and Settings are always there), in the order the installer lists them. */
18
20
  export declare const APPS: Readonly<Record<AppName, AppSpec>>;
@@ -20,6 +22,8 @@ export interface StarterOptions {
20
22
  readonly name: string;
21
23
  readonly apps: readonly AppName[];
22
24
  readonly port: number;
25
+ /** The back-office's port in development (default 5173). */
26
+ readonly webPort?: number;
23
27
  readonly koshVersion: string;
24
28
  /** The database name of the Docker PostgreSQL service (compose.yaml). */
25
29
  readonly dockerDb?: string;
@@ -52,5 +56,9 @@ export declare function tsconfig(): string;
52
56
  export declare function readme(options: StarterOptions): string;
53
57
  /** The apps the installer offers (every app but the bridges, which come by themselves). */
54
58
  export declare const SELECTABLE_APPS: readonly AppName[];
55
- /** The bridges whose every connected app is in `apps`. */
56
- export declare function bridgesFor(apps: readonly AppName[]): AppName[];
59
+ /** The bridges whose every connected app is in `apps`, and the localization of `country` among them. */
60
+ export declare function bridgesFor(apps: readonly AppName[], country?: string): AppName[];
61
+ /** The names of the countries with a localization (the installer's list). */
62
+ export declare const COUNTRY_NAMES: Readonly<Record<string, string>>;
63
+ /** The countries with a localization, for the installer's question. */
64
+ export declare const LOCALIZED_COUNTRIES: readonly string[];
@@ -67,6 +67,22 @@ export const APPS = {
67
67
  server: 'saleProject',
68
68
  bridge: ['sales', 'project'],
69
69
  },
70
+ l10n_bd: {
71
+ label: 'Bangladesh: chart of accounts, VAT and SD, VDS and TDS, BIN and TIN, July–June year',
72
+ icon: '🇧🇩',
73
+ server: 'l10nBd',
74
+ web: 'registerL10nBdWeb',
75
+ bridge: ['accounting'],
76
+ country: 'BD',
77
+ },
78
+ l10n_in: {
79
+ label: 'India: GST (CGST / SGST / IGST), cess, reverse charge, GSTIN, HSN / SAC, TDS, April–March year',
80
+ icon: '🇮🇳',
81
+ server: 'l10nIn',
82
+ web: 'registerL10nInWeb',
83
+ bridge: ['accounting'],
84
+ country: 'IN',
85
+ },
70
86
  };
71
87
  const REACT = {
72
88
  react: '^19.1.0',
@@ -125,10 +141,11 @@ import { notify } from 'shutro-erp/notify';
125
141
  import { realtime } from 'shutro-erp/realtime';
126
142
  import { settings } from 'shutro-erp/settings';
127
143
  import { assistant } from 'shutro-erp/assistant';
144
+ import { dataImport } from 'shutro-erp/import';
128
145
  import { studio } from 'shutro-erp/studio';
129
146
  ${imports.join('\n')}
130
147
 
131
- export const modules = [base, settings, realtime, notify, mail, automation, studio, assistant${apps.map((a) => `, ${APPS[a].server}`).join('')}];
148
+ export const modules = [base, settings, realtime, notify, mail, automation, studio, dataImport, assistant${apps.map((a) => `, ${APPS[a].server}`).join('')}];
132
149
  `;
133
150
  }
134
151
  export function serverTs() {
@@ -164,8 +181,8 @@ const api = process.env['KOSH_API'] ?? 'http://localhost:${options.port}';
164
181
  export default defineConfig({
165
182
  // koshPwa: the service worker (the app opens offline and gets web push)
166
183
  plugins: [react(), tailwindcss(), koshPwa()],
167
- server: { proxy: { '/api': { target: api, ws: true } } },
168
- preview: { proxy: { '/api': { target: api, ws: true } } },
184
+ server: { port: ${options.webPort ?? 5173}, proxy: { '/api': { target: api, ws: true } } },
185
+ preview: { port: ${options.webPort ?? 5173}, proxy: { '/api': { target: api, ws: true } } },
169
186
  });
170
187
  `;
171
188
  }
@@ -201,6 +218,7 @@ export function mainTsx(_options) {
201
218
  { name: 'automation', fn: 'registerAutomationWeb' },
202
219
  { name: 'studio', fn: 'registerStudioWeb' },
203
220
  { name: 'assistant', fn: 'registerAssistantWeb' },
221
+ { name: 'import', fn: 'registerImportWeb' },
204
222
  ].map(({ name, fn }) => {
205
223
  const messages = name === 'permissions' ? null : `${name}Messages`;
206
224
  return {
@@ -418,7 +436,7 @@ A [Kosh](https://www.npmjs.com/package/shutro-erp) installation, created with \`
418
436
  \`\`\`sh
419
437
  npm install
420
438
  npm start # the API on http://localhost:${options.port}
421
- npm run web # the back-office on http://localhost:5173 (sign in as the administrator)
439
+ npm run web # the back-office on http://localhost:${options.webPort ?? 5173} (sign in as the administrator)
422
440
  npx kosh migrate # after changing modules: plan and apply database migrations, load seed data
423
441
  npx kosh seed # load seed data again
424
442
  # Settings → Apps in the back-office installs, enables and disables apps (or, in the terminal:)
@@ -435,9 +453,16 @@ Settings (database, file storage, port) are in \`.env\`, which is not committed.
435
453
  }
436
454
  /** The apps the installer offers (every app but the bridges, which come by themselves). */
437
455
  export const SELECTABLE_APPS = Object.keys(APPS).filter((a) => APPS[a].bridge === undefined);
438
- /** The bridges whose every connected app is in `apps`. */
439
- export function bridgesFor(apps) {
456
+ /** The bridges whose every connected app is in `apps`, and the localization of `country` among them. */
457
+ export function bridgesFor(apps, country = '') {
440
458
  return Object.keys(APPS).filter((a) => {
441
- return APPS[a].bridge?.every((b) => apps.includes(b)) === true;
459
+ const spec = APPS[a];
460
+ if (spec.country !== undefined && spec.country !== country)
461
+ return false;
462
+ return spec.bridge?.every((b) => apps.includes(b)) === true;
442
463
  });
443
464
  }
465
+ /** The names of the countries with a localization (the installer's list). */
466
+ export const COUNTRY_NAMES = { BD: 'Bangladesh', IN: 'India' };
467
+ /** The countries with a localization, for the installer's question. */
468
+ export const LOCALIZED_COUNTRIES = Object.values(APPS).flatMap((a) => a.country === undefined ? [] : [a.country]);
@@ -0,0 +1,7 @@
1
+ import type { Prompter } from './prompt.js';
2
+ import { type Terminal } from './ui.js';
3
+ /** Thrown when the user stops the installer (Ctrl+C) during a question. */
4
+ export declare class CancelledError extends Error {
5
+ constructor();
6
+ }
7
+ export declare function interactivePrompter(input: NodeJS.ReadStream, output: NodeJS.WriteStream, terminal: Terminal): Prompter;
@@ -0,0 +1,254 @@
1
+ /**
2
+ * The installer's questions on a terminal (no dependency): one question at a time on the rail of `ui.ts`, answered
3
+ * with the keyboard — text with the default shown until typed over, secrets masked, choices with ↑ ↓ and Enter,
4
+ * several choices with Space, Yes / No with ← →. An answered question collapses to one dimmed line; a refused
5
+ * answer shows why under the input. Ctrl+C stops the installer. Not a terminal (pipes, tests): `prompt.ts` asks
6
+ * line by line instead.
7
+ */
8
+ import { emitKeypressEvents } from 'node:readline';
9
+ import { ESC, RAIL, stylerFor, width } from './ui.js';
10
+ /** Thrown when the user stops the installer (Ctrl+C) during a question. */
11
+ export class CancelledError extends Error {
12
+ constructor() {
13
+ super('Cancelled.');
14
+ this.name = 'CancelledError';
15
+ }
16
+ }
17
+ export function interactivePrompter(input, output, terminal) {
18
+ const s = stylerFor(terminal);
19
+ const columns = () => Math.max(40, output.columns ?? terminal.columns);
20
+ /** Terminal rows `text` takes (long lines wrap). */
21
+ const rows = (text) => text
22
+ .split('\n')
23
+ .slice(0, -1)
24
+ .reduce((n, line) => n + Math.max(1, Math.ceil(width(line) / columns())), 0);
25
+ const bar = (status) => status === 'error'
26
+ ? s('yellow', RAIL.bar)
27
+ : status === 'active'
28
+ ? s('cyan', RAIL.bar)
29
+ : s('gray', RAIL.bar);
30
+ const head = (status, message) => {
31
+ const mark = status === 'active'
32
+ ? s('cyan', RAIL.active)
33
+ : status === 'error'
34
+ ? s('yellow', RAIL.error)
35
+ : status === 'cancel'
36
+ ? s('red', RAIL.cancel)
37
+ : s('green', RAIL.done);
38
+ return `${s('gray', RAIL.bar)}\n${mark} ${status === 'done' ? message : s('bold', message)}\n`;
39
+ };
40
+ const foot = (status, error) => status === 'error'
41
+ ? `${s('yellow', RAIL.end)} ${s('yellow', error ?? '')}\n`
42
+ : status === 'active'
43
+ ? `${s('cyan', RAIL.end)}\n`
44
+ : '';
45
+ const cursor = s('inverse', ' ');
46
+ function run(widget) {
47
+ return new Promise((resolve, reject) => {
48
+ let drawn = 0;
49
+ let error = null;
50
+ const draw = (status) => {
51
+ const text = widget.frame(status, error);
52
+ if (drawn > 0)
53
+ output.write(`${ESC}${drawn}A\r`);
54
+ output.write(`${ESC}J${text}`);
55
+ drawn = rows(text);
56
+ };
57
+ const finish = () => {
58
+ input.off('keypress', onKey);
59
+ if (input.isTTY)
60
+ input.setRawMode(false);
61
+ input.pause();
62
+ output.write(`${ESC}?25h`);
63
+ };
64
+ const onKey = (text, key = {}) => {
65
+ if (key.ctrl === true && (key.name === 'c' || key.name === 'd')) {
66
+ draw('cancel');
67
+ finish();
68
+ reject(new CancelledError());
69
+ return;
70
+ }
71
+ const action = widget.key(text, key);
72
+ if (action === 'submit') {
73
+ const value = widget.value();
74
+ const problem = widget.validate?.(value) ?? null;
75
+ if (problem === null) {
76
+ error = null;
77
+ draw('done');
78
+ finish();
79
+ resolve(value);
80
+ return;
81
+ }
82
+ error = problem;
83
+ draw('error');
84
+ return;
85
+ }
86
+ draw(error === null ? 'active' : 'error');
87
+ };
88
+ emitKeypressEvents(input);
89
+ if (input.isTTY)
90
+ input.setRawMode(true);
91
+ input.resume();
92
+ output.write(`${ESC}?25l`);
93
+ input.on('keypress', onKey);
94
+ draw('active');
95
+ });
96
+ }
97
+ /** The question without the indentation of sub-questions (the rail aligns them). */
98
+ const label = (message) => message.trim();
99
+ function text(q) {
100
+ let value = '';
101
+ return run({
102
+ frame(status, error) {
103
+ const shown = q.secret === true ? '•'.repeat(value.length) : value;
104
+ if (status === 'done' || status === 'cancel') {
105
+ const answer = q.secret === true
106
+ ? s('gray', '•'.repeat(Math.min(12, (value || q.default || '').length)))
107
+ : s('gray', value === '' ? (q.default ?? '') : value);
108
+ return `${head(status, label(q.message))}${bar(status)} ${status === 'cancel' ? s('gray', 'cancelled') : answer}\n`;
109
+ }
110
+ const placeholder = value === '' && q.default !== undefined && q.default !== '' && q.secret !== true
111
+ ? s('gray', q.default)
112
+ : '';
113
+ return `${head(status, label(q.message))}${bar(status)} ${shown}${cursor}${placeholder}\n${foot(status, error)}`;
114
+ },
115
+ key(input, key) {
116
+ if (key.name === 'return' || key.name === 'enter')
117
+ return 'submit';
118
+ if (key.name === 'backspace')
119
+ value = value.slice(0, -1);
120
+ else if (key.ctrl === true && key.name === 'u')
121
+ value = '';
122
+ else if (key.name === 'tab' && value === '' && q.default !== undefined)
123
+ value = q.default;
124
+ else if (input !== undefined &&
125
+ key.ctrl !== true &&
126
+ key.meta !== true &&
127
+ [...input].every((ch) => ch >= ' ' && ch !== '\x7f'))
128
+ value += input;
129
+ return undefined;
130
+ },
131
+ value: () => (value.trim() === '' ? (q.default ?? '') : value.trim()),
132
+ validate: (v) => q.validate?.(v) ?? null,
133
+ });
134
+ }
135
+ const labelWidth = (choices) => Math.max(...choices.map((c) => width(c.label)));
136
+ /** An option: its mark, its label (bright when focused, with a ❯ pointer), its hint aligned after it. */
137
+ const optionLine = (c, mark, focused, pad) => {
138
+ const name = focused ? s('bold', c.label) : c.label;
139
+ const gap = ' '.repeat(Math.max(0, pad - width(c.label)) + 2);
140
+ const hint = c.hint === undefined ? '' : `${gap}${s('gray', c.hint)}`;
141
+ return `${focused ? s('cyan', '❯') : ' '} ${mark} ${name}${hint}`;
142
+ };
143
+ function select(q) {
144
+ let at = Math.max(0, q.choices.findIndex((c) => c.value === q.default));
145
+ return run({
146
+ frame(status) {
147
+ const chosen = q.choices[at];
148
+ if (status === 'done' || status === 'cancel')
149
+ return `${head(status, q.message)}${bar(status)} ${s('gray', status === 'cancel' ? 'cancelled' : (chosen?.label ?? ''))}\n`;
150
+ const list = q.choices
151
+ .map((c, i) => `${bar(status)} ${optionLine(c, i === at ? s('cyan', '●') : s('gray', '○'), i === at, labelWidth(q.choices))}`)
152
+ .join('\n');
153
+ return `${head(status, q.message)}${list}\n${foot(status, null)}`;
154
+ },
155
+ key(_text, key) {
156
+ if (key.name === 'return' || key.name === 'enter')
157
+ return 'submit';
158
+ if (key.name === 'up' || key.name === 'k')
159
+ at = (at - 1 + q.choices.length) % q.choices.length;
160
+ if (key.name === 'down' || key.name === 'j' || key.name === 'tab')
161
+ at = (at + 1) % q.choices.length;
162
+ return undefined;
163
+ },
164
+ value: () => q.choices[at]?.value ?? '',
165
+ });
166
+ }
167
+ function multiselect(q) {
168
+ let at = 0;
169
+ const picked = new Set(q.defaults ?? []);
170
+ const values = () => q.choices.filter((c) => picked.has(c.value)).map((c) => c.value);
171
+ return run({
172
+ frame(status, error) {
173
+ if (status === 'done' || status === 'cancel') {
174
+ const names = q.choices.filter((c) => picked.has(c.value)).map((c) => c.label);
175
+ return `${head(status, q.message)}${bar(status)} ${s('gray', status === 'cancel' ? 'cancelled' : names.length === 0 ? 'none' : names.join(', '))}\n`;
176
+ }
177
+ const list = q.choices
178
+ .map((c, i) => {
179
+ const mark = picked.has(c.value) ? s('green', '◼') : s('gray', '◻');
180
+ return `${bar(status)} ${optionLine(c, mark, i === at, labelWidth(q.choices))}`;
181
+ })
182
+ .join('\n');
183
+ const help = s('gray', '↑↓ move · Space toggle · a all · Enter confirm');
184
+ return `${head(status, q.message)}${list}\n${bar(status)} ${help}\n${foot(status, error)}`;
185
+ },
186
+ key(text, key) {
187
+ if (key.name === 'return' || key.name === 'enter')
188
+ return 'submit';
189
+ if (key.name === 'up' || key.name === 'k')
190
+ at = (at - 1 + q.choices.length) % q.choices.length;
191
+ else if (key.name === 'down' || key.name === 'j' || key.name === 'tab')
192
+ at = (at + 1) % q.choices.length;
193
+ else if (key.name === 'space' || text === ' ') {
194
+ const c = q.choices[at];
195
+ if (c !== undefined) {
196
+ if (picked.has(c.value))
197
+ picked.delete(c.value);
198
+ else
199
+ picked.add(c.value);
200
+ }
201
+ }
202
+ else if (key.name === 'a') {
203
+ if (picked.size === q.choices.length)
204
+ picked.clear();
205
+ else
206
+ for (const c of q.choices)
207
+ picked.add(c.value);
208
+ }
209
+ return undefined;
210
+ },
211
+ value: values,
212
+ validate: (v) => (q.required === true && v.length === 0 ? 'Choose at least one.' : null),
213
+ });
214
+ }
215
+ function confirm(message, fallback) {
216
+ let yes = fallback;
217
+ return run({
218
+ frame(status) {
219
+ if (status === 'done' || status === 'cancel')
220
+ return `${head(status, label(message))}${bar(status)} ${s('gray', status === 'cancel' ? 'cancelled' : yes ? 'Yes' : 'No')}\n`;
221
+ const option = (on, name) => on ? `${s('cyan', '●')} ${name}` : `${s('gray', '○')} ${s('gray', name)}`;
222
+ return `${head(status, label(message))}${bar(status)} ${option(yes, 'Yes')} ${s('gray', '/')} ${option(!yes, 'No')}\n${foot(status, null)}`;
223
+ },
224
+ key(text, key) {
225
+ if (key.name === 'return' || key.name === 'enter')
226
+ return 'submit';
227
+ if (key.name === 'left' ||
228
+ key.name === 'right' ||
229
+ key.name === 'tab' ||
230
+ key.name === 'h' ||
231
+ key.name === 'l')
232
+ yes = !yes;
233
+ else if (text?.toLowerCase() === 'y')
234
+ yes = true;
235
+ else if (text?.toLowerCase() === 'n')
236
+ yes = false;
237
+ return undefined;
238
+ },
239
+ value: () => yes,
240
+ });
241
+ }
242
+ return {
243
+ ask: text,
244
+ confirm,
245
+ select,
246
+ multiselect,
247
+ close() {
248
+ if (input.isTTY)
249
+ input.setRawMode(false);
250
+ input.pause();
251
+ output.write(`${ESC}?25h`);
252
+ },
253
+ };
254
+ }
package/dist/cli/ui.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * The installer's look in the terminal, without dependencies: a logo, numbered steps with icons, spinners for long
3
- * work, status lines (✔ ▲ ✖) and a summary card. Colour, Unicode and animation are detected from the terminal
4
- * (NO_COLOR, FORCE_COLOR, KOSH_ASCII=1, TERM=dumb, Windows consoles without Unicode); output that is not a terminal
5
- * (tests, logs, CI) gets plain lines with the same words and no escape codes.
2
+ * The installer's look in the terminal, without dependencies: a logo, numbered steps, spinners for long work,
3
+ * status lines (✔ ▲ ✖) and a summary card. On a Unicode terminal everything hangs on one rail down the left (│),
4
+ * the questions of the interactive prompter included (`tui.ts`). Colour, Unicode and animation are detected from
5
+ * the terminal (NO_COLOR, FORCE_COLOR, KOSH_ASCII=1, TERM=dumb, Windows consoles without Unicode); output that is not
6
+ * a terminal (tests, logs, CI) gets plain lines with the same words and no escape codes.
6
7
  */
7
8
  /** What the terminal can show. */
8
9
  export interface Terminal {
@@ -23,8 +24,23 @@ export declare function detectTerminal(stream: {
23
24
  readonly isTTY?: boolean;
24
25
  readonly columns?: number;
25
26
  }, env?: Env, platform?: string): Terminal;
27
+ export declare const ESC = "\u001B[";
26
28
  /** The colours and weights the installer uses. */
27
- export type Style = 'bold' | 'dim' | 'green' | 'yellow' | 'red' | 'cyan' | 'magenta' | 'gray';
29
+ export type Style = 'bold' | 'dim' | 'inverse' | 'underline' | 'green' | 'yellow' | 'red' | 'cyan' | 'magenta' | 'gray';
30
+ /** Styles text for `terminal` (no escape codes without colour). */
31
+ export declare function stylerFor(terminal: Terminal): (name: Style, text: string) => string;
32
+ /** The rail and its marks (Unicode terminals that animate), shared with the interactive prompter. */
33
+ export declare const RAIL: {
34
+ readonly bar: '│';
35
+ readonly start: '┌';
36
+ readonly end: '└';
37
+ readonly active: '◆';
38
+ readonly done: '◇';
39
+ readonly error: '▲';
40
+ readonly cancel: '■';
41
+ };
42
+ /** Whether `terminal` gets the rail look (a TTY that shows Unicode). */
43
+ export declare const railed: (terminal: Terminal) => boolean;
28
44
  /** Columns a string takes: escape codes take none, pictographs two. */
29
45
  export declare function width(text: string): number;
30
46
  /** The step icons: a pictograph where the terminal shows them, a symbol otherwise. */
@@ -58,6 +74,10 @@ export interface Ui {
58
74
  style(name: Style, text: string): string;
59
75
  icon(name: IconName): string;
60
76
  banner(subtitle: string): Promise<void>;
77
+ /** Opens the rail (┌ title) under the banner, with a hint line. */
78
+ intro(title: string, hint: string): void;
79
+ /** Closes the rail (└ text). */
80
+ outro(text: string): void;
61
81
  /** A numbered step title: "🗄️ Step 1/6 · Database". */
62
82
  step(icon: IconName, title: string, hint?: string): void;
63
83
  ok(text: string): void;
package/dist/cli/ui.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * The installer's look in the terminal, without dependencies: a logo, numbered steps with icons, spinners for long
3
- * work, status lines (✔ ▲ ✖) and a summary card. Colour, Unicode and animation are detected from the terminal
4
- * (NO_COLOR, FORCE_COLOR, KOSH_ASCII=1, TERM=dumb, Windows consoles without Unicode); output that is not a terminal
5
- * (tests, logs, CI) gets plain lines with the same words and no escape codes.
2
+ * The installer's look in the terminal, without dependencies: a logo, numbered steps, spinners for long work,
3
+ * status lines (✔ ▲ ✖) and a summary card. On a Unicode terminal everything hangs on one rail down the left (│),
4
+ * the questions of the interactive prompter included (`tui.ts`). Colour, Unicode and animation are detected from
5
+ * the terminal (NO_COLOR, FORCE_COLOR, KOSH_ASCII=1, TERM=dumb, Windows consoles without Unicode); output that is not
6
+ * a terminal (tests, logs, CI) gets plain lines with the same words and no escape codes.
6
7
  */
7
8
  /** Plain output: logs, tests, pipes. */
8
9
  export const PLAIN_TERMINAL = Object.freeze({
@@ -32,11 +33,13 @@ export function detectTerminal(stream, env = process.env, platform = process.pla
32
33
  columns: Math.max(40, Math.min(stream.columns ?? 80, 120)),
33
34
  };
34
35
  }
35
- const ESC = '\u001b[';
36
+ export const ESC = '\u001b[';
36
37
  const paint = (open, close) => (enabled) => (text) => enabled ? `${ESC}${open}m${text}${ESC}${close}m` : text;
37
38
  const styles = {
38
39
  bold: paint('1', '22'),
39
40
  dim: paint('2', '22'),
41
+ inverse: paint('7', '27'),
42
+ underline: paint('4', '24'),
40
43
  green: paint('32', '39'),
41
44
  yellow: paint('33', '39'),
42
45
  red: paint('31', '39'),
@@ -44,6 +47,22 @@ const styles = {
44
47
  magenta: paint('35', '39'),
45
48
  gray: paint('90', '39'),
46
49
  };
50
+ /** Styles text for `terminal` (no escape codes without colour). */
51
+ export function stylerFor(terminal) {
52
+ return (name, text) => styles[name](terminal.color)(text);
53
+ }
54
+ /** The rail and its marks (Unicode terminals that animate), shared with the interactive prompter. */
55
+ export const RAIL = {
56
+ bar: '│',
57
+ start: '┌',
58
+ end: '└',
59
+ active: '◆',
60
+ done: '◇',
61
+ error: '▲',
62
+ cancel: '■',
63
+ };
64
+ /** Whether `terminal` gets the rail look (a TTY that shows Unicode). */
65
+ export const railed = (terminal) => terminal.unicode && terminal.animate;
47
66
  /** Columns a string takes: escape codes take none, pictographs two. */
48
67
  export function width(text) {
49
68
  let n = 0;
@@ -86,7 +105,9 @@ const SPINNER_UNICODE = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧',
86
105
  const SPINNER_ASCII = ['|', '/', '-', '\\'];
87
106
  /** A {@link Ui} writing through `out`. */
88
107
  export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
89
- const s = (name, text) => styles[name](terminal.color)(text);
108
+ const s = stylerFor(terminal);
109
+ const rail = railed(terminal);
110
+ const bar = s('gray', RAIL.bar);
90
111
  const sym = terminal.unicode
91
112
  ? { ok: '✔', info: 'ℹ', warn: '▲', fail: '✖', dot: '•', bar: '█', rest: '░' }
92
113
  : { ok: '[ok]', info: '[i]', warn: '[!]', fail: '[x]', dot: '-', bar: '#', rest: '.' };
@@ -104,14 +125,14 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
104
125
  return;
105
126
  const f = s('cyan', frames[spin.frame % frames.length] ?? '');
106
127
  const extra = spin.extra === '' ? '' : s('gray', ` ${spin.extra}`);
107
- out(`\r${ESC}2K ${f} ${spin.label}${extra}`);
128
+ out(rail ? `\r${ESC}2K${f} ${spin.label}${extra}` : `\r${ESC}2K ${f} ${spin.label}${extra}`);
108
129
  };
109
130
  const write = (text) => {
110
131
  clear();
111
132
  out(text);
112
133
  draw();
113
134
  };
114
- const line = (mark, text) => write(` ${mark} ${text}\n`);
135
+ const line = (mark, text) => write(rail ? `${bar} ${mark} ${text}\n` : ` ${mark} ${text}\n`);
115
136
  const icon = (name) => (terminal.emoji ? ICONS[name][0] : s('cyan', ICONS[name][1]));
116
137
  return {
117
138
  terminal,
@@ -132,8 +153,25 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
132
153
  }
133
154
  write(` ${s('bold', subtitle)}\n`);
134
155
  },
156
+ intro(title, hint) {
157
+ if (!rail) {
158
+ write(` ${s('gray', hint)}\n`);
159
+ return;
160
+ }
161
+ write(`\n${s('gray', RAIL.start)} ${s('inverse', ` ${title} `)}\n${bar} ${s('gray', hint)}\n`);
162
+ },
163
+ outro(text) {
164
+ write(rail ? `${bar}\n${s('gray', RAIL.end)} ${text}\n` : `\n ${text}\n`);
165
+ },
135
166
  step(name, title, hint) {
136
167
  stepNo += 1;
168
+ if (rail) {
169
+ const n = options.steps === undefined ? '' : s('gray', ` ${stepNo}/${options.steps}`);
170
+ write(`${bar}\n${s('cyan', RAIL.active)} ${s('bold', title)}${n}\n`);
171
+ if (hint !== undefined)
172
+ write(`${bar} ${s('gray', hint)}\n`);
173
+ return;
174
+ }
137
175
  const n = options.steps === undefined ? '' : s('gray', `Step ${stepNo}/${options.steps} · `);
138
176
  const rule = terminal.unicode ? '─' : '-';
139
177
  const head = `${icon(name)} ${n}${s('bold', title)}`;
@@ -146,7 +184,9 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
146
184
  info: (text) => line(s('cyan', sym.info), text),
147
185
  warn: (text) => line(s('yellow', sym.warn), text),
148
186
  fail: (text) => line(s('red', sym.fail), text),
149
- detail: (text) => write(` ${s('gray', `${sym.dot} ${text}`)}\n`),
187
+ detail: (text) => write(rail
188
+ ? `${bar} ${s('gray', `${sym.dot} ${text}`)}\n`
189
+ : ` ${s('gray', `${sym.dot} ${text}`)}\n`),
150
190
  async task(label, work) {
151
191
  const started = Date.now();
152
192
  const previous = spin;
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: c572e760d811a5f6
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 07f36b4355890ebe
2
2
  // Bindings of module `contacts`: import the definition API from here to get types scoped to what it can see.
3
3
  export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
4
4
  import { definitions } from '../../server/orm.js';
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: c572e760d811a5f6
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 07f36b4355890ebe
2
2
  // Schema view of module `contacts` (RFC 0002 §7.7): the models it owns or depends on, with the extensions it can see.
3
3
  import type { F } from '../../server/orm.js';
4
4
 
@@ -173,6 +173,7 @@ export interface ResCompany {
173
173
  readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
174
174
  readonly name: F.Char<{ readonly required: true }>;
175
175
  readonly currency: F.Many2one<'res.currency', { readonly required: true }>;
176
+ readonly numberGrouping: F.Selection<'international' | 'lakh', { readonly required: true; readonly default: true }>;
176
177
  };
177
178
  readonly methods: ResCompanyMethods;
178
179
  }
@@ -226,6 +227,7 @@ export interface ResPartner {
226
227
  readonly city: F.Char;
227
228
  readonly zip: F.Char;
228
229
  readonly country: F.Char;
230
+ readonly state: F.Char;
229
231
  readonly taxId: F.Char;
230
232
  readonly isCustomer: F.Bool<{ readonly required: true; readonly default: true }>;
231
233
  readonly isVendor: F.Bool<{ readonly required: true; readonly default: true }>;