@loom-framework/core 0.1.0-alpha.164 → 0.1.0-alpha.166

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 (57) hide show
  1. package/builtin-skills/loom/SKILL.md +9 -6
  2. package/builtin-skills/loom/references/eject.md +102 -0
  3. package/dist/cli/commands/eject.d.ts.map +1 -1
  4. package/dist/cli/commands/eject.js +52 -285
  5. package/dist/cli/commands/eject.js.map +1 -1
  6. package/dist/cli/commands/generate-system-settings.d.ts +3 -3
  7. package/dist/cli/commands/generate-system-settings.d.ts.map +1 -1
  8. package/dist/cli/commands/generate-system-settings.js +29 -204
  9. package/dist/cli/commands/generate-system-settings.js.map +1 -1
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +27 -44
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/helpers/app-tsx-utils.d.ts +12 -0
  14. package/dist/cli/helpers/app-tsx-utils.d.ts.map +1 -0
  15. package/dist/cli/helpers/app-tsx-utils.js +22 -0
  16. package/dist/cli/helpers/app-tsx-utils.js.map +1 -0
  17. package/dist/cli/helpers/app-tsx-wiring-engine.d.ts +70 -0
  18. package/dist/cli/helpers/app-tsx-wiring-engine.d.ts.map +1 -0
  19. package/dist/cli/helpers/app-tsx-wiring-engine.js +546 -0
  20. package/dist/cli/helpers/app-tsx-wiring-engine.js.map +1 -0
  21. package/dist/cli/helpers/app-tsx-wiring.d.ts +4 -39
  22. package/dist/cli/helpers/app-tsx-wiring.d.ts.map +1 -1
  23. package/dist/cli/helpers/app-tsx-wiring.js +5 -608
  24. package/dist/cli/helpers/app-tsx-wiring.js.map +1 -1
  25. package/dist/cli/helpers/system-page-config.d.ts +27 -0
  26. package/dist/cli/helpers/system-page-config.d.ts.map +1 -0
  27. package/dist/cli/helpers/system-page-config.js +73 -0
  28. package/dist/cli/helpers/system-page-config.js.map +1 -0
  29. package/dist/cli/templates/login-page.d.ts +2 -3
  30. package/dist/cli/templates/login-page.d.ts.map +1 -1
  31. package/dist/cli/templates/login-page.js +18 -29
  32. package/dist/cli/templates/login-page.js.map +1 -1
  33. package/dist/cli/templates/model-management-page.d.ts +2 -3
  34. package/dist/cli/templates/model-management-page.d.ts.map +1 -1
  35. package/dist/cli/templates/model-management-page.js +11 -9
  36. package/dist/cli/templates/model-management-page.js.map +1 -1
  37. package/dist/cli/templates/notification-center-page.d.ts +2 -3
  38. package/dist/cli/templates/notification-center-page.d.ts.map +1 -1
  39. package/dist/cli/templates/notification-center-page.js +25 -62
  40. package/dist/cli/templates/notification-center-page.js.map +1 -1
  41. package/dist/cli/templates/notification-detail-page.d.ts +2 -3
  42. package/dist/cli/templates/notification-detail-page.d.ts.map +1 -1
  43. package/dist/cli/templates/notification-detail-page.js +13 -41
  44. package/dist/cli/templates/notification-detail-page.js.map +1 -1
  45. package/dist/cli/templates/process-management-page.d.ts +3 -3
  46. package/dist/cli/templates/process-management-page.d.ts.map +1 -1
  47. package/dist/cli/templates/process-management-page.js +75 -565
  48. package/dist/cli/templates/process-management-page.js.map +1 -1
  49. package/dist/cli/templates/skill-management-page.d.ts +2 -3
  50. package/dist/cli/templates/skill-management-page.d.ts.map +1 -1
  51. package/dist/cli/templates/skill-management-page.js +12 -15
  52. package/dist/cli/templates/skill-management-page.js.map +1 -1
  53. package/dist/cli/templates/user-management-page.d.ts +2 -4
  54. package/dist/cli/templates/user-management-page.d.ts.map +1 -1
  55. package/dist/cli/templates/user-management-page.js +21 -22
  56. package/dist/cli/templates/user-management-page.js.map +1 -1
  57. package/package.json +4 -2
@@ -3,40 +3,16 @@
3
3
  *
4
4
  * Surgically modifies App.tsx to add imports, navItems, and switch cases.
5
5
  * Nav labels use t() calls, so i18n keys are registered via registerMessages.
6
+ *
7
+ * For system settings pages (model/skill/user/login/process/notification),
8
+ * use wireAppTsx() from app-tsx-wiring-engine.ts instead.
6
9
  */
7
10
  import { promises as fs } from 'fs';
8
11
  import path from 'path';
9
- /**
10
- * Find the index of the newline character immediately before the first
11
- * JS/TS export statement. Matches `export default function`, `export function`,
12
- * `export const`, `export class`, `export type`, `export interface`.
13
- *
14
- * More robust than `indexOf('\nexport')` — avoids false matches on the word
15
- * "export" inside strings, comments, or object literals.
16
- */
17
- function findExportLineIndex(content) {
18
- const match = content.match(/\nexport\s+(?:default\s+)?(?:function|const|let|class|type|interface)\s/);
19
- return match && match.index !== undefined ? match.index : null;
20
- }
21
- /**
22
- * Find the position right after the last import statement.
23
- * Returns the index of the newline after the last `import ... from '...'` or `import ...` line.
24
- */
25
- function findLastImportEnd(content) {
26
- const importRegex = /^import\s.+;?\s*$/gm;
27
- let lastMatch = null;
28
- let m;
29
- while ((m = importRegex.exec(content)) !== null) {
30
- lastMatch = m;
31
- }
32
- if (!lastMatch)
33
- return null;
34
- // Return the position right after the last import line (including its newline)
35
- const endPos = lastMatch.index + lastMatch[0].length;
36
- return endPos;
37
- }
12
+ import { findLastImportEnd } from './app-tsx-utils.js';
38
13
  /**
39
14
  * Auto-wire App.tsx: add import, navItem, and switch case for a new page.
15
+ * Used by `loom generate page` for CRUD pages (not system settings pages).
40
16
  * Returns true if successful, false if App.tsx structure is unexpected.
41
17
  */
42
18
  export async function wireAppTsxAutomatic(projectRoot, pascalName, navKey, navLabel, options) {
@@ -166,583 +142,4 @@ export async function wireAppTsxAutomatic(projectRoot, pascalName, navKey, navLa
166
142
  await fs.writeFile(appPath, modified, 'utf-8');
167
143
  return true;
168
144
  }
169
- /**
170
- * Auto-wire the system-settings navigation group into App.tsx.
171
- * Creates or extends a system-settings submenu with the given page as a child.
172
- *
173
- * On first call, creates the submenu structure. On subsequent calls, appends
174
- * the new child to the existing submenu. Also handles migration from legacy
175
- * npm-import pattern to local imports.
176
- *
177
- * Returns true if wiring was performed, false if already wired or structure is unexpected.
178
- */
179
- export async function wireAppTsxForSystemSettings(projectRoot, pascalName, navKey, navLabelZh, navLabelEn) {
180
- const appPath = path.join(projectRoot, 'frontend', 'src', 'App.tsx');
181
- let content;
182
- try {
183
- content = await fs.readFile(appPath, 'utf-8');
184
- }
185
- catch {
186
- return false;
187
- }
188
- // Check if this page is already wired with local import (npm imports are migrated below)
189
- if (content.includes(`from './components/pages/${pascalName}'`)) {
190
- return false;
191
- }
192
- let modified = content;
193
- // ── Migration: replace legacy npm imports with local imports ──
194
- const migratedImports = [];
195
- const migratedComponents = [];
196
- // Find npm imports that contain page components (ending with 'Page')
197
- const legacyPageImport = modified.match(/import\s*\{([^}]+)\}\s*from\s*'@loom-framework\/frontend-antd';/g);
198
- if (legacyPageImport) {
199
- for (const importLine of legacyPageImport) {
200
- const namesMatch = importLine.match(/import\s*\{([^}]+)\}/);
201
- if (!namesMatch)
202
- continue;
203
- const importNames = namesMatch[1].split(',').map(s => s.trim()).filter(Boolean);
204
- const pageNames = importNames.filter(n => n.endsWith('Page'));
205
- const nonPageNames = importNames.filter(n => !n.endsWith('Page'));
206
- if (pageNames.length === 0)
207
- continue;
208
- for (const name of pageNames) {
209
- const pascalName = name.slice(0, -4);
210
- migratedImports.push(`import ${name} from './components/pages/${pascalName}';`);
211
- const navKey = pascalName.replace(/([A-Z])/g, (m, c, i) => i === 0 ? c.toLowerCase() : '-' + c.toLowerCase()).replace(/^-/, '');
212
- migratedComponents.push({ pascalName, navKey });
213
- }
214
- // Remove page names from npm import, keep non-page imports
215
- if (nonPageNames.length > 0) {
216
- const keptImport = `import { ${nonPageNames.join(', ')} } from '@loom-framework/frontend-antd';`;
217
- modified = modified.replace(importLine, keptImport);
218
- }
219
- else {
220
- modified = modified.replace(importLine, '');
221
- }
222
- }
223
- }
224
- const legacySystemImport = modified.match(/import\s*\{\s*SystemSettingsPage\s*\}\s*from\s*'@loom-framework\/frontend-antd';/);
225
- if (legacySystemImport) {
226
- modified = modified.replace(legacySystemImport[0], '');
227
- }
228
- // ── 1. Add local import ──
229
- const importLine = `import ${pascalName}Page from './components/pages/${pascalName}';`;
230
- let lastImportEnd = findLastImportEnd(modified);
231
- if (lastImportEnd === null)
232
- return false;
233
- modified = modified.slice(0, lastImportEnd) + '\n' + importLine + modified.slice(lastImportEnd);
234
- // ── 1a. Add migrated imports (from legacy npm imports) ──
235
- if (migratedImports.length > 0) {
236
- for (const migImport of migratedImports) {
237
- if (!modified.includes(migImport)) {
238
- lastImportEnd = findLastImportEnd(modified);
239
- if (lastImportEnd === null)
240
- return false;
241
- modified = modified.slice(0, lastImportEnd) + '\n' + migImport + modified.slice(lastImportEnd);
242
- }
243
- }
244
- }
245
- // ── 1b. Ensure registerMessages is imported ──
246
- if (!modified.includes('registerMessages')) {
247
- modified = modified.replace(/\}\s*from\s*'@loom-framework\/frontend-antd';/, ", registerMessages } from '@loom-framework/frontend-antd';");
248
- }
249
- // ── 1c. Register i18n keys ──
250
- // nav.system-settings (register if not present)
251
- const i18nLines = [];
252
- if (!modified.includes("'nav.system-settings'")) {
253
- i18nLines.push(`registerMessages('zh-CN', { 'nav.system-settings': '系统配置' });`, `registerMessages('en-US', { 'nav.system-settings': 'System Settings' });`);
254
- }
255
- // nav.navKey for this page
256
- if (!modified.includes(`'nav.${navKey}'`)) {
257
- i18nLines.push(`registerMessages('zh-CN', { 'nav.${navKey}': '${navLabelZh}' });`, `registerMessages('en-US', { 'nav.${navKey}': '${navLabelEn}' });`);
258
- }
259
- if (i18nLines.length > 0) {
260
- const registerBlock = i18nLines.join('\n');
261
- const afterImportsIdx = findLastImportEnd(modified);
262
- if (afterImportsIdx === null)
263
- return false;
264
- modified = modified.slice(0, afterImportsIdx) + '\n' + registerBlock + modified.slice(afterImportsIdx);
265
- }
266
- // ── 2. Handle navItems ──
267
- const childNavItem = `{ key: '${navKey}', label: t('nav.${navKey}') }`;
268
- if (modified.includes("key: 'system-settings'") && modified.includes('children:')) {
269
- // Submenu already exists — check if this child already exists
270
- const childAlreadyInNav = modified.includes(`key: '${navKey}', label: t('nav.${navKey}')`) ||
271
- modified.includes(`key: '${navKey}', label:`);
272
- if (childAlreadyInNav) {
273
- // Child already in submenu, skip navItem modification
274
- }
275
- else {
276
- // Append child to children array
277
- const childrenMatch = modified.match(/key:\s*'system-settings',\s*label:\s*t\('nav\.system-settings'\),\s*children:\s*\[/);
278
- if (childrenMatch) {
279
- // Find the closing ] of the children array
280
- const childrenStart = childrenMatch.index + childrenMatch[0].length;
281
- let depth = 1;
282
- let pos = childrenStart;
283
- while (pos < modified.length && depth > 0) {
284
- if (modified[pos] === '[')
285
- depth++;
286
- else if (modified[pos] === ']')
287
- depth--;
288
- pos++;
289
- }
290
- const childrenEnd = pos - 1;
291
- // Insert before the closing ]
292
- const beforeClose = modified.slice(0, childrenEnd).trimEnd();
293
- const needsComma = beforeClose.endsWith('}');
294
- modified = beforeClose + (needsComma ? ',' : '') + '\n ' + childNavItem + modified.slice(childrenEnd);
295
- }
296
- } // end if childAlreadyInNav
297
- }
298
- else {
299
- // No submenu exists — create it
300
- // First, remove any old flat navItem for system-settings (legacy migration)
301
- modified = modified.replace(/\{\s*key:\s*'system-settings',\s*label:\s*t\('nav\.system-settings'\)\s*\},?\n?/g, '');
302
- // Build the submenu navItem
303
- const submenuItem = ` { key: 'system-settings', label: t('nav.system-settings'), children: [
304
- ${childNavItem},
305
- ] },`;
306
- // Find where to insert: at the end of the navItems array, or before comment placeholder
307
- const commentIdx = modified.indexOf('// Add nav items');
308
- if (commentIdx !== -1) {
309
- const lineStart = modified.lastIndexOf('\n', commentIdx) + 1;
310
- modified = modified.slice(0, lineStart) + submenuItem + '\n' + modified.slice(lineStart);
311
- }
312
- else {
313
- // Append at end of navItems array
314
- const navArrayMatch = modified.match(/const navItems\s*=\s*\[/);
315
- if (!navArrayMatch)
316
- return false;
317
- const arrayStart = navArrayMatch.index + navArrayMatch[0].length;
318
- let depth = 1;
319
- let pos = arrayStart;
320
- while (pos < modified.length && depth > 0) {
321
- if (modified[pos] === '[')
322
- depth++;
323
- else if (modified[pos] === ']')
324
- depth--;
325
- pos++;
326
- }
327
- const closingBracket = pos - 1;
328
- const lineStart = modified.lastIndexOf('\n', closingBracket) + 1;
329
- modified = modified.slice(0, lineStart) + submenuItem + '\n' + modified.slice(lineStart);
330
- }
331
- }
332
- // ── 3. Add switch case ──
333
- const caseLine = ` case '${navKey}': return <${pascalName}Page />;`;
334
- // Remove legacy switch cases: case 'system-settings': return <SystemSettingsPage />;
335
- if (legacySystemImport) {
336
- modified = modified.replace(/case\s*'system-settings':\s*return\s*<SystemSettingsPage\s*\/>;\n?/g, '');
337
- }
338
- // Remove legacy npm-import switch cases only when migrating from npm imports
339
- if (migratedComponents.length > 0) {
340
- // Remove legacy npm-import switch cases for known system pages
341
- modified = modified.replace(/case\s*'model-management':\s*return\s*<ModelManagementPage\s*\/>;\n?/g, '');
342
- modified = modified.replace(/case\s*'skill-management':\s*return\s*<SkillManagementPage\s*\/>;\n?/g, '');
343
- }
344
- // Only add new case if it doesn't already exist
345
- if (!modified.includes(`case '${navKey}':`)) {
346
- // Insert before default case
347
- const defaultIdx = modified.indexOf('default:');
348
- if (defaultIdx !== -1) {
349
- const lineStart = modified.lastIndexOf('\n', defaultIdx) + 1;
350
- modified = modified.slice(0, lineStart) + caseLine + '\n' + modified.slice(lineStart);
351
- }
352
- }
353
- // Add switch cases for migrated components (they were removed above as npm-import cases)
354
- for (const { pascalName: migPascal, navKey: migKey } of migratedComponents) {
355
- if (migKey !== navKey && !modified.includes(`case '${migKey}':`)) {
356
- const migCaseLine = ` case '${migKey}': return <${migPascal}Page />;`;
357
- const defaultIdx = modified.indexOf('default:');
358
- if (defaultIdx !== -1) {
359
- const lineStart = modified.lastIndexOf('\n', defaultIdx) + 1;
360
- modified = modified.slice(0, lineStart) + migCaseLine + '\n' + modified.slice(lineStart);
361
- }
362
- }
363
- }
364
- // ── 4. Add breadcrumb mapping ──
365
- const breadcrumbLine = ` '${navKey}': [{ title: t('nav.${navKey}') }],`;
366
- // Check if breadcrumb already exists in the breadcrumbMap object (not in switch cases or other locations)
367
- const bcMapMatch = modified.match(/const\s+breadcrumbMap[^=]*=\s*\{([\s\S]*?)\n\s*\};/);
368
- const hasBreadcrumb = bcMapMatch ? bcMapMatch[1].includes(`'${navKey}':`) : modified.includes(`'${navKey}': [{ title:`);
369
- if (!hasBreadcrumb) {
370
- const breadcrumbCommentIdx = modified.indexOf('// Add breadcrumb mappings');
371
- if (breadcrumbCommentIdx !== -1) {
372
- const lineStart = modified.lastIndexOf('\n', breadcrumbCommentIdx) + 1;
373
- modified = modified.slice(0, lineStart) + breadcrumbLine + '\n' + modified.slice(lineStart);
374
- }
375
- else {
376
- // Add to existing breadcrumbMap
377
- const mapMatch = modified.match(/const\s+breadcrumbMap[\s\S]*?\{/);
378
- const mapObjMatch = modified.match(/const\s+breadcrumbMap[^=]*=\s*\{/);
379
- if (mapObjMatch) {
380
- const mapStart = mapObjMatch.index + mapObjMatch[0].length;
381
- modified = modified.slice(0, mapStart) + '\n' + breadcrumbLine + modified.slice(mapStart);
382
- }
383
- }
384
- }
385
- // Also add system-settings breadcrumb if not present
386
- if (!modified.includes("'system-settings':") && modified.includes("key: 'system-settings'")) {
387
- const sysBcLine = ` 'system-settings': [{ title: t('nav.system-settings') }],`;
388
- const breadcrumbCommentIdx = modified.indexOf('// Add breadcrumb mappings');
389
- if (breadcrumbCommentIdx !== -1) {
390
- const lineStart = modified.lastIndexOf('\n', breadcrumbCommentIdx) + 1;
391
- modified = modified.slice(0, lineStart) + sysBcLine + '\n' + modified.slice(lineStart);
392
- }
393
- }
394
- // ── 5. Migration cleanups ──
395
- // Ensure breadcrumbsMap prop on AppShell
396
- if (modified.includes('<AppShell')) {
397
- if (modified.includes('breadcrumbs={breadcrumbMap[selectedKey]}')) {
398
- modified = modified.replace('breadcrumbs={breadcrumbMap[selectedKey]}', 'breadcrumbsMap={breadcrumbMap}');
399
- }
400
- else if (!modified.includes('breadcrumbsMap=')) {
401
- modified = modified.replace('onNavClick={setSelectedKey}', 'onNavClick={setSelectedKey}\n breadcrumbsMap={breadcrumbMap}');
402
- }
403
- }
404
- // Ensure baseUrl="" on AppShell
405
- if (modified.includes('<AppShell') && !modified.includes('baseUrl=')) {
406
- modified = modified.replace('<AppShell', '<AppShell\n baseUrl=""');
407
- }
408
- await fs.writeFile(appPath, modified, 'utf-8');
409
- return true;
410
- }
411
- /**
412
- * Wire App.tsx for npm-first system settings pages.
413
- * Instead of writing local files, imports page components directly from @loom-framework/frontend-antd.
414
- * Follows the same nav/breadcrumb/switch pattern as wireAppTsxForSystemSettings but uses npm imports.
415
- *
416
- * Returns true if wiring was performed, false if already wired or structure unexpected.
417
- */
418
- export async function wireAppTsxForNpmSystemSettings(projectRoot, pascalName, navKey, navLabelZh, navLabelEn, options) {
419
- const appPath = path.join(projectRoot, 'frontend', 'src', 'App.tsx');
420
- let content;
421
- try {
422
- content = await fs.readFile(appPath, 'utf-8');
423
- }
424
- catch {
425
- return false;
426
- }
427
- // Check if this page is already wired (either local or npm import)
428
- if (content.includes(`from './components/pages/${pascalName}'`) || content.includes(`${pascalName}Page`)) {
429
- return false;
430
- }
431
- let modified = content;
432
- // ── 1. Add npm import for the page component ──
433
- const pageComponentName = `${pascalName}Page`;
434
- const existingNpmImport = modified.match(/import\s*\{([^}]+)\}\s*from\s*'@loom-framework\/frontend-antd';/);
435
- if (existingNpmImport) {
436
- const currentNames = existingNpmImport[1].split(',').map(s => s.trim()).filter(Boolean);
437
- if (!currentNames.includes(pageComponentName)) {
438
- const newNames = [...currentNames, pageComponentName].join(', ');
439
- const newImport = `import { ${newNames} } from '@loom-framework/frontend-antd';`;
440
- modified = modified.replace(existingNpmImport[0], newImport);
441
- }
442
- }
443
- else {
444
- const importLine = `import { ${pageComponentName} } from '@loom-framework/frontend-antd';`;
445
- const lastImportEnd = findLastImportEnd(modified);
446
- if (lastImportEnd === null)
447
- return false;
448
- modified = modified.slice(0, lastImportEnd) + '\n' + importLine + modified.slice(lastImportEnd);
449
- }
450
- // ── 1b. Ensure registerMessages is imported ──
451
- if (!modified.includes('registerMessages')) {
452
- const npmImportMatch = modified.match(/import\s*\{([^}]+)\}\s*from\s*'@loom-framework\/frontend-antd';/);
453
- if (npmImportMatch) {
454
- const currentNames = npmImportMatch[1].split(',').map(s => s.trim()).filter(Boolean);
455
- if (!currentNames.includes('registerMessages')) {
456
- const newNames = [...currentNames, 'registerMessages'].join(', ');
457
- const newImport = `import { ${newNames} } from '@loom-framework/frontend-antd';`;
458
- modified = modified.replace(npmImportMatch[0], newImport);
459
- }
460
- }
461
- }
462
- // ── 1c. Register i18n keys ──
463
- const i18nLines = [];
464
- if (!modified.includes("'nav.system-settings'")) {
465
- i18nLines.push(`registerMessages('zh-CN', { 'nav.system-settings': '系统配置' });`, `registerMessages('en-US', { 'nav.system-settings': 'System Settings' });`);
466
- }
467
- if (!modified.includes(`'nav.${navKey}'`)) {
468
- i18nLines.push(`registerMessages('zh-CN', { 'nav.${navKey}': '${navLabelZh}' });`, `registerMessages('en-US', { 'nav.${navKey}': '${navLabelEn}' });`);
469
- }
470
- if (i18nLines.length > 0) {
471
- const registerBlock = i18nLines.join('\n');
472
- const afterImportsIdx = findLastImportEnd(modified);
473
- if (afterImportsIdx === null)
474
- return false;
475
- modified = modified.slice(0, afterImportsIdx) + '\n' + registerBlock + modified.slice(afterImportsIdx);
476
- }
477
- // ── 2. Handle navItems ── (same logic as wireAppTsxForSystemSettings)
478
- // Skip nav item for pages accessed via other UI elements (e.g., notification bell icon)
479
- if (!options?.skipNav) {
480
- const childNavItem = `{ key: '${navKey}', label: t('nav.${navKey}') }`;
481
- if (modified.includes("key: 'system-settings'") && modified.includes('children:')) {
482
- const childAlreadyInNav = modified.includes(`key: '${navKey}', label: t('nav.${navKey}')`) ||
483
- modified.includes(`key: '${navKey}', label:`);
484
- if (!childAlreadyInNav) {
485
- const childrenMatch = modified.match(/key:\s*'system-settings',\s*label:\s*t\('nav\.system-settings'\),\s*children:\s*\[/);
486
- if (childrenMatch) {
487
- const childrenStart = childrenMatch.index + childrenMatch[0].length;
488
- let depth = 1;
489
- let pos = childrenStart;
490
- while (pos < modified.length && depth > 0) {
491
- if (modified[pos] === '[')
492
- depth++;
493
- else if (modified[pos] === ']')
494
- depth--;
495
- pos++;
496
- }
497
- const childrenEnd = pos - 1;
498
- const beforeClose = modified.slice(0, childrenEnd).trimEnd();
499
- const needsComma = beforeClose.endsWith('}');
500
- modified = beforeClose + (needsComma ? ',' : '') + '\n ' + childNavItem + modified.slice(childrenEnd);
501
- }
502
- }
503
- }
504
- else {
505
- modified = modified.replace(/\{\s*key:\s*'system-settings',\s*label:\s*t\('nav\.system-settings'\)\s*\},?\n?/g, '');
506
- const submenuItem = ` { key: 'system-settings', label: t('nav.system-settings'), children: [
507
- ${childNavItem},
508
- ] },`;
509
- const commentIdx = modified.indexOf('// Add nav items');
510
- if (commentIdx !== -1) {
511
- const lineStart = modified.lastIndexOf('\n', commentIdx) + 1;
512
- modified = modified.slice(0, lineStart) + submenuItem + '\n' + modified.slice(lineStart);
513
- }
514
- else {
515
- const navArrayMatch = modified.match(/const navItems\s*=\s*\[/);
516
- if (!navArrayMatch)
517
- return false;
518
- const arrayStart = navArrayMatch.index + navArrayMatch[0].length;
519
- let depth = 1;
520
- let pos = arrayStart;
521
- while (pos < modified.length && depth > 0) {
522
- if (modified[pos] === '[')
523
- depth++;
524
- else if (modified[pos] === ']')
525
- depth--;
526
- pos++;
527
- }
528
- const closingBracket = pos - 1;
529
- const lineStart = modified.lastIndexOf('\n', closingBracket) + 1;
530
- modified = modified.slice(0, lineStart) + submenuItem + '\n' + modified.slice(lineStart);
531
- }
532
- }
533
- } // end if (!skipNav)
534
- // ── 3. Add switch case ──
535
- const caseLine = ` case '${navKey}': return <${pageComponentName} />;`;
536
- if (!modified.includes(`case '${navKey}':`)) {
537
- const defaultIdx = modified.indexOf('default:');
538
- if (defaultIdx !== -1) {
539
- const lineStart = modified.lastIndexOf('\n', defaultIdx) + 1;
540
- modified = modified.slice(0, lineStart) + caseLine + '\n' + modified.slice(lineStart);
541
- }
542
- }
543
- // ── 4. Add breadcrumb mapping ──
544
- const breadcrumbLine = ` '${navKey}': [{ title: t('nav.${navKey}') }],`;
545
- const bcMapMatch = modified.match(/const\s+breadcrumbMap[^=]*=\s*\{([\s\S]*?)\n\s*\};/);
546
- const hasBreadcrumb = bcMapMatch ? bcMapMatch[1].includes(`'${navKey}':`) : modified.includes(`'${navKey}': [{ title:`);
547
- if (!hasBreadcrumb) {
548
- const breadcrumbCommentIdx = modified.indexOf('// Add breadcrumb mappings');
549
- if (breadcrumbCommentIdx !== -1) {
550
- const lineStart = modified.lastIndexOf('\n', breadcrumbCommentIdx) + 1;
551
- modified = modified.slice(0, lineStart) + breadcrumbLine + '\n' + modified.slice(lineStart);
552
- }
553
- }
554
- // Also add system-settings breadcrumb if not present
555
- if (!modified.includes("'system-settings':") && modified.includes("key: 'system-settings'")) {
556
- const sysBcLine = ` 'system-settings': [{ title: t('nav.system-settings') }],`;
557
- const breadcrumbCommentIdx = modified.indexOf('// Add breadcrumb mappings');
558
- if (breadcrumbCommentIdx !== -1) {
559
- const lineStart = modified.lastIndexOf('\n', breadcrumbCommentIdx) + 1;
560
- modified = modified.slice(0, lineStart) + sysBcLine + '\n' + modified.slice(lineStart);
561
- }
562
- }
563
- // ── 5. Migration cleanups ──
564
- if (modified.includes('<AppShell')) {
565
- if (modified.includes('breadcrumbs={breadcrumbMap[selectedKey]}')) {
566
- modified = modified.replace('breadcrumbs={breadcrumbMap[selectedKey]}', 'breadcrumbsMap={breadcrumbMap}');
567
- }
568
- else if (!modified.includes('breadcrumbsMap=')) {
569
- modified = modified.replace('onNavClick={setSelectedKey}', 'onNavClick={setSelectedKey}\n breadcrumbsMap={breadcrumbMap}');
570
- }
571
- }
572
- if (modified.includes('<AppShell') && !modified.includes('baseUrl=')) {
573
- modified = modified.replace('<AppShell', '<AppShell\n baseUrl=""');
574
- }
575
- await fs.writeFile(appPath, modified, 'utf-8');
576
- return true;
577
- }
578
- /**
579
- * Wire App.tsx for Auth: inject AuthProvider + AuthGuard wrapping.
580
- * Provider order: LoomConfigProvider > AuthProvider > AuthGuard > AppContent
581
- *
582
- * Returns true if wiring was performed, false if already wired or structure unexpected.
583
- */
584
- export async function wireAppTsxForAuth(projectRoot) {
585
- const appPath = path.join(projectRoot, 'frontend', 'src', 'App.tsx');
586
- let content;
587
- try {
588
- content = await fs.readFile(appPath, 'utf-8');
589
- }
590
- catch {
591
- return false;
592
- }
593
- // Check if auth is already wired
594
- if (content.includes('AuthProvider') && content.includes('AuthGuard')) {
595
- return false;
596
- }
597
- let modified = content;
598
- // 1. Add AuthProvider + AuthGuard imports after last import
599
- const authImport = `import { AuthProvider, AuthGuard } from '@loom-framework/frontend-antd';`;
600
- const lastImportEnd = findLastImportEnd(modified);
601
- if (lastImportEnd === null)
602
- return false;
603
- modified = modified.slice(0, lastImportEnd) + '\n' + authImport + modified.slice(lastImportEnd);
604
- // 2. Wrap the main content return with AuthProvider > AuthGuard
605
- // Look for <LoomConfigProvider> ... </LoomConfigProvider>
606
- const loomProviderMatch = modified.match(/<LoomConfigProvider[^>]*>/);
607
- if (loomProviderMatch) {
608
- const providerStart = loomProviderMatch.index + loomProviderMatch[0].length;
609
- const closingTag = '</LoomConfigProvider>';
610
- const closingIdx = modified.lastIndexOf(closingTag);
611
- if (closingIdx === -1)
612
- return false;
613
- modified = modified.slice(0, providerStart) +
614
- '\n <AuthProvider>\n <AuthGuard>' +
615
- modified.slice(providerStart, closingIdx) +
616
- '</AuthGuard>\n </AuthProvider>\n ' +
617
- modified.slice(closingIdx);
618
- }
619
- await fs.writeFile(appPath, modified, 'utf-8');
620
- return true;
621
- }
622
- /**
623
- * Wire App.tsx for Notifications: inject NotificationCenterProvider wrapping
624
- * and add notification switch case + breadcrumb + i18n.
625
- *
626
- * Navigation is via the bell icon in AppShell header (no sidebar navItem needed).
627
- *
628
- * Provider order: LoomConfigProvider > AuthProvider > NotificationCenterProvider > AuthGuard > AppContent
629
- *
630
- * Returns true if wiring was performed, false if already wired or structure unexpected.
631
- */
632
- export async function wireAppTsxForNotification(projectRoot) {
633
- const appPath = path.join(projectRoot, 'frontend', 'src', 'App.tsx');
634
- let content;
635
- try {
636
- content = await fs.readFile(appPath, 'utf-8');
637
- }
638
- catch {
639
- return false;
640
- }
641
- // Check if notification is already wired
642
- if (content.includes('NotificationCenterProvider')) {
643
- return false;
644
- }
645
- let modified = content;
646
- // 1. Add NotificationCenterProvider to existing @loom-framework/frontend-antd import
647
- // (NotificationCenterPage and NotificationDetailPage are already added by wireAppTsxForNpmSystemSettings)
648
- const existingNpmImport = modified.match(/import\s*\{([^}]+)\}\s*from\s*'@loom-framework\/frontend-antd';/);
649
- if (existingNpmImport) {
650
- const currentNames = existingNpmImport[1].split(',').map(s => s.trim()).filter(Boolean);
651
- if (!currentNames.includes('NotificationCenterProvider')) {
652
- const newNames = [...currentNames, 'NotificationCenterProvider'].join(', ');
653
- const newImport = `import { ${newNames} } from '@loom-framework/frontend-antd';`;
654
- modified = modified.replace(existingNpmImport[0], newImport);
655
- }
656
- }
657
- else {
658
- // Fallback: no existing import, add a new one
659
- const notifImport = `import { NotificationCenterProvider } from '@loom-framework/frontend-antd';`;
660
- const lastImportEnd = findLastImportEnd(modified);
661
- if (lastImportEnd === null)
662
- return false;
663
- modified = modified.slice(0, lastImportEnd) + '\n' + notifImport + modified.slice(lastImportEnd);
664
- }
665
- // 2. Wrap with NotificationCenterProvider (inside AuthProvider, outside AuthGuard)
666
- if (modified.includes('<AuthProvider>') && modified.includes('<AuthGuard>')) {
667
- modified = modified.replace('<AuthGuard>', '<NotificationCenterProvider>\n <AuthGuard>');
668
- modified = modified.replace('</AuthGuard>', '</AuthGuard>\n </NotificationCenterProvider>');
669
- }
670
- else if (modified.includes('<LoomConfigProvider')) {
671
- const loomProviderMatch2 = modified.match(/<LoomConfigProvider[^>]*>/);
672
- if (loomProviderMatch2) {
673
- const providerStart = loomProviderMatch2.index + loomProviderMatch2[0].length;
674
- const closingTag = '</LoomConfigProvider>';
675
- const closingIdx = modified.lastIndexOf(closingTag);
676
- if (closingIdx === -1)
677
- return false;
678
- modified = modified.slice(0, providerStart) +
679
- '\n <NotificationCenterProvider>' +
680
- modified.slice(providerStart, closingIdx) +
681
- '</NotificationCenterProvider>\n ' +
682
- modified.slice(closingIdx);
683
- }
684
- }
685
- // 3. Ensure registerMessages is imported
686
- if (!modified.includes('registerMessages')) {
687
- modified = modified.replace(/\}\s*from\s*'@loom-framework\/frontend-antd';/, ", registerMessages } from '@loom-framework/frontend-antd';");
688
- }
689
- // 4. Register i18n keys for notifications breadcrumb
690
- if (!modified.includes("'nav.notifications'")) {
691
- const registerBlock = `registerMessages('zh-CN', { 'nav.notifications': '通知', 'notification.detail': '通知详情' });\nregisterMessages('en-US', { 'nav.notifications': 'Notifications', 'notification.detail': 'Notification Detail' });`;
692
- const afterImportsIdx = findLastImportEnd(modified);
693
- if (afterImportsIdx === null)
694
- return false;
695
- modified = modified.slice(0, afterImportsIdx) + '\n' + registerBlock + modified.slice(afterImportsIdx);
696
- }
697
- // 5. Add switch cases for notifications pages
698
- const caseLine = ` case 'notifications': return <NotificationCenterPage />;`;
699
- const detailCaseLine = ` case 'notification-detail': return <NotificationDetailPage />;`;
700
- if (!modified.includes("case 'notifications':")) {
701
- const defaultIdx = modified.indexOf('default:');
702
- if (defaultIdx !== -1) {
703
- const lineStart = modified.lastIndexOf('\n', defaultIdx) + 1;
704
- modified = modified.slice(0, lineStart) + caseLine + '\n' + modified.slice(lineStart);
705
- }
706
- }
707
- if (!modified.includes("case 'notification-detail':")) {
708
- const notifCaseIdx = modified.indexOf("case 'notifications':");
709
- if (notifCaseIdx !== -1) {
710
- const lineStart = modified.lastIndexOf('\n', notifCaseIdx) + 1;
711
- const afterCase = modified.indexOf('\n', notifCaseIdx) + 1;
712
- modified = modified.slice(0, afterCase) + detailCaseLine + '\n' + modified.slice(afterCase);
713
- }
714
- }
715
- // 6. Add breadcrumb mapping for notifications
716
- const breadcrumbLine = ` 'notifications': [{ title: t('nav.notifications') }],`;
717
- const detailBreadcrumbLine = ` 'notification-detail': [{ title: t('nav.notifications'), path: 'notifications' }, { title: t('notification.detail') }],`;
718
- const bcMapMatch = modified.match(/const\s+breadcrumbMap[^=]*=\s*\{([\s\S]*?)\n\s*\};/);
719
- const hasBreadcrumb = bcMapMatch ? bcMapMatch[1].includes("'notifications':") : modified.includes("'notifications': [{ title:");
720
- const hasDetailBreadcrumb = bcMapMatch ? bcMapMatch[1].includes("'notification-detail':") : modified.includes("'notification-detail':");
721
- const breadcrumbsToAdd = [];
722
- if (!hasBreadcrumb)
723
- breadcrumbsToAdd.push(breadcrumbLine);
724
- if (!hasDetailBreadcrumb)
725
- breadcrumbsToAdd.push(detailBreadcrumbLine);
726
- if (breadcrumbsToAdd.length > 0) {
727
- const allBreadcrumbLines = breadcrumbsToAdd.join('\n') + '\n';
728
- const breadcrumbCommentIdx = modified.indexOf('// Add breadcrumb mappings');
729
- if (breadcrumbCommentIdx !== -1) {
730
- const lineStart = modified.lastIndexOf('\n', breadcrumbCommentIdx) + 1;
731
- modified = modified.slice(0, lineStart) + allBreadcrumbLines + modified.slice(lineStart);
732
- }
733
- else {
734
- const mapObjMatch = modified.match(/const\s+breadcrumbMap[^=]*=\s*\{/);
735
- if (mapObjMatch) {
736
- const mapStart = mapObjMatch.index + mapObjMatch[0].length;
737
- modified = modified.slice(0, mapStart) + '\n' + allBreadcrumbLines + modified.slice(mapStart);
738
- }
739
- }
740
- }
741
- // 7. Add onNotificationViewAll + onNotificationClick props to AppShell
742
- if (modified.includes('<AppShell') && !modified.includes('onNotificationViewAll')) {
743
- modified = modified.replace('onNavClick={setSelectedKey}', "onNavClick={setSelectedKey}\n onNotificationViewAll={() => setSelectedKey('notifications')}\n onNotificationClick={(id) => setSelectedKey('notification-detail')}");
744
- }
745
- await fs.writeFile(appPath, modified, 'utf-8');
746
- return true;
747
- }
748
145
  //# sourceMappingURL=app-tsx-wiring.js.map