@mostajs/setup 2.1.27 → 2.1.29

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.
@@ -85,7 +85,31 @@ export function createSetupRoutes(config) {
85
85
  const { NetClient } = await import('../lib/net-client.js');
86
86
  const client = new NetClient({ url: body.url });
87
87
  const health = await client.health();
88
- return Response.json({ ok: true, ...health });
88
+ // Auto-save MOSTA_DATA, MOSTA_NET_URL, MOSTA_NET_TRANSPORT in .env.local
89
+ try {
90
+ const { readFileSync, writeFileSync, existsSync } = await import('fs');
91
+ const { resolve } = await import('path');
92
+ const envPath = resolve(process.cwd(), '.env.local');
93
+ let content = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
94
+ const updates = {
95
+ 'MOSTA_DATA': 'net',
96
+ 'MOSTA_NET_URL': body.url,
97
+ 'MOSTA_NET_TRANSPORT': body.transport || 'rest',
98
+ };
99
+ for (const [key, val] of Object.entries(updates)) {
100
+ const regex = new RegExp(`^${key}=.*$`, 'm');
101
+ if (regex.test(content)) {
102
+ content = content.replace(regex, `${key}=${val}`);
103
+ }
104
+ else {
105
+ content += `\n${key}=${val}`;
106
+ }
107
+ process.env[key] = val;
108
+ }
109
+ writeFileSync(envPath, content);
110
+ }
111
+ catch { }
112
+ return Response.json({ ok: true, ...health, saved: true });
89
113
  }
90
114
  catch (e) {
91
115
  return Response.json({ ok: false, error: e instanceof Error ? e.message : 'Connexion echouee' });
@@ -836,7 +836,7 @@ export default function SetupWizard({ t: tProp, onComplete, endpoints = {}, dbNa
836
836
  const res = await fetch(ep.setupJson.replace('setup-json', 'net-test'), {
837
837
  method: 'POST',
838
838
  headers: { 'Content-Type': 'application/json' },
839
- body: JSON.stringify({ url: netUrl }),
839
+ body: JSON.stringify({ url: netUrl, transport: netTransport }),
840
840
  });
841
841
  const data = await res.json();
842
842
  setNetTestResult(data);
@@ -849,49 +849,24 @@ export default function SetupWizard({ t: tProp, onComplete, endpoints = {}, dbNa
849
849
  padding: 12, borderRadius: 8, marginBottom: 16,
850
850
  backgroundColor: netTestResult.ok ? '#f0fdf4' : '#fef2f2',
851
851
  border: `1px solid ${netTestResult.ok ? '#bbf7d0' : '#fecaca'}`,
852
- }, children: netTestResult.ok ? (_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 600, color: '#166534', marginBottom: 4 }, children: "\u2705 Serveur connecte" }), netTestResult.entities && (_jsxs("div", { style: { fontSize: 13, color: '#374151' }, children: [_jsx("strong", { children: netTestResult.entities.length }), " entites : ", netTestResult.entities.join(', ')] })), netTestResult.transports && (_jsxs("div", { style: { fontSize: 13, color: '#6b7280', marginTop: 4 }, children: ["Transports : ", netTestResult.transports.join(', ')] }))] })) : (_jsxs("div", { style: { color: '#991b1b' }, children: ["\u274C ", netTestResult.error || 'Connexion echouee'] })) })), netTestResult?.ok && netTestResult.entities?.length === 0 && !schemasReady && (_jsxs("div", { style: {
853
- padding: 16, borderRadius: 8, marginBottom: 16,
854
- backgroundColor: '#fffbeb', border: '1px solid #fde68a',
855
- }, children: [_jsx("div", { style: { fontWeight: 600, color: '#92400e', marginBottom: 8 }, children: "\u26A0\uFE0F Le serveur n'a aucun schema \u2014 envoyez les schemas pour continuer" }), _jsxs("div", { style: { display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 8 }, children: [_jsx("button", { style: { ...S.btn('primary'), fontSize: 13 }, onClick: () => document.getElementById('schemaFileInput')?.click(), children: "\uD83D\uDCC4 Envoyer schemas.json" }), _jsx("input", { id: "schemaFileInput", type: "file", accept: ".json", style: { display: 'none' }, onChange: async (e) => {
852
+ }, children: netTestResult.ok ? (_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 600, color: '#166534', marginBottom: 4 }, children: "\u2705 Serveur connecte" }), netTestResult.entities && (_jsxs("div", { style: { fontSize: 13, color: '#374151' }, children: [_jsx("strong", { children: netTestResult.entities.length }), " entites : ", netTestResult.entities.join(', ')] })), netTestResult.transports && (_jsxs("div", { style: { fontSize: 13, color: '#6b7280', marginTop: 4 }, children: ["Transports : ", netTestResult.transports.join(', ')] }))] })) : (_jsxs("div", { style: { color: '#991b1b' }, children: ["\u274C ", netTestResult.error || 'Connexion echouee'] })) })), netTestResult?.ok && (_jsxs("div", { style: { padding: 16, borderRadius: 8, marginBottom: 16, backgroundColor: '#f8fafc', border: '1px solid #e2e8f0' }, children: [_jsxs("div", { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 12, flexWrap: 'wrap' }, children: [_jsx("span", { style: { fontSize: 13, fontWeight: 600, color: '#64748b', minWidth: 65 }, children: "Etape 1:" }), _jsx("button", { style: { ...S.btn('primary'), fontSize: 13 }, onClick: () => document.getElementById('schemaFileInput')?.click(), children: "Uploader schemas.json" }), _jsx("input", { id: "schemaFileInput", type: "file", accept: ".json", style: { display: 'none' }, onChange: async (e) => {
856
853
  const file = e.target.files?.[0];
857
854
  if (!file)
858
855
  return;
859
- setSchemaUploadStatus({ phase: '📤 Envoi du fichier...', color: '#2563eb' });
856
+ setSchemaUploadStatus({ phase: 'Envoi...', color: '#2563eb' });
860
857
  try {
861
858
  const text = await file.text();
862
859
  const schemas = JSON.parse(text);
863
- const res = await fetch(netUrl + '/api/upload-schemas-json', {
864
- method: 'POST',
865
- headers: { 'Content-Type': 'application/json' },
866
- body: JSON.stringify({ schemas }),
860
+ const res = await fetch(netUrl + '/api/upload-schemas', {
861
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
862
+ body: JSON.stringify({ schemas: Array.isArray(schemas) ? schemas : [schemas] }),
867
863
  });
868
864
  const data = await res.json();
869
865
  if (data.ok) {
870
- if (data.needsRestart) {
871
- setSchemaUploadStatus({ phase: '⏳ Serveur redémarre...', color: '#d97706' });
872
- // Poll health
873
- for (let i = 0; i < 30; i++) {
874
- await new Promise(r => setTimeout(r, 1500));
875
- setSchemaUploadStatus({ phase: `⏳ En attente du serveur... (${i + 1}/30)`, color: '#d97706' });
876
- try {
877
- const h = await fetch(netUrl + '/health');
878
- if (h.ok) {
879
- const hd = await h.json();
880
- if (hd.entities?.length > 0) {
881
- setSchemaUploadStatus({ phase: `✅ Serveur prêt — ${hd.entities.length} entités`, color: '#16a34a' });
882
- setSchemasReady(true);
883
- setNetTestResult({ ...netTestResult, entities: hd.entities });
884
- break;
885
- }
886
- }
887
- }
888
- catch { }
889
- }
890
- }
891
- else {
892
- setSchemaUploadStatus({ phase: `✅ ${data.count} schemas chargés`, color: '#16a34a' });
893
- setSchemasReady(true);
894
- }
866
+ setSchemaUploadStatus({ phase: `✅ ${data.count} schemas uploades`, color: '#16a34a' });
867
+ const h = await fetch(netUrl + '/health').then(r => r.json());
868
+ if (h.entities)
869
+ setNetTestResult({ ...netTestResult, entities: h.entities });
895
870
  }
896
871
  else {
897
872
  setSchemaUploadStatus({ phase: `❌ ${data.error}`, color: '#dc2626' });
@@ -901,74 +876,33 @@ export default function SetupWizard({ t: tProp, onComplete, endpoints = {}, dbNa
901
876
  setSchemaUploadStatus({ phase: `❌ ${err.message}`, color: '#dc2626' });
902
877
  }
903
878
  e.target.value = '';
904
- } }), _jsx("button", { style: { ...S.btn('outline'), fontSize: 13 }, onClick: () => document.getElementById('schemaZipInput')?.click(), children: "\uD83D\uDCE6 Envoyer ZIP de schemas" }), _jsx("input", { id: "schemaZipInput", type: "file", accept: ".zip", style: { display: 'none' }, onChange: async (e) => {
905
- const file = e.target.files?.[0];
906
- if (!file)
907
- return;
908
- setSchemaUploadStatus({ phase: '📤 Envoi du ZIP...', color: '#2563eb' });
879
+ } }), _jsx("span", { style: { fontSize: 12, color: schemaUploadStatus?.color || '#94a3b8' }, children: schemaUploadStatus?.phase || ((netTestResult.entities?.length ?? 0) > 0 ? `✅ ${netTestResult.entities.length} schemas` : 'Aucun schema') })] }), _jsxs("div", { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 12, flexWrap: 'wrap' }, children: [_jsx("span", { style: { fontSize: 13, fontWeight: 600, color: '#64748b', minWidth: 65 }, children: "Etape 2:" }), _jsx("button", { style: { ...S.btn('primary'), fontSize: 13, backgroundColor: '#f59e0b', color: '#000' }, disabled: (netTestResult.entities?.length ?? 0) === 0, onClick: async () => {
880
+ setSchemaUploadStatus({ phase: 'Application du schema...', color: '#2563eb' });
909
881
  try {
910
- const formData = new FormData();
911
- formData.append('file', file);
912
- const res = await fetch(netUrl + '/api/upload-schemas', {
913
- method: 'POST',
914
- body: formData,
915
- });
882
+ const res = await fetch(netUrl + '/api/apply-schema', { method: 'POST' });
916
883
  const data = await res.json();
917
- if (data.ok) {
918
- setSchemaUploadStatus({ phase: `✅ ${data.count} schemas importés depuis ZIP`, color: '#16a34a' });
919
- setSchemasReady(true);
920
- // Refresh test result
921
- const h = await fetch(netUrl + '/health').then(r => r.json());
922
- if (h.entities)
923
- setNetTestResult({ ...netTestResult, entities: h.entities });
924
- }
925
- else {
926
- setSchemaUploadStatus({ phase: `❌ ${data.error}`, color: '#dc2626' });
927
- }
884
+ setSchemaUploadStatus({ phase: data.ok ? `✅ ${data.message || 'Schema applique'}` : `❌ ${data.error || data.message}`, color: data.ok ? '#16a34a' : '#dc2626' });
928
885
  }
929
886
  catch (err) {
930
887
  setSchemaUploadStatus({ phase: `❌ ${err.message}`, color: '#dc2626' });
931
888
  }
932
- e.target.value = '';
933
- } }), _jsx("button", { style: { ...S.btn('outline'), fontSize: 13 }, onClick: async () => {
934
- const schemasPath = prompt('Chemin vers le répertoire des schemas (*.schema.ts) :', './src/dal/schemas');
935
- if (!schemasPath)
936
- return;
937
- setSchemaUploadStatus({ phase: '🔍 Scan en cours...', color: '#2563eb' });
889
+ }, children: "Appliquer le schema" })] }), _jsxs("div", { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8, flexWrap: 'wrap' }, children: [_jsx("span", { style: { fontSize: 13, fontWeight: 600, color: '#64748b', minWidth: 65 }, children: "Etape 3:" }), _jsx("button", { style: { ...S.btn('primary'), fontSize: 13, backgroundColor: '#22c55e' }, disabled: (netTestResult.entities?.length ?? 0) === 0, onClick: async () => {
890
+ setSchemaUploadStatus({ phase: 'Enregistrement...', color: '#2563eb' });
938
891
  try {
939
- const res = await fetch(netUrl + '/api/scan-schemas', {
940
- method: 'POST',
941
- headers: { 'Content-Type': 'application/json' },
942
- body: JSON.stringify({ path: schemasPath }),
943
- });
892
+ const res = await fetch(netUrl + '/api/save-config', { method: 'POST' });
944
893
  const data = await res.json();
945
- if (data.ok && data.count > 0) {
946
- // Générer et appliquer
947
- const genRes = await fetch(netUrl + '/api/generate-schemas', {
948
- method: 'POST',
949
- headers: { 'Content-Type': 'application/json' },
950
- body: JSON.stringify({ path: schemasPath }),
951
- });
952
- const genData = await genRes.json();
953
- if (genData.ok) {
954
- setSchemaUploadStatus({ phase: `✅ ${genData.count} schemas scannés et générés`, color: '#16a34a' });
955
- setSchemasReady(true);
956
- const h = await fetch(netUrl + '/health').then(r => r.json());
957
- if (h.entities)
958
- setNetTestResult({ ...netTestResult, entities: h.entities });
959
- }
894
+ if (data.ok) {
895
+ setSchemaUploadStatus({ phase: '✅ Config enregistree', color: '#16a34a' });
896
+ setSchemasReady(true);
960
897
  }
961
898
  else {
962
- setSchemaUploadStatus({ phase: `❌ Aucun schema trouvé dans ${schemasPath}`, color: '#dc2626' });
899
+ setSchemaUploadStatus({ phase: `❌ ${data.error || data.message}`, color: '#dc2626' });
963
900
  }
964
901
  }
965
902
  catch (err) {
966
903
  setSchemaUploadStatus({ phase: `❌ ${err.message}`, color: '#dc2626' });
967
904
  }
968
- }, children: "\uD83D\uDCC1 Scanner un r\u00E9pertoire" })] }), schemaUploadStatus && (_jsx("div", { style: { fontSize: 13, fontWeight: 500, color: schemaUploadStatus.color }, children: schemaUploadStatus.phase }))] })), netTestResult?.ok && (netTestResult.entities?.length ?? 0) > 0 && (_jsx("div", { style: {
969
- padding: 12, borderRadius: 8, marginBottom: 16,
970
- backgroundColor: '#f0fdf4', border: '1px solid #bbf7d0',
971
- }, children: _jsxs("div", { style: { fontWeight: 600, color: '#166534' }, children: ["\u2705 Serveur pr\u00EAt \u2014 ", netTestResult.entities?.length, " entit\u00E9s charg\u00E9es"] }) })), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', !canGoNext()), onClick: goNext, disabled: !canGoNext(), children: [t('setup.next'), " \u2192"] })] })] })), step === 'admin' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDC64" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.admin.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.admin.description') })] })] }), _jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.firstName') }), _jsx("input", { style: S.input, value: adminConfig.firstName, onChange: e => setAdminConfig({ ...adminConfig, firstName: e.target.value }) })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.lastName') }), _jsx("input", { style: S.input, value: adminConfig.lastName, onChange: e => setAdminConfig({ ...adminConfig, lastName: e.target.value }) })] })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.email') }), _jsx("input", { style: S.input, type: "email", value: adminConfig.email, onChange: e => setAdminConfig({ ...adminConfig, email: e.target.value }), placeholder: "admin@example.com" })] }), _jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.password') }), _jsx("input", { style: S.input, type: "password", value: adminConfig.password, onChange: e => setAdminConfig({ ...adminConfig, password: e.target.value }) })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.confirmPassword') }), _jsx("input", { style: S.input, type: "password", value: adminConfig.confirmPassword, onChange: e => setAdminConfig({ ...adminConfig, confirmPassword: e.target.value }) })] })] }), adminConfig.password && adminConfig.confirmPassword && adminConfig.password !== adminConfig.confirmPassword && (_jsx("p", { style: { fontSize: 13, color: '#dc2626' }, children: t('setup.admin.passwordMismatch') })), setupMode === 'net' && (_jsxs("div", { style: { marginTop: 16, marginBottom: 16 }, children: [_jsx("button", { style: { ...S.btn('primary'), backgroundColor: '#16a34a' }, disabled: adminSaving || !adminConfig.email || !adminConfig.password || !adminConfig.firstName || adminConfig.password !== adminConfig.confirmPassword, onClick: async () => {
905
+ }, children: "Enregistrer la config" })] }), schemaUploadStatus && (_jsx("div", { style: { fontSize: 13, fontWeight: 500, color: schemaUploadStatus.color, marginTop: 4 }, children: schemaUploadStatus.phase }))] })), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', !canGoNext()), onClick: goNext, disabled: !canGoNext(), children: [t('setup.next'), " \u2192"] })] })] })), step === 'admin' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDC64" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.admin.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.admin.description') })] })] }), _jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.firstName') }), _jsx("input", { style: S.input, value: adminConfig.firstName, onChange: e => setAdminConfig({ ...adminConfig, firstName: e.target.value }) })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.lastName') }), _jsx("input", { style: S.input, value: adminConfig.lastName, onChange: e => setAdminConfig({ ...adminConfig, lastName: e.target.value }) })] })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.email') }), _jsx("input", { style: S.input, type: "email", value: adminConfig.email, onChange: e => setAdminConfig({ ...adminConfig, email: e.target.value }), placeholder: "admin@example.com" })] }), _jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.password') }), _jsx("input", { style: S.input, type: "password", value: adminConfig.password, onChange: e => setAdminConfig({ ...adminConfig, password: e.target.value }) })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.confirmPassword') }), _jsx("input", { style: S.input, type: "password", value: adminConfig.confirmPassword, onChange: e => setAdminConfig({ ...adminConfig, confirmPassword: e.target.value }) })] })] }), adminConfig.password && adminConfig.confirmPassword && adminConfig.password !== adminConfig.confirmPassword && (_jsx("p", { style: { fontSize: 13, color: '#dc2626' }, children: t('setup.admin.passwordMismatch') })), setupMode === 'net' && (_jsxs("div", { style: { marginTop: 16, marginBottom: 16 }, children: [_jsx("button", { style: { ...S.btn('primary'), backgroundColor: '#16a34a' }, disabled: adminSaving || !adminConfig.email || !adminConfig.password || !adminConfig.firstName || adminConfig.password !== adminConfig.confirmPassword, onClick: async () => {
972
906
  setAdminSaving(true);
973
907
  setAdminSaveResult(null);
974
908
  try {
@@ -93,7 +93,7 @@ function buildConfig(json, repoFactory) {
93
93
  if (json.rbac.roles?.length) {
94
94
  const roleRepo = await getRepo('role');
95
95
  const allPermIds = Object.values(permissionMap);
96
- for (const roleDef of json.rbac.roles.filter(r => r.name)) {
96
+ for (const roleDef of json.rbac.roles.filter(r => r.name && r.name.trim() !== '')) {
97
97
  const permissionIds = roleDef.permissions.includes('*')
98
98
  ? allPermIds
99
99
  : roleDef.permissions.map(code => permissionMap[code]).filter(Boolean);
package/dist/lib/setup.js CHANGED
@@ -179,16 +179,18 @@ async function runNetInstall(installConfig, setupConfig) {
179
179
  extraVars['MOSTAJS_MODULES'] = installConfig.modules.join(',');
180
180
  }
181
181
  const seeded = [];
182
- // 2. Verify NET server is reachable (retry up to 10s if just restarted)
182
+ // 2. Verify NET server is reachable (retry with backoff, max 20s)
183
183
  let health = null;
184
- for (let i = 0; i < 10; i++) {
184
+ let retryDelay = 500;
185
+ for (let i = 0; i < 15; i++) {
185
186
  try {
186
187
  health = await net.health();
187
188
  if (health.entities?.length > 0)
188
189
  break;
189
190
  }
190
191
  catch { }
191
- await new Promise(r => setTimeout(r, 1000));
192
+ await new Promise(r => setTimeout(r, retryDelay));
193
+ retryDelay = Math.min(retryDelay * 1.5, 3000);
192
194
  }
193
195
  if (!health) {
194
196
  return { ok: false, error: 'Serveur NET non joignable', needsRestart: false };
@@ -237,8 +239,24 @@ async function runNetInstall(installConfig, setupConfig) {
237
239
  // Si le serveur redémarre, attendre qu'il soit de retour
238
240
  if (result.needsRestart) {
239
241
  console.log('[Setup] Serveur NET redémarre pour charger les schemas...');
240
- await new Promise(r => setTimeout(r, 4000)); // Laisser le temps au process.exit + restart
241
- // Poll health jusqu'à ce que le serveur soit de retour (max 30s)
242
+ // Phase 1 : attendre que le serveur soit DOWN (max 10s)
243
+ let serverDown = false;
244
+ for (let i = 0; i < 20; i++) {
245
+ try {
246
+ await net.health();
247
+ // Encore up — attendre
248
+ }
249
+ catch {
250
+ serverDown = true;
251
+ break;
252
+ }
253
+ await new Promise(r => setTimeout(r, 500));
254
+ }
255
+ if (!serverDown) {
256
+ console.log('[Setup] Serveur n\'a pas redémarré — continue quand même');
257
+ }
258
+ // Phase 2 : attendre que le serveur soit UP avec schemas (max 60s, backoff)
259
+ let backoff = 500;
242
260
  for (let i = 0; i < 30; i++) {
243
261
  try {
244
262
  const h = await net.health();
@@ -248,7 +266,8 @@ async function runNetInstall(installConfig, setupConfig) {
248
266
  }
249
267
  }
250
268
  catch { }
251
- await new Promise(r => setTimeout(r, 1000));
269
+ await new Promise(r => setTimeout(r, backoff));
270
+ backoff = Math.min(backoff * 1.5, 3000); // backoff: 500→750→1125→...→3000ms max
252
271
  }
253
272
  // Recharger la collection map
254
273
  await net.loadCollectionMap();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/setup",
3
- "version": "2.1.27",
3
+ "version": "2.1.29",
4
4
  "description": "Reusable setup wizard module — multi-dialect DB configuration, .env.local writer, seed runner",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "MIT",