create-mithaq 0.1.1 → 0.2.1
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/LICENSES/openstatus-time-picker.txt +21 -0
- package/index.js +457 -63
- package/package.json +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
- package/template/apps/asas-backoffice/src/components.ts +35 -0
- package/template/apps/asas-backoffice/src/first-user.ts +14 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
- package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
- package/template/apps/asas-backoffice/src/redirect.ts +1 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/request-headers.ts +1 -1
- package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
- package/template/apps/asas-backoffice/src/router.tsx +18 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
- package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
- package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
- package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
- package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
- package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
- package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
- package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
- package/template/apps/asas-backoffice/src/server.ts +30 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
- package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
- package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
- package/template/apps/asas-tanstack/package.json +4 -2
- package/template/apps/asas-tanstack/src/api.ts +1 -1
- package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
- package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
- package/template/apps/asas-tanstack/src/components.ts +35 -0
- package/template/apps/asas-tanstack/src/copy.ts +416 -79
- package/template/apps/asas-tanstack/src/first-run.ts +16 -0
- package/template/apps/asas-tanstack/src/links.ts +1 -1
- package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
- package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
- package/template/apps/asas-tanstack/src/redirect.ts +20 -1
- package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
- package/template/apps/asas-tanstack/src/router.tsx +12 -1
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
- package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
- package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
- package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
- package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
- package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
- package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
- package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
- package/template/apps/asas-tanstack/src/screens.tsx +46 -26
- package/template/apps/asas-tanstack/src/server.ts +10 -0
- package/template/apps/asas-tanstack/src/slide.ts +1 -1
- package/template/apps/asas-tanstack/src/styles.css +7 -3
- package/template/apps/asas-tanstack/tsconfig.json +2 -2
- package/template/apps/asas-tanstack/vite.config.ts +7 -1
- package/template/package.json +3 -3
- package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
- package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
- package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
- package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
- package/template/templates/asas/.env.example +22 -5
- package/template/templates/asas/AGENTS.md +101 -0
- package/template/templates/asas/CLAUDE.md +1 -0
- package/template/templates/asas/README.md +1094 -0
- package/template/templates/asas/package.json +19 -4
- package/template/templates/asas/src/asas.ts +81 -56
- package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
- package/template/templates/asas/src/dev-setup.ts +112 -0
- package/template/templates/asas/src/local-database.ts +22 -0
- package/template/templates/asas/src/messages/account.json +129 -103
- package/template/templates/asas/src/messages/auditLog.json +223 -105
- package/template/templates/asas/src/messages/backofficeLog.json +132 -0
- package/template/templates/asas/src/messages/courses.json +256 -214
- package/template/templates/asas/src/messages/enrollments.json +45 -27
- package/template/templates/asas/src/messages/invitation.json +72 -56
- package/template/templates/asas/src/messages/localUsers.json +34 -28
- package/template/templates/asas/src/messages/media.json +31 -21
- package/template/templates/asas/src/messages/member.json +59 -39
- package/template/templates/asas/src/messages/memberRoles.json +37 -31
- package/template/templates/asas/src/messages/mithaq.json +206 -162
- package/template/templates/asas/src/messages/organization.json +63 -41
- package/template/templates/asas/src/messages/organizationRole.json +59 -47
- package/template/templates/asas/src/messages/roles.json +59 -45
- package/template/templates/asas/src/messages/session.json +99 -79
- package/template/templates/asas/src/messages/sessions.json +110 -80
- package/template/templates/asas/src/messages/settings.json +51 -53
- package/template/templates/asas/src/messages/tags.json +31 -21
- package/template/templates/asas/src/messages/user.json +109 -87
- package/template/templates/asas/src/messages/verification.json +59 -47
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
- package/template/templates/asas/src/migrations/schema.ts +1 -0
- package/template/templates/asas/src/mithaq.ts +4 -406
- package/template/templates/asas/src/organizations.ts +3 -0
- package/template/templates/asas/src/seed.ts +147 -45
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
- package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
- package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
- package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +637 -438
- package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
- package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
- package/template/templates/asas/src/ui-portal/home.tsx +9 -13
- package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
- package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
- package/template/templates/features/audit-log/index.ts +4 -0
- package/template/templates/features/audit-log/package.json +13 -0
- package/template/templates/features/auth/index.ts +11 -0
- package/template/templates/{asas/src → features/auth}/local-users/local-users.ts +7 -7
- package/template/templates/{auth → features/auth}/package.json +2 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
- package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
- package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
- package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
- package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
- package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
- package/template/templates/features/courses/courses/cover-reads.ts +33 -0
- package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
- package/template/templates/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
- package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
- package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
- package/template/templates/features/courses/domain/archive.ts +39 -0
- package/template/templates/features/courses/domain/on-offer.ts +6 -0
- package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
- package/template/templates/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
- package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
- package/template/templates/features/courses/index.ts +8 -0
- package/template/templates/features/courses/package.json +24 -0
- package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
- package/template/templates/features/courses/portal/ui/course.tsx +176 -0
- package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
- package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
- package/template/templates/features/courses/portal/ui/reads.ts +76 -0
- package/template/templates/features/courses/portal/ui/rules.ts +87 -0
- package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
- package/template/templates/features/courses/sessions/seats.ts +42 -0
- package/template/templates/{asas/src → features/courses}/sessions/sessions.ts +45 -14
- package/template/templates/features/media/index.ts +10 -0
- package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
- package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
- package/template/templates/features/media/package.json +19 -0
- package/template/templates/features/roles/index.ts +4 -0
- package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
- package/template/templates/features/roles/package.json +21 -0
- package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
- package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
- package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
- package/template/templates/features/settings/index.ts +3 -0
- package/template/templates/features/settings/package.json +13 -0
- package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
- package/template/templates/features/tags/index.ts +3 -0
- package/template/templates/features/tags/package.json +14 -0
- package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
- package/template/templates/mithaq/package.json +33 -0
- package/template/templates/mithaq/src/mail/index.ts +32 -0
- package/template/templates/mithaq/src/mail/mailers.ts +36 -0
- package/template/templates/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
- package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
- package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
- package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
- package/template/templates/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
- package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
- package/template/templates/mithaq/src/mithaq.ts +593 -0
- package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
- package/template/templates/{asas → mithaq}/src/roles/grants.ts +1 -1
- package/template/templates/mithaq/src/roles/member-access.ts +11 -0
- package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
- package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
- package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
- package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
- package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
- package/template/templates/mithaq/src/ui/activity.tsx +527 -0
- package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
- package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
- package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
- package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
- package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
- package/template/apps/asas-platform/src/redirect.ts +0 -1
- package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
- package/template/apps/asas-platform/src/router.tsx +0 -7
- package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
- package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
- package/template/apps/asas-platform/src/server.ts +0 -20
- package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
- package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
- package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
- package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
- package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
- package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
- package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
- package/template/templates/asas/src/rich-text-nodes.ts +0 -11
- package/template/templates/asas/src/roles.ts +0 -16
- package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
- package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
- package/template/templates/auth/src/index.ts +0 -690
- /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
- /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
- /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
- /package/template/templates/{asas → mithaq}/components.json +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/table-page.tsx +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OpenStatus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/index.js
CHANGED
|
@@ -1,61 +1,361 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `
|
|
3
|
-
// package carries, at the version of `@drhomet/mithaq` this
|
|
4
|
-
// published with. Plain JavaScript on Node's standard library, so
|
|
5
|
-
//
|
|
2
|
+
// `npm create mithaq my-app` (or pnpm, yarn or bun): lays a new project down from
|
|
3
|
+
// the template this package carries, at the version of `@drhomet/mithaq` this
|
|
4
|
+
// package was published with. Plain JavaScript on Node's standard library, so
|
|
5
|
+
// every package manager runs it with nothing to install first.
|
|
6
6
|
import { spawnSync } from 'node:child_process'
|
|
7
7
|
import { randomBytes } from 'node:crypto'
|
|
8
|
-
import { cpSync, existsSync, readdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs'
|
|
9
|
-
import { basename, dirname, join } from 'node:path'
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import { basename, dirname, join, relative } from 'node:path'
|
|
10
10
|
import { createInterface } from 'node:readline/promises'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import { parseArgs } from 'node:util'
|
|
13
13
|
|
|
14
14
|
const readJson = (file) => JSON.parse(readFileSync(file, 'utf8'))
|
|
15
15
|
const writeJson = (file, value) => writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
|
|
16
|
+
const edit = (file, change) => writeFileSync(file, change(readFileSync(file, 'utf8')))
|
|
16
17
|
|
|
17
18
|
const generateSecret = () => randomBytes(32).toString('hex')
|
|
18
19
|
|
|
20
|
+
/** The package manager `npm_config_user_agent` names (`pnpm/10.15.0 npm/? node/v24…`), when it is one of the four. */
|
|
21
|
+
export const managerOf = (agent = '') => ['npm', 'pnpm', 'yarn', 'bun'].find((name) => agent.startsWith(`${name}/`))
|
|
22
|
+
|
|
19
23
|
/**
|
|
20
|
-
* Pins the template's manifests to what a published project installs
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `file:` spec is an error rather than a pin, because nothing published stands
|
|
27
|
-
* behind it.
|
|
24
|
+
* Pins the template's manifests to what a published project installs, in
|
|
25
|
+
* `dependencies` and `devDependencies` alike. `mithaq` and `mithaq-admin`
|
|
26
|
+
* become the published packages under the names every import uses; `asas` and
|
|
27
|
+
* `@asas/auth` stay `"*"`, the project's own workspaces. Any other
|
|
28
|
+
* `workspace:`, `link:` or `file:` spec is an error rather than a pin,
|
|
29
|
+
* because nothing published stands behind it.
|
|
28
30
|
*/
|
|
29
|
-
export function updatePackageJSONDependencies({ mithaq, packageJson }) {
|
|
31
|
+
export function updatePackageJSONDependencies({ mithaq, mithaqAdmin, packageJson }) {
|
|
30
32
|
for (const dependencies of [packageJson.dependencies, packageJson.devDependencies]) {
|
|
31
33
|
if (!dependencies) continue
|
|
32
34
|
if ('mithaq' in dependencies) dependencies.mithaq = mithaq
|
|
33
|
-
|
|
35
|
+
if ('mithaq-admin' in dependencies) dependencies['mithaq-admin'] = mithaqAdmin
|
|
34
36
|
for (const [name, spec] of Object.entries(dependencies)) {
|
|
35
|
-
if (spec !== mithaq && /^(workspace|link|file):/.test(spec)) {
|
|
37
|
+
if (spec !== mithaq && spec !== mithaqAdmin && /^(workspace|link|file):/.test(spec)) {
|
|
36
38
|
throw new Error(`The template's dependency ${name} is ${spec}, which only resolves inside the mithaq repository.`)
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Replaces `from`, a string or a pattern with no group, where the template holds it exactly `count` times, so a
|
|
46
|
+
* template line that moved fails creation instead of shipping a wrong path.
|
|
47
|
+
*/
|
|
48
|
+
const once = (text, from, to, count = 1) => {
|
|
49
|
+
if (text.split(from).length !== count + 1) throw new Error(`The template no longer holds ${from} ${count === 1 ? 'exactly once' : `${count} times`}.`)
|
|
50
|
+
// A function, so a replacement pattern such as `$'` or `$&` in what a person typed, an organization's name among it, is written as typed.
|
|
51
|
+
return typeof from === 'string' ? text.replaceAll(from, () => to) : text.replace(from, () => to)
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What a project may hold beyond the language, the asas, the `auth` feature and the `tags` example. Each add-on names the
|
|
55
|
+
* add-ons it needs, the files that are its alone (removed when it is left out), the files that stand in for it (removed
|
|
56
|
+
* when it is chosen), the npm packages only it uses (a name ending in `/` is a whole scope), the keys of its words in
|
|
57
|
+
* the shared copy file, `mithaq.json`, which can hold no region, and whether it brings
|
|
58
|
+
* tables, without which the template's migrations, written for every table, do not fit the project. Its lines in a
|
|
59
|
+
* shared file sit in `// add-on: <name>` regions, which `keepAddOns` reads. `rich-text` and `uploads` bring words of the
|
|
60
|
+
* language, kinds and a target, so a project's vocabulary follows the add-ons it holds.
|
|
61
|
+
*/
|
|
62
|
+
export const addOns = {
|
|
63
|
+
roles: {
|
|
64
|
+
needs: [],
|
|
65
|
+
files: ['packages/features/roles', 'packages/mithaq/src/roles/grants.ts', 'packages/asas/src/ui-dashboard/members.tsx', 'packages/asas/src/ui-dashboard/members-card.tsx', 'packages/asas/src/messages/roles.json', 'packages/asas/src/messages/memberRoles.json'],
|
|
66
|
+
without: ['packages/mithaq/src/roles/default-member-roles.ts', 'packages/mithaq/src/roles/member-access.ts'],
|
|
67
|
+
packages: [],
|
|
68
|
+
copy: [],
|
|
69
|
+
tables: true,
|
|
70
|
+
},
|
|
71
|
+
'audit-log': {
|
|
72
|
+
needs: [],
|
|
73
|
+
files: ['packages/features/audit-log', 'packages/mithaq/src/mithaq/audit-sink.ts', 'packages/mithaq/src/ui/activity.tsx', 'packages/mithaq/src/ui/activity-page.tsx', 'packages/asas/src/messages/auditLog.json', 'packages/asas/src/messages/backofficeLog.json'],
|
|
74
|
+
without: [],
|
|
75
|
+
packages: [],
|
|
76
|
+
copy: [],
|
|
77
|
+
tables: true,
|
|
78
|
+
},
|
|
79
|
+
settings: { needs: [], files: ['packages/features/settings', 'packages/asas/src/messages/settings.json'], without: [], packages: [], copy: [], tables: true },
|
|
80
|
+
// The `richText` kind, its Lexical nodes and editor.
|
|
81
|
+
'rich-text': {
|
|
82
|
+
needs: [],
|
|
83
|
+
files: ['packages/mithaq/src/mithaq/rich-text-nodes.ts', 'packages/mithaq/src/ui/rich-text-nodes.tsx'],
|
|
84
|
+
without: [],
|
|
85
|
+
packages: ['lexical', '@lexical/'],
|
|
86
|
+
copy: ['kinds.richText', 'kinds.richText.invalid'],
|
|
87
|
+
tables: false,
|
|
88
|
+
},
|
|
89
|
+
// The `upload` kind, the storage target and its adapters.
|
|
90
|
+
uploads: { needs: [], files: ['packages/mithaq/src/storage'], without: [], packages: ['aws4fetch', 'file-type', 'image-dimensions'], copy: ['kinds.upload', 'kinds.upload.invalid'], tables: false },
|
|
91
|
+
// The organization's images, which an upload field points at.
|
|
92
|
+
media: { needs: ['uploads'], files: ['packages/features/media', 'packages/asas/src/messages/media.json'], without: [], packages: [], copy: [], tables: true },
|
|
93
|
+
// The worked example: its business roles are the roles add-on's, its edit pages draw the audit log's activity, its
|
|
94
|
+
// details are rich text and a course's cover is one of the media.
|
|
95
|
+
courses: {
|
|
96
|
+
needs: ['media', 'roles', 'audit-log', 'rich-text'],
|
|
97
|
+
files: ['packages/features/courses', 'packages/asas/src/ui-dashboard/dashboard.tsx', 'packages/asas/src/ui-portal/courses-home.tsx', ...['courses', 'sessions', 'enrollments'].map((one) => `packages/asas/src/messages/${one}.json`)],
|
|
98
|
+
without: ['packages/asas/src/ui-dashboard/home.tsx', 'packages/asas/src/ui-portal/home.tsx'],
|
|
99
|
+
packages: [],
|
|
100
|
+
copy: [],
|
|
101
|
+
tables: true,
|
|
102
|
+
},
|
|
103
|
+
backoffice: { needs: [], files: ['apps/asas-backoffice'], without: [], packages: [], copy: [], tables: false },
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The add-ons `asked` names with every one they need, in the table's order, and the ones added for that. A name the
|
|
108
|
+
* table does not hold is refused with the list, so a typo never makes a smaller project than asked for.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveAddOns(asked) {
|
|
111
|
+
const unknown = asked.filter((name) => !Object.hasOwn(addOns, name))
|
|
112
|
+
if (unknown.length) throw new Error(`No add-on is named ${unknown.map((name) => `"${name}"`).join(', ')}. The add-ons are ${Object.keys(addOns).join(', ')}.`)
|
|
113
|
+
const chosen = new Set()
|
|
114
|
+
const take = (name) => {
|
|
115
|
+
if (chosen.has(name)) return
|
|
116
|
+
chosen.add(name)
|
|
117
|
+
for (const one of addOns[name].needs) take(one)
|
|
118
|
+
}
|
|
119
|
+
for (const name of asked) take(name)
|
|
120
|
+
const ordered = Object.keys(addOns).filter((name) => chosen.has(name))
|
|
121
|
+
return { chosen: ordered, added: ordered.filter((name) => !asked.includes(name)) }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Keeps the lines of `text` the chosen add-ons hold. A region from `// add-on: <names>` to `// end add-on: <names>` keeps
|
|
126
|
+
* its lines when every add-on it names is chosen; one from `// without add-on: <names>` to its end holds commented lines,
|
|
127
|
+
* uncommented when they are not. The marker lines go either way, so a project reads as if written for its add-ons. A
|
|
128
|
+
* marker that is unbalanced or names no add-on fails creation, as a template line that moved does.
|
|
129
|
+
*/
|
|
130
|
+
export function keepAddOns(text, chosen) {
|
|
131
|
+
const open = []
|
|
132
|
+
const kept = []
|
|
133
|
+
for (const line of text.split('\n')) {
|
|
134
|
+
const marker = /^\s*\/\/ (end )?(without )?add-on: (.+)$/.exec(line)
|
|
135
|
+
// A misspelled marker would otherwise read as a comment, and leave its region in every project.
|
|
136
|
+
if (!marker && /^\s*\/\/.*\badd-?on\b:/.test(line)) throw new Error(`The template holds "${line.trim()}", which reads like an add-on marker and is none.`)
|
|
137
|
+
if (!marker) {
|
|
138
|
+
if (open.every((one) => one.keep)) kept.push(open.some((one) => one.without) ? line.replace(/^(\s*)\/\/ ?/, '$1') : line)
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
const [, end, without, names] = marker
|
|
142
|
+
const label = `${without ?? ''}add-on: ${names}`
|
|
143
|
+
if (end) {
|
|
144
|
+
if (open.pop()?.label !== label) throw new Error(`The template closes "${label}" where no such region is open.`)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const named = names.split(', ')
|
|
148
|
+
for (const name of named) if (!Object.hasOwn(addOns, name)) throw new Error(`The template marks a region of the add-on "${name}", which the table does not hold.`)
|
|
149
|
+
const all = named.every((name) => chosen.includes(name))
|
|
150
|
+
open.push({ label, without: Boolean(without), keep: without ? !all : all })
|
|
151
|
+
}
|
|
152
|
+
if (open.length) throw new Error(`The template leaves "${open.at(-1).label}" open.`)
|
|
153
|
+
return kept.join('\n')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Every text file a project's own commands or imports read, outside what an install writes. */
|
|
157
|
+
const textsUnder = (folder) =>
|
|
158
|
+
readdirSync(folder, { recursive: true, encoding: 'utf8' })
|
|
159
|
+
.filter((file) => !file.split(/[\\/]/).includes('node_modules') && /\.(tsx?|jsx?|json|css|md)$/.test(file))
|
|
160
|
+
.map((file) => join(folder, file))
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Whether a package's `exports` names `sub` (`''` for the package itself) as Node resolves it: the key equal to it, else
|
|
164
|
+
* the longest `*` pattern it lies under, and not where that key's value is `null`, an exclusion. A `.` or `..` segment
|
|
165
|
+
* names nothing, since it would climb out of what the key exports.
|
|
166
|
+
*/
|
|
167
|
+
export const exportsPath = (exports, sub) => {
|
|
168
|
+
if (sub.split('/').some((segment) => segment === '.' || segment === '..')) return false
|
|
169
|
+
const asked = sub === '' ? '.' : `.${sub}`
|
|
170
|
+
const [, value] = Object.entries(exports ?? {})
|
|
171
|
+
.filter(([key]) => (key.endsWith('*') ? asked.startsWith(key.slice(0, -1)) : key === asked))
|
|
172
|
+
.sort(([a], [b]) => (b === asked) - (a === asked) || b.length - a.length)[0] ?? [, null]
|
|
173
|
+
return value !== null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Folds the monorepo into one package at `project`: the language's and the asas's `src/` side by side, each feature
|
|
178
|
+
* under `src/features/`, and the tenant app, whose `src/` holds no file of the same name as theirs. The package keeps
|
|
179
|
+
* the name `asas`, because the screen host imports it by that name and a package reaches itself through its own
|
|
180
|
+
* `exports`, which also keeps it out of Vite's server bundle, so `import.meta.url` and each `ui()` file stay real.
|
|
181
|
+
* Every import of the language or a feature becomes a path.
|
|
182
|
+
*/
|
|
183
|
+
const foldIntoOnePackage = (project) => {
|
|
184
|
+
const at = (...path) => join(project, ...path)
|
|
185
|
+
const features = readdirSync(at('packages/features'))
|
|
186
|
+
const [root, language, asas, app, ...ofFeatures] = ['', 'packages/mithaq', 'packages/asas', 'apps/asas-tanstack', ...features.map((one) => `packages/features/${one}`)].map((folder) => readJson(at(folder, 'package.json')))
|
|
187
|
+
// Each path the language names from the asas's folder becomes the package's own, before any import is rewritten.
|
|
188
|
+
edit(at('packages/mithaq/src/mithaq.ts'), (text) =>
|
|
189
|
+
[
|
|
190
|
+
["new URL('../../asas/src/', import.meta.url)", "new URL('./', import.meta.url)"],
|
|
191
|
+
["new URL('../../features/', import.meta.url)", "new URL('./features/', import.meta.url)"],
|
|
192
|
+
["language: '@asas/mithaq',", "language: '../../../mithaq.ts',"],
|
|
193
|
+
["features: '../features/',", "features: 'src/features/',"],
|
|
194
|
+
["auth: '../features/auth/',", "auth: 'src/features/auth/',"],
|
|
195
|
+
["providers: '@asas/mithaq/auth',", "providers: '../../../mithaq/auth/index.ts',"],
|
|
196
|
+
// The dashboard's and the portal's, which the tenant app serves.
|
|
197
|
+
["'../../apps/asas-tanstack/src/routes/(screens)/'", "'src/routes/(screens)/'", 2],
|
|
198
|
+
].reduce((one, [from, to, count]) => once(one, from, to, count), text),
|
|
199
|
+
)
|
|
200
|
+
for (const entry of readdirSync(at('packages/asas'))) if (!['package.json', 'components.json', 'src'].includes(entry)) renameSync(at('packages/asas', entry), at(entry))
|
|
201
|
+
renameSync(at('packages/asas/src'), at('src'))
|
|
202
|
+
// The asas's `src/mithaq.ts` is where the mithaq command finds the language; the language's own takes its place.
|
|
203
|
+
rmSync(at('src/mithaq.ts'))
|
|
204
|
+
for (const entry of readdirSync(at('packages/mithaq/src'))) renameSync(at('packages/mithaq/src', entry), at('src', entry))
|
|
205
|
+
for (const one of features) {
|
|
206
|
+
mkdirSync(at('src/features', one), { recursive: true })
|
|
207
|
+
for (const entry of readdirSync(at('packages/features', one))) if (entry !== 'package.json') renameSync(at('packages/features', one, entry), at('src/features', one, entry))
|
|
208
|
+
}
|
|
209
|
+
for (const entry of readdirSync(at('apps/asas-tanstack/src'))) renameSync(at('apps/asas-tanstack/src', entry), at('src', entry))
|
|
210
|
+
for (const entry of ['public', 'vite.config.ts', 'tsconfig.json', 'components.json']) renameSync(at('apps/asas-tanstack', entry), at(entry))
|
|
211
|
+
rmSync(at('packages'), { recursive: true })
|
|
212
|
+
rmSync(at('apps'), { recursive: true })
|
|
213
|
+
const own = (dependencies) => Object.fromEntries(Object.entries(dependencies).filter(([name]) => !name.startsWith('@asas/') && name !== 'asas'))
|
|
214
|
+
const lockCheck = (script) => script.replace('(cd ../../packages/asas && mithaq lock --check)', 'mithaq lock --check')
|
|
215
|
+
writeJson(at('package.json'), {
|
|
216
|
+
name: 'asas',
|
|
217
|
+
private: true,
|
|
218
|
+
type: 'module',
|
|
219
|
+
engines: root.engines,
|
|
220
|
+
scripts: { ...Object.fromEntries(Object.entries(app.scripts).map(([name, script]) => [name, lockCheck(script)])), ...asas.scripts },
|
|
221
|
+
exports: asas.exports,
|
|
222
|
+
dependencies: own(Object.assign({}, language.dependencies, ...ofFeatures.map((one) => one.dependencies), asas.dependencies, app.dependencies)),
|
|
223
|
+
devDependencies: own(Object.assign({}, language.devDependencies, ...ofFeatures.map((one) => one.devDependencies), asas.devDependencies, app.devDependencies, root.devDependencies)),
|
|
224
|
+
})
|
|
225
|
+
const exportsOf = Object.fromEntries(ofFeatures.map((one) => [one.name.slice('@asas/'.length), one.exports]))
|
|
226
|
+
// A feature is reached through what its `exports` say, as it was while it was a package.
|
|
227
|
+
const targetOf = (name, sub) => {
|
|
228
|
+
if (name === 'mithaq') return sub === '' ? 'src/mithaq.ts' : sub === '/auth' ? 'src/mithaq/auth/index.ts' : ['/auth/backoffice-roles', '/auth/organizations'].includes(sub) ? `src/mithaq${sub}.ts` : `src${sub}`
|
|
229
|
+
if (sub === '') return `src/features/${name}/index.ts`
|
|
230
|
+
if (!exportsPath(exportsOf[name], sub)) throw new Error(`@asas/${name}${sub} reaches past the feature's exports.`)
|
|
231
|
+
return `src/features/${name}${sub}`
|
|
232
|
+
}
|
|
233
|
+
for (const file of textsUnder(at('src')).filter((one) => /\.tsx?$/.test(one))) {
|
|
234
|
+
edit(file, (text) =>
|
|
235
|
+
text.replaceAll(/(['"])@asas\/([a-z0-9._-]+)((?:\/[^'"]*)?)\1/g, (_, quote, name, sub) => {
|
|
236
|
+
const path = relative(dirname(file), at(targetOf(name, sub))).replaceAll('\\', '/')
|
|
237
|
+
return `${quote}${path.startsWith('.') ? path : `./${path}`}${quote}`
|
|
238
|
+
}),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
// A screen draws the language's and each feature's files by package; in one package they are all its own, read
|
|
242
|
+
// off Vite's root as `mithaq generate screens` writes them for an app that is the project.
|
|
243
|
+
for (const screen of readdirSync(at('src/routes/(screens)')).filter((one) => existsSync(at('src/routes/(screens)', one, 'screen.json')))) {
|
|
244
|
+
const folder = at('src/routes/(screens)', screen)
|
|
245
|
+
edit(join(folder, 'screen.json'), (text) => text.replaceAll('"@asas/mithaq:./', '"./').replaceAll(/"@asas\/([a-z0-9._-]+):\.\//g, '"./src/features/$1/'))
|
|
246
|
+
const referenced = [...new Set([...readFileSync(join(folder, 'screen.json'), 'utf8').matchAll(/"(\.\/[^"#]+)#[^"]*"/g)].map(([, file]) => file))].sort()
|
|
247
|
+
writeFileSync(join(folder, 'files.ts'), `// Written by \`mithaq generate screens\`. Do not edit.\n/// <reference types="vite/client" />\n// Every file the \`${screen}\` screen's references name, each loaded when a page draws it.\nexport const files = import.meta.glob([${referenced.map((file) => `'${file}'`).join(', ')}], { base: '/' })\n`)
|
|
248
|
+
}
|
|
249
|
+
edit(at('src/styles.css'), (text) => once(text, /\/\*[^*]*\*\/\n@source '[^']*';\n/, "/* The project's own shells, pages and components, which the screens load\n (`src/ui/app-shell.tsx`). */\n@source './';\n"))
|
|
250
|
+
edit(at('tsconfig.json'), (text) => once(text, /, "\.\.\/\.\.\/packages\/[^\]]*\]/, ']'))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Leaves `project` holding the chosen add-ons alone: each other add-on's files, regions and package go, and so do the
|
|
255
|
+
* files that stand in for a chosen one. The template's migrations build every table, so a project short of any add-on
|
|
256
|
+
* with tables starts with none, and its first `mithaq migrate` writes them for the tables it has.
|
|
257
|
+
*/
|
|
258
|
+
const keepOnly = ({ project, chosen }) => {
|
|
259
|
+
for (const [name, { files, without }] of Object.entries(addOns)) {
|
|
260
|
+
for (const file of chosen.includes(name) ? without : files) rmSync(join(project, file), { recursive: true })
|
|
261
|
+
}
|
|
262
|
+
for (const file of textsUnder(project).filter((one) => /\.tsx?$/.test(one))) {
|
|
263
|
+
const text = readFileSync(file, 'utf8')
|
|
264
|
+
if (text.includes('add-on: ')) writeFileSync(file, keepAddOns(text, chosen))
|
|
265
|
+
}
|
|
266
|
+
for (const file of textsUnder(project).filter((one) => basename(one) === 'package.json')) {
|
|
267
|
+
const manifest = readJson(file)
|
|
268
|
+
for (const [name, { packages }] of Object.entries(addOns).filter(([one]) => !chosen.includes(one))) {
|
|
269
|
+
for (const dependencies of [manifest.dependencies, manifest.devDependencies]) {
|
|
270
|
+
for (const dependency of Object.keys(dependencies ?? {})) {
|
|
271
|
+
if (dependency === `@asas/${name}` || packages.some((one) => (one.endsWith('/') ? dependency.startsWith(one) : dependency === one))) delete dependencies[dependency]
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
writeJson(file, manifest)
|
|
276
|
+
}
|
|
277
|
+
const copy = join(project, 'packages/asas/src/messages/mithaq.json')
|
|
278
|
+
const shared = readJson(copy)
|
|
279
|
+
for (const [name, { copy: keys }] of Object.entries(addOns)) if (!chosen.includes(name)) for (const key of keys) {
|
|
280
|
+
const parts = key.split('.')
|
|
281
|
+
const last = parts.pop()
|
|
282
|
+
delete parts.reduce((branch, part) => branch?.[part], shared)?.[last]
|
|
283
|
+
}
|
|
284
|
+
writeJson(copy, shared)
|
|
285
|
+
if (Object.entries(addOns).every(([name, { tables }]) => !tables || chosen.includes(name))) return
|
|
286
|
+
const migrations = join(project, 'packages/asas/src/migrations')
|
|
287
|
+
for (const entry of readdirSync(migrations)) if (entry !== 'drizzle.config.ts') rmSync(join(migrations, entry), { recursive: true })
|
|
288
|
+
// Each answer names a change between two of the migrations just removed.
|
|
289
|
+
writeFileSync(join(project, 'packages/asas/mithaq.hints.json'), '[]\n')
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The organization mode and who may sign up, as `organizations.ts` writes them: one organization,
|
|
294
|
+
* shown by name, by invitation unless asked, and many founding one by every sign-up. Open joiners of
|
|
295
|
+
* the courses example are students; without the roles add-on nobody is given a role on joining. Every
|
|
296
|
+
* open sign-up proves its address.
|
|
297
|
+
*/
|
|
298
|
+
export function organizationSettings({ organizations = 'one', showOrganization = true, signUp, chosen, organizationName }) {
|
|
299
|
+
if (organizations === 'none') throw new Error('--organizations is one or many, not "none": one organization never shown is --organizations one --show-organization no.')
|
|
300
|
+
if (!['one', 'many'].includes(organizations)) throw new Error(`--organizations is one or many, not "${organizations}".`)
|
|
301
|
+
if (signUp !== undefined && !['invite', 'open'].includes(signUp)) throw new Error(`--sign-up is invite or open, not "${signUp}".`)
|
|
302
|
+
const many = organizations === 'many'
|
|
303
|
+
if (many && signUp === 'invite') throw new Error('many organizations take --sign-up open: each sign-up founds its own.')
|
|
304
|
+
if (many && !showOrganization) throw new Error('many organizations are shown by name: a person who belongs to several switches between them.')
|
|
305
|
+
// One line of 1 to 100 characters, as the auth setup keeps an organization's name, so the project loads.
|
|
306
|
+
for (const one of Object.values(organizationName ?? {})) {
|
|
307
|
+
if (typeof one !== 'string' || !one.trim() || one.length > 100 || /[\p{Cc}\p{Zl}\p{Zp}\u202a-\u202e\u2066-\u2069]/u.test(one)) throw new Error('--organization-name is one line of 1 to 100 characters.')
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
organizations,
|
|
311
|
+
showOrganization,
|
|
312
|
+
signUp: signUp ?? (many ? 'open' : 'invite'),
|
|
313
|
+
joinRole: !many && chosen.includes('courses') ? 'student' : null,
|
|
314
|
+
verifyEmail: true,
|
|
315
|
+
allowedDomains: [],
|
|
316
|
+
// The Arabic falls back to the English, so the language never reads an organization with no name.
|
|
317
|
+
...(organizationName?.en && { organizationName: { en: organizationName.en.trim(), ar: organizationName.ar?.trim() || organizationName.en.trim() } }),
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Writes `settings` over the values `organizations.ts` holds, each line the template holds once. A name is written as
|
|
323
|
+
* JSON, whose double quotes leave an apostrophe in it as written.
|
|
324
|
+
*/
|
|
325
|
+
const writeOrganizations = (file, settings) =>
|
|
326
|
+
edit(file, (text) => Object.entries(settings).reduce((one, [key, value]) => once(one, new RegExp(`(?<=^export const ${key}: [^=]+ = ).+$`, 'm'), key === 'organizationName' ? JSON.stringify(value) : JSON.stringify(value).replaceAll('"', "'")), text))
|
|
327
|
+
|
|
42
328
|
/**
|
|
43
329
|
* Writes the project `name` under `cwd` from `template`, and answers its
|
|
44
|
-
* folder. `version` is the `@drhomet/mithaq`
|
|
45
|
-
*
|
|
46
|
-
* upgrade is a change the project makes; `mithaq`
|
|
47
|
-
*
|
|
330
|
+
* folder. `version` is the `@drhomet/mithaq` and `@drhomet/mithaq-admin` the
|
|
331
|
+
* project installs, pinned exactly so the two cannot drift apart and an
|
|
332
|
+
* upgrade is a change the project makes; `mithaq` and `mithaqAdmin` replace the
|
|
333
|
+
* dependencies themselves, which only the release check does. `layout` is `monorepo`, the
|
|
334
|
+
* language, each feature and the asas as packages, or `single`, one package with the tenant app and no
|
|
335
|
+
* backoffice console. `addOns` names the add-ons the project holds beside auth and the tags example, with
|
|
336
|
+
* every one they need; none is the minimal project.
|
|
48
337
|
*/
|
|
49
|
-
export function create({ name, cwd, template, version, mithaq = `npm:@drhomet/mithaq@${version}` }) {
|
|
50
|
-
// A package name npm accepts, so it is also one folder, inside `cwd
|
|
338
|
+
export function create({ name, cwd, template, version, mithaq = `npm:@drhomet/mithaq@${version}`, mithaqAdmin = `npm:@drhomet/mithaq-admin@${version}`, layout = 'monorepo', addOns: asked = [], organizations, showOrganization, signUp, organizationName }) {
|
|
339
|
+
// A package name npm accepts, so it is also one folder, inside `cwd`, and a scope.
|
|
51
340
|
if (!/^[a-z0-9][a-z0-9._-]*$/.test(name) || name.length > 214) {
|
|
52
341
|
throw new Error(`"${name}" is not a valid project name: use lowercase letters, digits, "-", "." and "_", starting with a letter or digit.`)
|
|
53
342
|
}
|
|
343
|
+
if (!['monorepo', 'single'].includes(layout)) throw new Error(`The layout is monorepo or single, not "${layout}".`)
|
|
344
|
+
const { chosen } = resolveAddOns(asked)
|
|
345
|
+
const settings = organizationSettings({ organizations, showOrganization, signUp, chosen, organizationName })
|
|
346
|
+
const backoffice = chosen.includes('backoffice')
|
|
347
|
+
if (layout === 'single' && backoffice) throw new Error('A single package serves the tenant app alone. Choose the monorepo layout to include the backoffice console.')
|
|
54
348
|
const project = join(cwd, name)
|
|
55
349
|
if (existsSync(project) && readdirSync(project).length > 0) {
|
|
56
350
|
throw new Error(`${project} already exists and is not empty. Pick another name.`)
|
|
57
351
|
}
|
|
58
352
|
cpSync(template, project, { recursive: true })
|
|
353
|
+
// The template's packages sit in `templates/` in the mithaq repository, beside the framework's own `packages/`; a
|
|
354
|
+
// project holds them in `packages/`, and every relative path an app names into them follows.
|
|
355
|
+
renameSync(join(project, 'templates'), join(project, 'packages'))
|
|
356
|
+
for (const file of textsUnder(project)) edit(file, (text) => text.replaceAll(/((?:\.\.\/)+)templates(?=[/'"])/g, '$1packages'))
|
|
357
|
+
// A snippet's marker names the repository file the docs test checks it against, a path no project holds.
|
|
358
|
+
for (const file of textsUnder(project).filter((one) => one.endsWith('.md'))) edit(file, (text) => text.replaceAll(/^[ \t]*<!-- from: \S+ -->\n/gm, ''))
|
|
59
359
|
const files = readdirSync(project, { recursive: true })
|
|
60
360
|
// npm drops every `.gitignore` from a published package, so the stage keeps
|
|
61
361
|
// each as `gitignore`.
|
|
@@ -64,59 +364,153 @@ export function create({ name, cwd, template, version, mithaq = `npm:@drhomet/mi
|
|
|
64
364
|
}
|
|
65
365
|
for (const file of files.filter((file) => basename(file) === 'package.json')) {
|
|
66
366
|
const packageJson = readJson(join(project, file))
|
|
67
|
-
updatePackageJSONDependencies({ mithaq, packageJson })
|
|
367
|
+
updatePackageJSONDependencies({ mithaq, mithaqAdmin, packageJson })
|
|
68
368
|
writeJson(join(project, file), file === 'package.json' ? { name, ...packageJson } : packageJson)
|
|
69
369
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
for
|
|
73
|
-
|
|
74
|
-
writeJson(join(project, file), { ...components, registries: { ...components.registries, '@mithaq': `https://cdn.jsdelivr.net/npm/@drhomet/mithaq@${version}/r/{name}.json` } })
|
|
75
|
-
}
|
|
76
|
-
// One `.env` for the template's commands and one for each app's dev server,
|
|
77
|
-
// the same secrets in each: the tenant app and the platform console sign
|
|
370
|
+
keepOnly({ project, chosen })
|
|
371
|
+
writeOrganizations(join(project, 'packages/mithaq/src/mithaq/auth/organizations.ts'), settings)
|
|
372
|
+
// One `.env` for the asas's commands and one for each app's dev server,
|
|
373
|
+
// the same secrets in each: the tenant app and the backoffice console sign
|
|
78
374
|
// and read each other's sessions under one `BETTER_AUTH_SECRET`.
|
|
79
|
-
const env = readFileSync(join(project, '
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
375
|
+
const env = readFileSync(join(project, 'packages/asas/.env.example'), 'utf8').replace(/^([A-Z_]*SECRET)=$/gm, (_, key) => `${key}=${generateSecret()}`)
|
|
376
|
+
if (layout === 'single') {
|
|
377
|
+
foldIntoOnePackage(project)
|
|
378
|
+
writeFileSync(join(project, '.env'), env, { mode: 0o600 })
|
|
379
|
+
} else {
|
|
380
|
+
for (const folder of ['packages/asas', 'apps/asas-tanstack', ...(backoffice ? ['apps/asas-backoffice'] : [])]) writeFileSync(join(project, folder, '.env'), env, { mode: 0o600 })
|
|
381
|
+
// An agent started at the root reads no file below it until it touches one, and the repository's page shows the
|
|
382
|
+
// root's README alone, so the guide, the agent files and the skills sit there, as a single package's do.
|
|
383
|
+
for (const file of ['AGENTS.md', 'CLAUDE.md', 'README.md', '.claude']) renameSync(join(project, 'packages/asas', file), join(project, file))
|
|
384
|
+
// The project's own scope, on each of its package names and every import of one.
|
|
385
|
+
for (const file of textsUnder(project)) edit(file, (text) => text.replaceAll('@asas/', `@${name}/`))
|
|
386
|
+
writeJson(join(project, 'tsconfig.json'), {
|
|
387
|
+
compilerOptions: { target: 'es2024', module: 'nodenext', moduleResolution: 'nodenext', strict: true, noUncheckedIndexedAccess: true, noEmit: true, skipLibCheck: true, erasableSyntaxOnly: true, allowImportingTsExtensions: true, types: ['node'] },
|
|
388
|
+
include: ['packages/*/src/**/*.ts', 'packages/features/**/*.ts', 'apps/*/src/**/*.ts'],
|
|
389
|
+
// Each server entry reads Vite's `import.meta.glob`, and each components map mithaq-admin's `.tsx`, which the app's own config checks.
|
|
390
|
+
exclude: ['apps/*/src/server.ts', 'apps/*/src/components.ts'],
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
// npm, yarn and Bun read `workspaces`; pnpm 10.6 and later read this file.
|
|
394
|
+
// Told nothing, pnpm installs a `"*"` workspace from the registry, where
|
|
395
|
+
// another package is named `asas`, and skips esbuild's install script, which
|
|
396
|
+
// pnpm 11 then fails on; pnpm 10 reads `onlyBuiltDependencies`, pnpm 11
|
|
397
|
+
// `allowBuilds`.
|
|
398
|
+
const { workspaces = [] } = readJson(join(project, 'package.json'))
|
|
399
|
+
const packages = workspaces.length ? `packages:\n${workspaces.map((folder) => ` - ${folder}\n`).join('')}linkWorkspacePackages: true\npreferWorkspacePackages: true\n` : ''
|
|
400
|
+
writeFileSync(join(project, 'pnpm-workspace.yaml'), `${packages}onlyBuiltDependencies:\n - esbuild\nallowBuilds:\n esbuild: true\n`)
|
|
401
|
+
// Yarn 2 and later would otherwise install Plug'n'Play, which neither Vite's
|
|
402
|
+
// externals nor Node's type stripping read.
|
|
403
|
+
writeFileSync(join(project, '.yarnrc.yml'), 'nodeLinker: node-modules\n')
|
|
404
|
+
writeFileSync(join(project, '.gitignore'), ['node_modules', '.env', 'dist', '.storage/', '.tanstack', ''].join('\n'))
|
|
89
405
|
return project
|
|
90
406
|
}
|
|
91
407
|
|
|
408
|
+
const managers = 'A project installs with npm, pnpm 10.6 or later, Yarn 1 or Bun 1.3. Yarn 2 and later install a copy of mithaq for each workspace of a monorepo, which the build refuses, so a monorepo does not take them yet.'
|
|
409
|
+
|
|
410
|
+
const help = `Usage: create-mithaq [name] [options]
|
|
411
|
+
|
|
412
|
+
--layout monorepo the language, each feature, the asas and each app as workspace packages (the default)
|
|
413
|
+
--layout single one package holding the language, the features, the asas and the tenant app
|
|
414
|
+
--with <a,b> the add-ons to hold beside auth and the tags example, with every one they need
|
|
415
|
+
--full every add-on: the whole worked example (a single package leaves out backoffice)
|
|
416
|
+
--organizations one|many
|
|
417
|
+
one organization, made on /auth/first-run (the default), or many, each sign-up
|
|
418
|
+
founding its own
|
|
419
|
+
--show-organization yes|no
|
|
420
|
+
with one organization: whether a page names it (the default), or the app never
|
|
421
|
+
shows one and its members are its users
|
|
422
|
+
--sign-up invite|open
|
|
423
|
+
with one organization: by invitation alone (the default), or anyone, who joins
|
|
424
|
+
once their address is proved (production needs a mailer); many takes open
|
|
425
|
+
--organization-name <name>
|
|
426
|
+
with one organization: its name, which lives in code; first-run asks none
|
|
427
|
+
--organization-name-ar <name>
|
|
428
|
+
its name in Arabic, the English one where not given
|
|
429
|
+
--help print this
|
|
430
|
+
|
|
431
|
+
With neither --with nor --full the project is minimal: auth and the tags example. The add-ons:
|
|
432
|
+
|
|
433
|
+
${Object.entries(addOns).map(([name, { needs }]) => ` ${name}${needs.length ? ` (needs ${needs.join(', ')})` : ''}`).join('\n')}
|
|
434
|
+
|
|
435
|
+
Asked when not given, unless the input is not a terminal, where the defaults stand.
|
|
436
|
+
|
|
437
|
+
${managers}`
|
|
438
|
+
|
|
439
|
+
/** The `mithaq` bin an install laid down for `folder`, in the nearest `node_modules/.bin` above it. */
|
|
440
|
+
const binAbove = (folder) => {
|
|
441
|
+
const at = join(folder, 'node_modules/.bin/mithaq')
|
|
442
|
+
if (existsSync(at)) return at
|
|
443
|
+
return dirname(folder) === folder ? undefined : binAbove(dirname(folder))
|
|
444
|
+
}
|
|
445
|
+
|
|
92
446
|
const main = async () => {
|
|
93
|
-
const { values, positionals } = parseArgs({
|
|
447
|
+
const { values, positionals } = parseArgs({
|
|
448
|
+
allowPositionals: true,
|
|
449
|
+
options: { mithaq: { type: 'string' }, 'mithaq-admin': { type: 'string' }, layout: { type: 'string' }, with: { type: 'string' }, full: { type: 'boolean' }, organizations: { type: 'string' }, 'show-organization': { type: 'string' }, 'sign-up': { type: 'string' }, 'organization-name': { type: 'string' }, 'organization-name-ar': { type: 'string' }, help: { type: 'boolean' } },
|
|
450
|
+
})
|
|
451
|
+
if (values.help) return console.log(help)
|
|
452
|
+
if (values.full && values.with !== undefined) throw new Error('Pass --full or --with, not both: --full is every add-on.')
|
|
94
453
|
let [name] = positionals
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
454
|
+
let { layout } = values
|
|
455
|
+
let picked = values.with?.split(',').map((one) => one.trim()).filter(Boolean)
|
|
456
|
+
let { organizations, 'show-organization': shown, 'sign-up': signUp, 'organization-name': named, 'organization-name-ar': namedInArabic } = values
|
|
457
|
+
const prompt = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined
|
|
458
|
+
try {
|
|
459
|
+
if (!name) name = (await prompt?.question('Project name: '))?.trim() ?? ''
|
|
460
|
+
layout ??= (await prompt?.question('Monorepo, or a single package? (monorepo/single) [monorepo]: '))?.trim().toLowerCase() || 'monorepo'
|
|
461
|
+
if (!values.full && !picked && prompt) {
|
|
462
|
+
const answer = (await prompt.question('minimal: auth + tags / full: every add-on / pick: choose add-ons (minimal/full/pick) [minimal]: ')).trim().toLowerCase() || 'minimal'
|
|
463
|
+
if (answer === 'full') values.full = true
|
|
464
|
+
else if (answer === 'pick') picked = (await prompt.question(`Add-ons, comma-separated (${Object.keys(addOns).join(', ')}): `)).split(',').map((one) => one.trim()).filter(Boolean)
|
|
465
|
+
else if (answer !== 'minimal') throw new Error(`The answer is minimal, full or pick, not "${answer}".`)
|
|
466
|
+
}
|
|
467
|
+
if (!organizations && prompt) organizations = (await prompt.question('One organization, or many? (one/many) [one]: ')).trim().toLowerCase() || 'one'
|
|
468
|
+
if (!shown && prompt && organizations !== 'many') shown = (await prompt.question('Show the organization\'s name on its pages? (yes/no) [yes]: ')).trim().toLowerCase() || 'yes'
|
|
469
|
+
if (!signUp && prompt && organizations !== 'many') signUp = (await prompt.question('Who signs up: the invited, or anyone? (invite/open) [invite]: ')).trim().toLowerCase() || 'invite'
|
|
470
|
+
if (!named && prompt && organizations !== 'many') named = (await prompt.question('The organization\'s name [My organization]: ')).trim() || undefined
|
|
471
|
+
if (named && !namedInArabic && prompt) namedInArabic = (await prompt.question('Its name in Arabic [the same]: ')).trim() || undefined
|
|
472
|
+
} finally {
|
|
473
|
+
prompt?.close()
|
|
99
474
|
}
|
|
475
|
+
if (shown !== undefined && !['yes', 'no'].includes(shown)) throw new Error(`--show-organization is yes or no, not "${shown}".`)
|
|
476
|
+
const showOrganization = shown !== 'no'
|
|
477
|
+
// A single package cannot hold the backoffice console, so its whole example is every other add-on.
|
|
478
|
+
const asked = values.full ? Object.keys(addOns).filter((one) => layout !== 'single' || one !== 'backoffice') : (picked ?? [])
|
|
479
|
+
const { chosen, added } = resolveAddOns(asked)
|
|
480
|
+
if (added.length) console.log(`Added ${added.join(', ')}, which ${asked.filter((one) => addOns[one].needs.length).join(', ')} need${added.length === 1 ? 's' : ''}.`)
|
|
100
481
|
const { version } = readJson(new URL('package.json', import.meta.url))
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
482
|
+
const organizationName = named ? { en: named, ...(namedInArabic && { ar: namedInArabic }) } : undefined
|
|
483
|
+
const project = create({ name, cwd: process.cwd(), template: fileURLToPath(new URL('template/', import.meta.url)), version, mithaq: values.mithaq, mithaqAdmin: values['mithaq-admin'], layout, addOns: chosen, organizations, showOrganization, signUp, organizationName })
|
|
484
|
+
const settings = organizationSettings({ organizations, showOrganization, signUp, chosen, organizationName })
|
|
485
|
+
console.log(`Created ${project}${chosen.length ? `, with ${chosen.join(', ')}` : ', minimal'}.`)
|
|
486
|
+
const manager = managerOf(process.env.npm_config_user_agent)
|
|
487
|
+
const installed = manager !== undefined && spawnSync(manager, ['install'], { cwd: project, stdio: 'inherit', shell: process.platform === 'win32' }).status === 0
|
|
488
|
+
const run = manager ?? 'npm'
|
|
489
|
+
if (!installed) console.warn(`\nNot installed. Run ${run} install in ${project}. ${managers}`)
|
|
490
|
+
const single = layout === 'single'
|
|
491
|
+
const asas = single ? project : join(project, 'packages/asas')
|
|
492
|
+
const backoffice = chosen.includes('backoffice')
|
|
493
|
+
// Each screen's files were generated for every kayan of the template, so a project short of an add-on with tables
|
|
494
|
+
// writes its own once the install has laid the bin down.
|
|
495
|
+
const short = Object.entries(addOns).some(([one, { tables }]) => tables && !chosen.includes(one))
|
|
496
|
+
// With no organization shown, the Members page is named Users, which the template's screens do not say.
|
|
497
|
+
const regenerate = short || !settings.showOrganization
|
|
498
|
+
const bin = installed && regenerate ? binAbove(asas) : undefined
|
|
499
|
+
const generated = bin !== undefined && spawnSync(bin, ['generate', 'screens'], { cwd: asas, stdio: 'inherit', shell: process.platform === 'win32' }).status === 0
|
|
500
|
+
if (regenerate && !generated) console.warn(`\nThe screens are not written for this project yet. Run mithaq generate screens in ${asas}, after the install.`)
|
|
501
|
+
const language = single ? '' : 'packages/asas/'
|
|
502
|
+
if (single) console.log(`\nThe package is named asas, not ${name}: the screen host imports the language as asas, and a package reaches itself by its own name.`)
|
|
108
503
|
console.log(`
|
|
109
|
-
Next, with PostgreSQL 15 or later serving the database DATABASE_URL names in
|
|
504
|
+
Next, with PostgreSQL 15 or later serving the database DATABASE_URL names in ${language}.env:
|
|
505
|
+
|
|
506
|
+
cd ${name}${installed ? '' : `\n ${run} install`}${single ? '' : '\n cd packages/asas'}
|
|
507
|
+
# push the schema, make the dev admin and fill its organization, development only
|
|
508
|
+
${run} run dev:setup${run === 'npm' ? ' --' : ''} --seed${single ? '' : '\n cd ../../apps/asas-tanstack'}
|
|
509
|
+
${run} run dev # the tenant app, http://localhost:5173${backoffice ? `\n # in a second terminal, from apps/asas-backoffice: ${run} run dev # the backoffice console, http://localhost:5174` : ''}
|
|
110
510
|
|
|
111
|
-
|
|
112
|
-
cd templates/asas
|
|
113
|
-
# push the schema once, development only
|
|
114
|
-
bun -e "const { asas } = await import('./src/asas.ts'); await asas.connect(); await asas.destroy()"
|
|
115
|
-
cd ../..
|
|
116
|
-
bun run --cwd apps/asas-tanstack dev # the tenant app, http://localhost:5173
|
|
117
|
-
bun run --cwd apps/asas-platform dev # in a second terminal: the platform console, http://localhost:5174
|
|
511
|
+
Sign in${backoffice ? ' to both apps' : ''} with DEV_ADMIN_EMAIL and DEV_ADMIN_PASSWORD from ${language}.env. A deploy is set up by hand instead: ${settings.organizations === 'many' ? 'sign up in the tenant app, prove your address by the link it mails, sign in and create an organization.' : `open /auth/first-run and make the app's owner${settings.showOrganization ? ' and its organization' : ''} with the SETUP_CODE it was started with.`} A production start needs a mailer: RESEND_API_KEY and MAIL_FROM. ${run} run seed in ${single ? 'the project' : 'packages/asas'} fills an organization with sample rows again: the first user's first, or the ones SEED_EMAIL and SEED_ORGANIZATION name.${short ? `
|
|
118
512
|
|
|
119
|
-
|
|
513
|
+
The project holds no migration yet. Before it runs in production, write its first against an empty database with mithaq migrate --name init in ${single ? 'the project' : 'packages/asas'} (README.md, "Running in production").` : ''}`)
|
|
120
514
|
}
|
|
121
515
|
|
|
122
516
|
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|