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.
- package/README.md +17 -10
- package/dist/account_portal/.kosh/bindings.d.ts +22 -0
- package/dist/account_portal/.kosh/bindings.js +15 -0
- package/dist/account_portal/.kosh/schema.d.ts +1350 -0
- package/dist/account_portal/index.d.ts +3 -0
- package/dist/account_portal/index.js +73 -0
- package/dist/account_portal/module.d.ts +5983 -0
- package/dist/account_portal/module.js +18 -0
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +142 -9
- package/dist/accounting/chart.d.ts +40 -8
- package/dist/accounting/chart.js +124 -30
- package/dist/accounting/closing.d.ts +6 -1
- package/dist/accounting/closing.js +5 -1
- package/dist/accounting/demo.js +6 -5
- package/dist/accounting/index.d.ts +3 -3
- package/dist/accounting/index.js +200 -23
- package/dist/accounting/module.d.ts +3326 -1086
- package/dist/accounting/module.js +408 -66
- package/dist/accounting/payments.d.ts +25 -0
- package/dist/accounting/payments.js +96 -4
- package/dist/accounting/web/RegisterPayment.d.ts +3 -2
- package/dist/accounting/web/RegisterPayment.js +39 -6
- package/dist/accounting/web/SettingsPage.js +5 -1
- package/dist/accounting/web/messages.js +10 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +195 -53
- package/dist/cli/launch.d.ts +16 -0
- package/dist/cli/launch.js +138 -0
- package/dist/cli/prompt.d.ts +35 -0
- package/dist/cli/prompt.js +46 -0
- package/dist/cli/templates.d.ts +11 -3
- package/dist/cli/templates.js +32 -7
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.d.ts +25 -5
- package/dist/cli/ui.js +49 -9
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +3 -1
- package/dist/contacts/index.js +24 -1
- package/dist/contacts/module.d.ts +42 -0
- package/dist/contacts/module.js +3 -1
- package/dist/import/convert.d.ts +55 -0
- package/dist/import/convert.js +224 -0
- package/dist/import/csv.d.ts +28 -0
- package/dist/import/csv.js +115 -0
- package/dist/import/index.d.ts +33 -0
- package/dist/import/index.js +60 -0
- package/dist/import/mapping.d.ts +15 -0
- package/dist/import/mapping.js +33 -0
- package/dist/import/match.d.ts +24 -0
- package/dist/import/match.js +60 -0
- package/dist/import/models.d.ts +2 -0
- package/dist/import/models.js +102 -0
- package/dist/import/routes.d.ts +13 -0
- package/dist/import/routes.js +392 -0
- package/dist/import/run.d.ts +131 -0
- package/dist/import/run.js +568 -0
- package/dist/import/service.d.ts +153 -0
- package/dist/import/service.js +367 -0
- package/dist/import/web/ImportButton.d.ts +7 -0
- package/dist/import/web/ImportButton.js +31 -0
- package/dist/import/web/ImportWizard.d.ts +75 -0
- package/dist/import/web/ImportWizard.js +217 -0
- package/dist/import/web/index.d.ts +13 -0
- package/dist/import/web/index.js +18 -0
- package/dist/import/web/messages.d.ts +2 -0
- package/dist/import/web/messages.js +139 -0
- package/dist/import/worker.d.ts +3 -0
- package/dist/import/worker.js +52 -0
- package/dist/import/xlsx.d.ts +20 -0
- package/dist/import/xlsx.js +192 -0
- package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_bd/.kosh/bindings.js +15 -0
- package/dist/l10n_bd/.kosh/schema.d.ts +1281 -0
- package/dist/l10n_bd/documents.d.ts +8 -0
- package/dist/l10n_bd/documents.js +183 -0
- package/dist/l10n_bd/index.d.ts +8 -0
- package/dist/l10n_bd/index.js +156 -0
- package/dist/l10n_bd/module.d.ts +5705 -0
- package/dist/l10n_bd/module.js +163 -0
- package/dist/l10n_bd/mushak.d.ts +67 -0
- package/dist/l10n_bd/mushak.js +139 -0
- package/dist/l10n_bd/routes.d.ts +3 -0
- package/dist/l10n_bd/routes.js +205 -0
- package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
- package/dist/l10n_bd/web/MushakPage.js +53 -0
- package/dist/l10n_bd/web/index.d.ts +13 -0
- package/dist/l10n_bd/web/index.js +18 -0
- package/dist/l10n_bd/web/messages.d.ts +2 -0
- package/dist/l10n_bd/web/messages.js +101 -0
- package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_in/.kosh/bindings.js +15 -0
- package/dist/l10n_in/.kosh/schema.d.ts +1343 -0
- package/dist/l10n_in/documents.d.ts +8 -0
- package/dist/l10n_in/documents.js +114 -0
- package/dist/l10n_in/einvoice.d.ts +253 -0
- package/dist/l10n_in/einvoice.js +485 -0
- package/dist/l10n_in/gstr.d.ts +109 -0
- package/dist/l10n_in/gstr.js +430 -0
- package/dist/l10n_in/index.d.ts +20 -0
- package/dist/l10n_in/index.js +232 -0
- package/dist/l10n_in/module.d.ts +7823 -0
- package/dist/l10n_in/module.js +427 -0
- package/dist/l10n_in/routes-einvoice.d.ts +15 -0
- package/dist/l10n_in/routes-einvoice.js +124 -0
- package/dist/l10n_in/routes.d.ts +17 -0
- package/dist/l10n_in/routes.js +282 -0
- package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
- package/dist/l10n_in/web/EInvoicePanel.js +63 -0
- package/dist/l10n_in/web/GstPage.d.ts +7 -0
- package/dist/l10n_in/web/GstPage.js +57 -0
- package/dist/l10n_in/web/GstSettings.d.ts +10 -0
- package/dist/l10n_in/web/GstSettings.js +36 -0
- package/dist/l10n_in/web/index.d.ts +15 -0
- package/dist/l10n_in/web/index.js +22 -0
- package/dist/l10n_in/web/messages.d.ts +2 -0
- package/dist/l10n_in/web/messages.js +166 -0
- package/dist/modules-sdk/demo.d.ts +2 -0
- package/dist/modules-sdk/demo.js +7 -3
- package/dist/modules-sdk/index.d.ts +2 -2
- package/dist/modules-sdk/index.js +1 -1
- package/dist/modules-sdk/manifest.d.ts +132 -0
- package/dist/modules-sdk/manifest.js +22 -0
- package/dist/notify/inbox.js +21 -0
- package/dist/notify/messages.js +4 -0
- package/dist/notify/models.d.ts +1 -1
- package/dist/notify/models.js +2 -0
- package/dist/notify/web/messages.js +4 -0
- package/dist/portal/.kosh/bindings.d.ts +22 -0
- package/dist/portal/.kosh/bindings.js +15 -0
- package/dist/portal/.kosh/schema.d.ts +346 -0
- package/dist/portal/announce.d.ts +16 -0
- package/dist/portal/announce.js +117 -0
- package/dist/portal/documents.d.ts +17 -0
- package/dist/portal/documents.js +61 -0
- package/dist/portal/index.d.ts +20 -0
- package/dist/portal/index.js +65 -0
- package/dist/portal/module.d.ts +1405 -0
- package/dist/portal/module.js +84 -0
- package/dist/portal/routes-documents.d.ts +2 -0
- package/dist/portal/routes-documents.js +391 -0
- package/dist/portal/routes.d.ts +13 -0
- package/dist/portal/routes.js +313 -0
- package/dist/portal/rules.d.ts +11 -0
- package/dist/portal/rules.js +25 -0
- package/dist/portal/web/PortalAccess.d.ts +8 -0
- package/dist/portal/web/PortalAccess.js +66 -0
- package/dist/portal/web/index.d.ts +12 -0
- package/dist/portal/web/index.js +17 -0
- package/dist/portal/web/messages.d.ts +2 -0
- package/dist/portal/web/messages.js +33 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +12 -3
- package/dist/product/demo.js +6 -5
- package/dist/product/index.d.ts +1 -0
- package/dist/product/index.js +17 -4
- package/dist/product/module.d.ts +346 -14
- package/dist/product/module.js +48 -18
- package/dist/product/taxes.d.ts +59 -0
- package/dist/product/taxes.js +61 -0
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +3 -1
- package/dist/project/module.d.ts +20 -0
- package/dist/project_portal/.kosh/bindings.d.ts +22 -0
- package/dist/project_portal/.kosh/bindings.js +15 -0
- package/dist/project_portal/.kosh/schema.d.ts +671 -0
- package/dist/project_portal/index.d.ts +9 -0
- package/dist/project_portal/index.js +42 -0
- package/dist/project_portal/module.d.ts +2142 -0
- package/dist/project_portal/module.js +22 -0
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +15 -4
- package/dist/purchase/demo.js +3 -3
- package/dist/purchase/index.js +1 -1
- package/dist/purchase/module.d.ts +232 -8
- package/dist/purchase/module.js +51 -9
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +144 -10
- package/dist/purchase_account/module.d.ts +2008 -641
- package/dist/purchase_account/module.js +2 -2
- package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_portal/.kosh/bindings.js +15 -0
- package/dist/purchase_portal/.kosh/schema.d.ts +822 -0
- package/dist/purchase_portal/index.d.ts +9 -0
- package/dist/purchase_portal/index.js +53 -0
- package/dist/purchase_portal/module.d.ts +2758 -0
- package/dist/purchase_portal/module.js +18 -0
- package/dist/reporting/documents.js +20 -3
- package/dist/reporting/index.d.ts +1 -0
- package/dist/reporting/index.js +1 -0
- package/dist/reporting/routes-documents.js +9 -2
- package/dist/reporting/words.d.ts +11 -0
- package/dist/reporting/words.js +229 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +144 -10
- package/dist/sale_account/module.d.ts +2008 -641
- package/dist/sale_account/module.js +3 -3
- package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
- package/dist/sale_portal/.kosh/bindings.js +15 -0
- package/dist/sale_portal/.kosh/schema.d.ts +736 -0
- package/dist/sale_portal/index.d.ts +9 -0
- package/dist/sale_portal/index.js +71 -0
- package/dist/sale_portal/module.d.ts +2718 -0
- package/dist/sale_portal/module.js +19 -0
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +15 -4
- package/dist/sale_project/module.d.ts +390 -10
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +15 -4
- package/dist/sales/demo.js +3 -3
- package/dist/sales/index.js +67 -1
- package/dist/sales/module.d.ts +232 -8
- package/dist/sales/module.js +51 -12
- package/dist/server/auth/base.js +2 -0
- package/dist/server/auth/service.d.ts +2 -0
- package/dist/server/auth/service.js +7 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/server.js +26 -1
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.js +1 -1
- package/dist/ui/context.d.ts +3 -1
- package/dist/ui/context.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/lib/format.d.ts +2 -0
- package/dist/ui/lib/format.js +3 -1
- package/dist/ui/views/FormView.js +19 -6
- package/package.json +50 -2
package/dist/cli/prompt.js
CHANGED
|
@@ -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) {
|
package/dist/cli/templates.d.ts
CHANGED
|
@@ -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[];
|
package/dist/cli/templates.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|
package/dist/cli/tui.js
ADDED
|
@@ -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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (
|
|
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 = (
|
|
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(
|
|
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:
|
|
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:
|
|
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 }>;
|