From df5449b1e1141279fe771b6f106f8222a139759f Mon Sep 17 00:00:00 2001 From: Melvin Chia Date: Sun, 4 Jan 2026 00:40:46 +0800 Subject: [PATCH] feat: very crude implementation --- .gitignore | 9 + .npmrc | 2 + bunfig.toml | 3 + client/src/routes/index.tsx | 70 +---- docker-compose.yaml | 1 + docs/index.html | 15 - package.json | 16 +- server/src/core/routes/index.ts | 2 +- tools/forgeCLI/package.json | 8 +- .../migration-generation/migration-file.ts | 4 +- .../db/handlers/generateMigrationsHandler.ts | 26 +- .../src/commands/db/utils/pocketbase-utils.ts | 6 +- .../src/commands/locales/constants/index.tsx | 8 + .../locales/functions/ensureLocaleNotInUse.ts | 27 ++ .../locales/functions/getInstalledLocales.ts | 44 +++ .../locales/functions/getLocalesMeta.ts | 35 +++ .../locales/functions/getPackagesToCheck.ts | 33 ++ .../commands/locales/functions/getUpgrades.ts | 63 ++++ .../functions/installAndMoveLocales.ts | 30 ++ .../locales/functions/setFirstLangInDB.ts | 23 ++ .../functions/validateLocaleStructure.ts | 65 ++++ .../locales/handlers/addLocaleHandler.ts | 191 ------------ .../locales/handlers/install-locale.ts | 38 +++ .../commands/locales/handlers/list-locales.ts | 26 ++ .../locales/handlers/listLocalesHandler.ts | 43 --- .../locales/handlers/publish-locale.ts | 48 +++ .../locales/handlers/removeLocaleHandler.ts | 191 ------------ .../locales/handlers/uninstall-locale.ts | 35 +++ .../locales/handlers/upgrade-locale.ts | 39 +++ tools/forgeCLI/src/commands/locales/index.ts | 52 +++- tools/forgeCLI/src/commands/locales/utils.ts | 52 ---- .../modules/functions/{git => }/git-status.ts | 0 .../modules/functions/git/clone-repository.ts | 32 -- .../modules/functions/git/git-submodule.ts | 108 ------- .../modules/functions/git/github-cli.ts | 88 ------ .../commands/modules/functions/git/index.ts | 17 - .../src/commands/modules/functions/index.ts | 14 +- .../install-dependencies.ts | 0 .../modules/functions/migrations/index.ts | 5 - .../functions/module-lifecycle/index.ts | 13 - .../functions/module-lifecycle/move-module.ts | 81 ----- .../process-server-injection.ts | 43 --- .../remove-server-references.ts | 24 -- .../validate-module-structure.ts | 38 --- .../{migrations => }/module-migrations.ts | 0 .../functions/registry/client-registry.ts | 41 +++ .../modules/functions/registry/generator.ts | 150 +++++++++ .../modules/functions/registry/index.ts | 11 + .../functions/registry/module-utils.ts | 62 ++++ .../functions/registry/schema-registry.ts | 25 ++ .../functions/registry/server-registry.ts | 31 ++ .../commands/modules/handlers/add-module.ts | 68 ---- .../modules/handlers/create-module.ts | 11 +- .../modules/handlers/install-module.ts | 155 ++++++++++ .../commands/modules/handlers/login-module.ts | 38 +++ .../modules/handlers/migrate-module.ts | 283 +++++++++++++++++ .../modules/handlers/publish-module.ts | 285 ++++++++++++++++- .../modules/handlers/remove-module.ts | 47 --- .../modules/handlers/uninstall-module.ts | 125 ++++++++ .../modules/handlers/update-module.ts | 90 ------ .../modules/handlers/upgrade-module.ts | 292 ++++++++++++++++++ tools/forgeCLI/src/commands/modules/index.ts | 73 +++-- .../src/commands/modules/utils/ast-utils.ts | 16 - .../src/commands/modules/utils/constants.ts | 22 -- .../commands/modules/utils/route-injection.ts | 177 ----------- .../modules/utils/schema-injection.ts | 206 ------------ .../src/commands/modules/utils/validation.ts | 39 --- .../src/templates/bare-bones/_tsconfig.json | 18 ++ .../bare-bones/client/_tsconfig.json | 25 +- .../bare-bones/client/{src => }/index.tsx | 0 .../client/{src => }/utils/forgeAPI.ts | 0 .../bare-bones/client/{src => }/vite-env.d.ts | 0 .../bare-bones/{client => }/manifest.ts | 0 .../src/templates/bare-bones/package.json | 18 +- .../src/templates/client-only/_tsconfig.json | 18 ++ .../client-only/client/_tsconfig.json | 25 +- .../client-only/client/{src => }/index.tsx | 0 .../client/{src => }/vite-env.d.ts | 0 .../client-only/{client => }/manifest.ts | 0 .../src/templates/client-only/package.json | 58 ++-- .../forgeCLI/src/templates/widget/.gitignore | 19 -- .../templates/widget/client/_tsconfig.json | 36 --- .../client/src/widgets/ExampleWidget.tsx | 24 -- .../src/templates/widget/locales/en.json | 8 - .../src/templates/widget/locales/ms.json | 8 - .../src/templates/widget/locales/zh-CN.json | 8 - .../src/templates/widget/locales/zh-TW.json | 8 - .../src/templates/widget/package.json | 29 -- .../src/templates/with-crud/_tsconfig.json | 18 ++ .../templates/with-crud/client/_tsconfig.json | 25 +- .../client/{src => }/components/EntryItem.tsx | 0 .../{src => }/components/ModifyEntryModal.tsx | 0 .../with-crud/client/{src => }/index.tsx | 0 .../client/{src => }/utils/forgeAPI.ts | 0 .../src => with-crud/client}/vite-env.d.ts | 0 .../with-crud/{client => }/manifest.ts | 0 .../src/templates/with-crud/package.json | 60 ++-- .../src/templates/with-routes/_tsconfig.json | 18 ++ .../with-routes/client/_tsconfig.json | 25 +- .../{src => }/pages/EntryDetails/index.tsx | 0 .../{src => }/pages/EntryList/index.tsx | 0 .../with-routes/client/src/vite-env.d.ts | 1 - .../client/{src => }/utils/forgeAPI.ts | 0 .../src => with-routes/client}/vite-env.d.ts | 0 .../with-routes/{client => }/manifest.ts | 0 .../src/templates/with-routes/package.json | 60 ++-- tools/forgeCLI/src/utils/github-cli.ts | 84 +++++ tools/forgeCLI/src/utils/package.ts | 82 +++++ tools/forgeCLI/src/utils/registry.ts | 79 +++++ 109 files changed, 2657 insertions(+), 2012 deletions(-) create mode 100644 .npmrc create mode 100644 bunfig.toml create mode 100644 tools/forgeCLI/src/commands/locales/constants/index.tsx create mode 100644 tools/forgeCLI/src/commands/locales/functions/ensureLocaleNotInUse.ts create mode 100644 tools/forgeCLI/src/commands/locales/functions/getInstalledLocales.ts create mode 100644 tools/forgeCLI/src/commands/locales/functions/getLocalesMeta.ts create mode 100644 tools/forgeCLI/src/commands/locales/functions/getPackagesToCheck.ts create mode 100644 tools/forgeCLI/src/commands/locales/functions/getUpgrades.ts create mode 100644 tools/forgeCLI/src/commands/locales/functions/installAndMoveLocales.ts create mode 100644 tools/forgeCLI/src/commands/locales/functions/setFirstLangInDB.ts create mode 100644 tools/forgeCLI/src/commands/locales/functions/validateLocaleStructure.ts delete mode 100644 tools/forgeCLI/src/commands/locales/handlers/addLocaleHandler.ts create mode 100644 tools/forgeCLI/src/commands/locales/handlers/install-locale.ts create mode 100644 tools/forgeCLI/src/commands/locales/handlers/list-locales.ts delete mode 100644 tools/forgeCLI/src/commands/locales/handlers/listLocalesHandler.ts create mode 100644 tools/forgeCLI/src/commands/locales/handlers/publish-locale.ts delete mode 100644 tools/forgeCLI/src/commands/locales/handlers/removeLocaleHandler.ts create mode 100644 tools/forgeCLI/src/commands/locales/handlers/uninstall-locale.ts create mode 100644 tools/forgeCLI/src/commands/locales/handlers/upgrade-locale.ts delete mode 100644 tools/forgeCLI/src/commands/locales/utils.ts rename tools/forgeCLI/src/commands/modules/functions/{git => }/git-status.ts (100%) delete mode 100644 tools/forgeCLI/src/commands/modules/functions/git/clone-repository.ts delete mode 100644 tools/forgeCLI/src/commands/modules/functions/git/git-submodule.ts delete mode 100644 tools/forgeCLI/src/commands/modules/functions/git/github-cli.ts delete mode 100644 tools/forgeCLI/src/commands/modules/functions/git/index.ts rename tools/forgeCLI/src/commands/modules/functions/{module-lifecycle => }/install-dependencies.ts (100%) delete mode 100644 tools/forgeCLI/src/commands/modules/functions/migrations/index.ts delete mode 100644 tools/forgeCLI/src/commands/modules/functions/module-lifecycle/index.ts delete mode 100644 tools/forgeCLI/src/commands/modules/functions/module-lifecycle/move-module.ts delete mode 100644 tools/forgeCLI/src/commands/modules/functions/module-lifecycle/process-server-injection.ts delete mode 100644 tools/forgeCLI/src/commands/modules/functions/module-lifecycle/remove-server-references.ts delete mode 100644 tools/forgeCLI/src/commands/modules/functions/module-lifecycle/validate-module-structure.ts rename tools/forgeCLI/src/commands/modules/functions/{migrations => }/module-migrations.ts (100%) create mode 100644 tools/forgeCLI/src/commands/modules/functions/registry/client-registry.ts create mode 100644 tools/forgeCLI/src/commands/modules/functions/registry/generator.ts create mode 100644 tools/forgeCLI/src/commands/modules/functions/registry/index.ts create mode 100644 tools/forgeCLI/src/commands/modules/functions/registry/module-utils.ts create mode 100644 tools/forgeCLI/src/commands/modules/functions/registry/schema-registry.ts create mode 100644 tools/forgeCLI/src/commands/modules/functions/registry/server-registry.ts delete mode 100644 tools/forgeCLI/src/commands/modules/handlers/add-module.ts create mode 100644 tools/forgeCLI/src/commands/modules/handlers/install-module.ts create mode 100644 tools/forgeCLI/src/commands/modules/handlers/login-module.ts create mode 100644 tools/forgeCLI/src/commands/modules/handlers/migrate-module.ts delete mode 100644 tools/forgeCLI/src/commands/modules/handlers/remove-module.ts create mode 100644 tools/forgeCLI/src/commands/modules/handlers/uninstall-module.ts delete mode 100644 tools/forgeCLI/src/commands/modules/handlers/update-module.ts create mode 100644 tools/forgeCLI/src/commands/modules/handlers/upgrade-module.ts delete mode 100644 tools/forgeCLI/src/commands/modules/utils/ast-utils.ts delete mode 100644 tools/forgeCLI/src/commands/modules/utils/constants.ts delete mode 100644 tools/forgeCLI/src/commands/modules/utils/route-injection.ts delete mode 100644 tools/forgeCLI/src/commands/modules/utils/schema-injection.ts delete mode 100644 tools/forgeCLI/src/commands/modules/utils/validation.ts create mode 100644 tools/forgeCLI/src/templates/bare-bones/_tsconfig.json rename tools/forgeCLI/src/templates/bare-bones/client/{src => }/index.tsx (100%) rename tools/forgeCLI/src/templates/bare-bones/client/{src => }/utils/forgeAPI.ts (100%) rename tools/forgeCLI/src/templates/bare-bones/client/{src => }/vite-env.d.ts (100%) rename tools/forgeCLI/src/templates/bare-bones/{client => }/manifest.ts (100%) create mode 100644 tools/forgeCLI/src/templates/client-only/_tsconfig.json rename tools/forgeCLI/src/templates/client-only/client/{src => }/index.tsx (100%) rename tools/forgeCLI/src/templates/client-only/client/{src => }/vite-env.d.ts (100%) rename tools/forgeCLI/src/templates/client-only/{client => }/manifest.ts (100%) delete mode 100644 tools/forgeCLI/src/templates/widget/.gitignore delete mode 100644 tools/forgeCLI/src/templates/widget/client/_tsconfig.json delete mode 100644 tools/forgeCLI/src/templates/widget/client/src/widgets/ExampleWidget.tsx delete mode 100644 tools/forgeCLI/src/templates/widget/locales/en.json delete mode 100644 tools/forgeCLI/src/templates/widget/locales/ms.json delete mode 100644 tools/forgeCLI/src/templates/widget/locales/zh-CN.json delete mode 100644 tools/forgeCLI/src/templates/widget/locales/zh-TW.json delete mode 100644 tools/forgeCLI/src/templates/widget/package.json create mode 100644 tools/forgeCLI/src/templates/with-crud/_tsconfig.json rename tools/forgeCLI/src/templates/with-crud/client/{src => }/components/EntryItem.tsx (100%) rename tools/forgeCLI/src/templates/with-crud/client/{src => }/components/ModifyEntryModal.tsx (100%) rename tools/forgeCLI/src/templates/with-crud/client/{src => }/index.tsx (100%) rename tools/forgeCLI/src/templates/with-crud/client/{src => }/utils/forgeAPI.ts (100%) rename tools/forgeCLI/src/templates/{widget/client/src => with-crud/client}/vite-env.d.ts (100%) rename tools/forgeCLI/src/templates/with-crud/{client => }/manifest.ts (100%) create mode 100644 tools/forgeCLI/src/templates/with-routes/_tsconfig.json rename tools/forgeCLI/src/templates/with-routes/client/{src => }/pages/EntryDetails/index.tsx (100%) rename tools/forgeCLI/src/templates/with-routes/client/{src => }/pages/EntryList/index.tsx (100%) delete mode 100644 tools/forgeCLI/src/templates/with-routes/client/src/vite-env.d.ts rename tools/forgeCLI/src/templates/with-routes/client/{src => }/utils/forgeAPI.ts (100%) rename tools/forgeCLI/src/templates/{with-crud/client/src => with-routes/client}/vite-env.d.ts (100%) rename tools/forgeCLI/src/templates/with-routes/{client => }/manifest.ts (100%) create mode 100644 tools/forgeCLI/src/utils/github-cli.ts create mode 100644 tools/forgeCLI/src/utils/package.ts create mode 100644 tools/forgeCLI/src/utils/registry.ts diff --git a/.gitignore b/.gitignore index ab6c8fad0..1f2fa98fc 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,9 @@ pb_*/ medium .temp server/src/core/routes/app.routes.ts +server/src/core/routes/generated-routes.ts server/src/core/schema.ts +client/src/module-registry.ts # system files Thumbs.db @@ -52,3 +54,10 @@ env/* !env/.env.example !env/.env.docker.example keys + +# module and locale packages (installed from registry) +# Note: Tailwind still scans these via @source directives in index.css +apps/* +!apps/.gitkeep +locales/* +!locales/.gitkeep diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..e9d14ac4a --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +registry=https://registry.npmjs.org/ +@lifeforge:registry=https://registry.lifeforge.dev/ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 000000000..3a82fca5d --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,3 @@ +[install] +[install.scopes] +"@lifeforge" = "https://registry.lifeforge.dev/" diff --git a/client/src/routes/index.tsx b/client/src/routes/index.tsx index 729dc3d11..9400114db 100644 --- a/client/src/routes/index.tsx +++ b/client/src/routes/index.tsx @@ -1,42 +1,25 @@ import type { ModuleCategory, ModuleConfig } from 'shared' +import { modules } from '../module-registry' + let ROUTES: ModuleCategory[] = [] -const categoryFile = import.meta.glob('../../../apps/cat.config.json', { - eager: true -}) +// Process modules from generated registry +for (const mod of modules as (ModuleConfig & { category?: string })[]) { + const category = mod.category || 'Miscellaneous' -let categoriesSeq: string[] = [] + const categoryIndex = ROUTES.findIndex(cat => cat.title === category) -if (categoryFile['../../../apps/cat.config.json']) { - categoriesSeq = ( - categoryFile['../../../apps/cat.config.json'] as { default: string[] } - ).default + if (categoryIndex > -1) { + ROUTES[categoryIndex].items.push(mod) + } else { + ROUTES.push({ + title: category, + items: [mod] + }) + } } -await Promise.all( - Object.entries( - import.meta.glob(['../apps/**/manifest.ts', '../../../apps/**/manifest.ts']) - ).map(async ([_, resolver]) => { - const mod = (await resolver()) as { - default: ModuleConfig & { category?: string } - } - - const category = mod.default.category || 'Miscellaneous' - - const categoryIndex = ROUTES.findIndex(cat => cat.title === category) - - if (categoryIndex > -1) { - ROUTES[categoryIndex].items.push(mod.default) - } else { - ROUTES.push({ - title: category, - items: [mod.default] - }) - } - }) -) - ROUTES = ROUTES.sort((a, b) => { const order = ['', 'Miscellaneous', 'Settings', 'SSO', ''] @@ -61,27 +44,6 @@ ROUTES = ROUTES.sort((a, b) => { if (bIndex >= 1) return -1 // Settings, SSO, go last } - if (categoriesSeq.length > 0) { - const aCatIndex = categoriesSeq.indexOf(a.title) - - const bCatIndex = categoriesSeq.indexOf(b.title) - - // Both found in sequence - if (aCatIndex !== -1 && bCatIndex !== -1) { - return aCatIndex - bCatIndex - } - - // Only a found in sequence - if (aCatIndex !== -1) { - return -1 - } - - // Only b found in sequence - if (bCatIndex !== -1) { - return 1 - } - } - // Default to alphabetical return a.title.localeCompare(b.title) }).map(cat => ({ @@ -89,8 +51,4 @@ ROUTES = ROUTES.sort((a, b) => { items: cat.items.sort((a, b) => a.name.localeCompare(b.name)) })) -import.meta.glob('../../../apps/**/client/index.css', { - eager: true -}) - export default ROUTES diff --git a/docker-compose.yaml b/docker-compose.yaml index 8d25e0888..a0818e71f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -52,3 +52,4 @@ services: - "80:80" depends_on: - server + diff --git a/docs/index.html b/docs/index.html index 9b5ddbab7..beded651d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -87,20 +87,5 @@
- - - - diff --git a/package.json b/package.json index 26598a3fb..930fc2c40 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "./shared", "./packages/*", "./apps/*", + "./locales/*", "./tools/*" ], "scripts": { @@ -31,19 +32,13 @@ "lifeforge" ], "devDependencies": { - "@babel/generator": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.1", "@eslint/js": "^9.26.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/lodash": "^4.17.20", "@types/prettier": "^3.0.0", - "@types/prompts": "^2.4.9", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "bun-types": "latest", - "commander": "^14.0.1", "concurrently": "^9.1.2", "eslint": "^9.26.0", "eslint-config-standard-with-typescript": "^40.0.0", @@ -55,14 +50,17 @@ "eslint-plugin-sonarjs": "^3.0.2", "eslint-plugin-unused-imports": "^4.2.0", "globals": "^16.5.0", - "inquirer-autocomplete-prompt": "^3.0.1", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.7.1", - "prompts": "^2.4.2", "typescript": "^5.9.3", "typescript-eslint": "^8.31.1" }, "dependencies": { - "dotenv": "^17.2.3" + "@lifeforge/lang-en": "workspace:*", + "@lifeforge/lang-ms": "workspace:*", + "@lifeforge/lang-tr": "workspace:*", + "@lifeforge/lang-zh-CN": "workspace:*", + "@lifeforge/lang-zh-TW": "workspace:*", + "@lifeforge/lifeforge--achievements": "workspace:*" } } diff --git a/server/src/core/routes/index.ts b/server/src/core/routes/index.ts index a294dbf89..4553661cb 100644 --- a/server/src/core/routes/index.ts +++ b/server/src/core/routes/index.ts @@ -5,8 +5,8 @@ import { forgeController, forgeRouter } from '@functions/routes' import { registerRoutes } from '@functions/routes/functions/forgeRouter' import { clientError } from '@functions/routes/utils/response' -import appRoutes from './app.routes' import coreRoutes from './core.routes' +import appRoutes from './generated-routes' const router = express.Router() diff --git a/tools/forgeCLI/package.json b/tools/forgeCLI/package.json index 4ee3193f7..2859899b9 100644 --- a/tools/forgeCLI/package.json +++ b/tools/forgeCLI/package.json @@ -7,12 +7,6 @@ "types": "tsc --noEmit" }, "dependencies": { - "@babel/generator": "^7.28.5", - "@babel/parser": "^7.28.5", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@types/babel__generator": "^7.27.0", - "@types/babel__traverse": "^7.28.0", "axios": "^1.12.2", "chalk": "^5.6.2", "commander": "^14.0.2", @@ -33,4 +27,4 @@ "@types/crypto-js": "^4.2.2", "@types/lodash": "^4.17.21" } -} +} \ No newline at end of file diff --git a/tools/forgeCLI/src/commands/db/functions/migration-generation/migration-file.ts b/tools/forgeCLI/src/commands/db/functions/migration-generation/migration-file.ts index 06504c966..a401038f2 100644 --- a/tools/forgeCLI/src/commands/db/functions/migration-generation/migration-file.ts +++ b/tools/forgeCLI/src/commands/db/functions/migration-generation/migration-file.ts @@ -141,13 +141,13 @@ export async function createStructureMigration( * Runs migrate up to apply pending migrations */ export function runMigrateUp(): void { - CLILoggingService.info('Applying pending migrations...') + CLILoggingService.debug('Applying pending migrations...') execSync(`${PB_BINARY_PATH} migrate up ${PB_KWARGS.join(' ')}`, { stdio: ['pipe', 'pipe', 'pipe'] }) - CLILoggingService.success('Migrations applied successfully') + CLILoggingService.debug('Migrations applied successfully') } /** diff --git a/tools/forgeCLI/src/commands/db/handlers/generateMigrationsHandler.ts b/tools/forgeCLI/src/commands/db/handlers/generateMigrationsHandler.ts index 3d0699bc4..8872ba7ad 100644 --- a/tools/forgeCLI/src/commands/db/handlers/generateMigrationsHandler.ts +++ b/tools/forgeCLI/src/commands/db/handlers/generateMigrationsHandler.ts @@ -60,7 +60,7 @@ export async function generateMigrationsHandler( const schemaFiles = getSchemaFiles(targetModule) - CLILoggingService.info( + CLILoggingService.debug( targetModule ? `Processing module: ${chalk.bold.blue(targetModule)}` : `Found ${chalk.bold.blue(schemaFiles.length)} schema files.` @@ -76,7 +76,7 @@ export async function generateMigrationsHandler( ) // Phase 1: Generate all skeleton migrations - CLILoggingService.step('Phase 1: Creating skeleton migrations...') + CLILoggingService.debug('Phase 1: Creating skeleton migrations...') for (const { moduleName, schema } of importedSchemas) { const result = await createSkeletonMigration(moduleName, schema) @@ -90,16 +90,16 @@ export async function generateMigrationsHandler( } } - CLILoggingService.success( + CLILoggingService.debug( `Created ${importedSchemas.length} skeleton migrations` ) // Phase 2: Run migrate up to apply skeleton migrations - CLILoggingService.step('Phase 2: Applying skeleton migrations...') + CLILoggingService.debug('Phase 2: Applying skeleton migrations...') runMigrateUp() // Phase 3: Generate all structure migrations - CLILoggingService.step('Phase 3: Creating structure migrations...') + CLILoggingService.debug('Phase 3: Creating structure migrations...') for (const { moduleName, schema } of importedSchemas) { const result = await createStructureMigration( @@ -117,16 +117,16 @@ export async function generateMigrationsHandler( } } - CLILoggingService.success( + CLILoggingService.debug( `Created ${importedSchemas.length} structure migrations` ) // Phase 4: Run migrate up to apply structure migrations - CLILoggingService.step('Phase 4: Applying structure migrations...') + CLILoggingService.debug('Phase 4: Applying structure migrations...') runMigrateUp() // Phase 5: Generate view query migrations (for modules with view collections) - CLILoggingService.step('Phase 5: Creating view query migrations...') + CLILoggingService.debug('Phase 5: Creating view query migrations...') let viewMigrationCount = 0 @@ -147,23 +147,23 @@ export async function generateMigrationsHandler( } if (viewMigrationCount > 0) { - CLILoggingService.success( + CLILoggingService.debug( `Created ${viewMigrationCount} view query migrations` ) // Phase 6: Apply view query migrations - CLILoggingService.step('Phase 6: Applying view query migrations...') + CLILoggingService.debug('Phase 6: Applying view query migrations...') runMigrateUp() } else { - CLILoggingService.info( + CLILoggingService.debug( 'No view collections found, skipping view migrations' ) } // Summary const message = targetModule - ? `Migration script completed for module ${chalk.bold.blue(targetModule)}` - : 'Migration script completed for all modules' + ? `Database migrations applied for ${chalk.bold.blue(targetModule)}` + : 'Database migrations applied for all modules' CLILoggingService.success(message) } catch (error) { diff --git a/tools/forgeCLI/src/commands/db/utils/pocketbase-utils.ts b/tools/forgeCLI/src/commands/db/utils/pocketbase-utils.ts index 890d286cd..61e2a56e6 100644 --- a/tools/forgeCLI/src/commands/db/utils/pocketbase-utils.ts +++ b/tools/forgeCLI/src/commands/db/utils/pocketbase-utils.ts @@ -6,8 +6,6 @@ import path from 'path' import { PB_BINARY_PATH, PB_KWARGS, PB_MIGRATIONS_DIR } from '@/constants/db' import CLILoggingService from '@/utils/logging' - - /** * Cleans up old migrations */ @@ -15,7 +13,7 @@ export async function cleanupOldMigrations( targetModule?: string ): Promise { try { - CLILoggingService.warn('Cleaning up old migrations directory...') + CLILoggingService.debug('Cleaning up old migrations directory...') if (!targetModule) { fs.rmSync(PB_MIGRATIONS_DIR, { recursive: true, force: true }) @@ -41,7 +39,7 @@ export async function cleanupOldMigrations( } ) - CLILoggingService.info( + CLILoggingService.debug( `Removed ${chalk.bold.blue( migrationFiles.filter(file => file.endsWith(`_${targetModule}.js`)) .length diff --git a/tools/forgeCLI/src/commands/locales/constants/index.tsx b/tools/forgeCLI/src/commands/locales/constants/index.tsx new file mode 100644 index 000000000..13362276f --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/constants/index.tsx @@ -0,0 +1,8 @@ +import fs from 'fs' +import path from 'path' + +export const LOCALES_DIR = path.join(process.cwd(), 'locales') + +if (!fs.existsSync(LOCALES_DIR)) { + fs.mkdirSync(LOCALES_DIR) +} diff --git a/tools/forgeCLI/src/commands/locales/functions/ensureLocaleNotInUse.ts b/tools/forgeCLI/src/commands/locales/functions/ensureLocaleNotInUse.ts new file mode 100644 index 000000000..115f59771 --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/functions/ensureLocaleNotInUse.ts @@ -0,0 +1,27 @@ +import CLILoggingService from '@/utils/logging' +import getPBInstance from '@/utils/pocketbase' + +async function ensureLocaleNotInUse(shortName: string) { + CLILoggingService.debug('Checking if locale is in use...') + + const { pb, killPB } = await getPBInstance() + + try { + const user = await pb.collection('users').getFirstListItem("id != ''") + + if (user.language === shortName) { + CLILoggingService.actionableError( + `Cannot uninstall locale "${shortName}"`, + 'This language is currently selected. Change your language first.' + ) + + killPB?.() + + process.exit(1) + } + } finally { + killPB?.() + } +} + +export default ensureLocaleNotInUse diff --git a/tools/forgeCLI/src/commands/locales/functions/getInstalledLocales.ts b/tools/forgeCLI/src/commands/locales/functions/getInstalledLocales.ts new file mode 100644 index 000000000..3e5ffbe3b --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/functions/getInstalledLocales.ts @@ -0,0 +1,44 @@ +import fs from 'fs' +import path from 'path' + +import { LOCALES_DIR } from '../constants' + +export function getInstalledLocales(): string[] { + return fs.readdirSync(LOCALES_DIR).filter(dir => { + if (dir.startsWith('.')) return false + + const fullPath = path.join(LOCALES_DIR, dir) + + const packageJsonPath = path.join(fullPath, 'package.json') + + return fs.statSync(fullPath).isDirectory() && fs.existsSync(packageJsonPath) + }) +} + +export function getInstalledLocalesWithMeta(): { + name: string + displayName: string + version: string +}[] { + const locales = getInstalledLocales() + + const installedLocales: { + name: string + displayName: string + version: string + }[] = [] + + locales.forEach(locale => { + const packageJsonPath = path.join(LOCALES_DIR, locale, 'package.json') + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) + + installedLocales.push({ + name: locale, + displayName: packageJson.lifeforge?.displayName || locale, + version: packageJson.version + }) + }) + + return installedLocales +} diff --git a/tools/forgeCLI/src/commands/locales/functions/getLocalesMeta.ts b/tools/forgeCLI/src/commands/locales/functions/getLocalesMeta.ts new file mode 100644 index 000000000..5b24d42b0 --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/functions/getLocalesMeta.ts @@ -0,0 +1,35 @@ +import path from 'path' + +import { LOCALES_DIR } from '../constants' + +function extractLocaleName(packageName: string): string { + return packageName.replace('@lifeforge/lang-', '') +} + +export function normalizeLocalePackageName(langCode: string): string { + return langCode.startsWith('@lifeforge/lang-') + ? langCode + : `@lifeforge/lang-${langCode}` +} + +function getLocalePath(langCode: string): string { + const shortName = extractLocaleName(langCode) + + return path.join(LOCALES_DIR, shortName) +} + +function getLocalesMeta(langCode: string) { + const fullPackageName = normalizeLocalePackageName(langCode) + + const shortName = extractLocaleName(fullPackageName) + + const targetDir = getLocalePath(shortName) + + return { + fullPackageName, + shortName, + targetDir + } +} + +export default getLocalesMeta diff --git a/tools/forgeCLI/src/commands/locales/functions/getPackagesToCheck.ts b/tools/forgeCLI/src/commands/locales/functions/getPackagesToCheck.ts new file mode 100644 index 000000000..2ced3d8fd --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/functions/getPackagesToCheck.ts @@ -0,0 +1,33 @@ +import CLILoggingService from '@/utils/logging' + +import { getInstalledLocalesWithMeta } from './getInstalledLocales' +import { normalizeLocalePackageName } from './getLocalesMeta' + +function getPackagesToCheck(langCode?: string) { + const localePackages = getInstalledLocalesWithMeta() + + if (!localePackages.length) { + CLILoggingService.info('No locales installed') + + process.exit(0) + } + + const packagesToCheck = langCode + ? localePackages.filter( + p => p.name === normalizeLocalePackageName(langCode) + ) + : localePackages + + if (!packagesToCheck?.length) { + CLILoggingService.actionableError( + `Locale "${langCode}" is not installed`, + 'Run "bun forge locales list" to see installed locales' + ) + + process.exit(0) + } + + return packagesToCheck +} + +export default getPackagesToCheck diff --git a/tools/forgeCLI/src/commands/locales/functions/getUpgrades.ts b/tools/forgeCLI/src/commands/locales/functions/getUpgrades.ts new file mode 100644 index 000000000..81856744e --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/functions/getUpgrades.ts @@ -0,0 +1,63 @@ +import CLILoggingService from '@/utils/logging' +import { getRegistryUrl } from '@/utils/registry' + +interface LocaleUpgrade { + name: string + current: string + latest: string +} + +async function getLatestLocaleVersion( + packageName: string +): Promise { + try { + const registryUrl = getRegistryUrl() + + const response = await fetch(`${registryUrl}/${packageName}`) + + if (!response.ok) { + return null + } + + const data = (await response.json()) as { + 'dist-tags'?: { latest?: string } + } + + return data['dist-tags']?.latest || null + } catch { + return null + } +} + +async function getUpgrades( + packagesToCheck: { name: string; version: string }[] +) { + const upgrades: LocaleUpgrade[] = [] + + for (const pkg of packagesToCheck) { + const latestVersion = await getLatestLocaleVersion(pkg.name) + + if (latestVersion && latestVersion !== pkg.version) { + upgrades.push({ + name: pkg.name, + current: pkg.version, + latest: latestVersion + }) + } + } + + if (!upgrades.length) { + CLILoggingService.success('All locales are up to date!') + + process.exit(0) + } + + CLILoggingService.info('Available upgrades:') + upgrades.forEach(u => + CLILoggingService.info(` ${u.name}: ${u.current} → ${u.latest}`) + ) + + return upgrades +} + +export default getUpgrades diff --git a/tools/forgeCLI/src/commands/locales/functions/installAndMoveLocales.ts b/tools/forgeCLI/src/commands/locales/functions/installAndMoveLocales.ts new file mode 100644 index 000000000..c3c21e119 --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/functions/installAndMoveLocales.ts @@ -0,0 +1,30 @@ +import fs from 'fs' + +import { executeCommand } from '@/utils/helpers' +import { addWorkspaceDependency } from '@/utils/package' + +function installAndMoveLocales(fullPackageName: string, targetDir: string) { + if (fs.existsSync(targetDir)) { + fs.rmSync(targetDir, { recursive: true, force: true }) + } + + executeCommand(`bun add ${fullPackageName}@latest`, { + cwd: process.cwd(), + stdio: 'inherit' + }) + + const installedPath = `${process.cwd()}/node_modules/${fullPackageName}` + + if (!fs.existsSync(installedPath)) { + throw new Error(`Failed to install ${fullPackageName}`) + } + + fs.cpSync(installedPath, targetDir, { recursive: true }) + + addWorkspaceDependency(fullPackageName) + + fs.rmSync(installedPath, { recursive: true, force: true }) + executeCommand('bun install', { cwd: process.cwd(), stdio: 'inherit' }) +} + +export default installAndMoveLocales diff --git a/tools/forgeCLI/src/commands/locales/functions/setFirstLangInDB.ts b/tools/forgeCLI/src/commands/locales/functions/setFirstLangInDB.ts new file mode 100644 index 000000000..24ede5e29 --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/functions/setFirstLangInDB.ts @@ -0,0 +1,23 @@ +import CLILoggingService from '@/utils/logging' +import getPBInstance from '@/utils/pocketbase' + +import { getInstalledLocales } from './getInstalledLocales' + +async function setFirstLangInDB(shortName: string) { + const installedLocales = getInstalledLocales() + + if (installedLocales.length === 1) { + CLILoggingService.step('First language pack - setting as default for user') + + const { pb, killPB } = await getPBInstance() + + const user = await pb.collection('users').getFirstListItem("id != ''") + + await pb.collection('users').update(user.id, { language: shortName }) + + CLILoggingService.info(`Set ${shortName} as default language`) + killPB?.() + } +} + +export default setFirstLangInDB diff --git a/tools/forgeCLI/src/commands/locales/functions/validateLocaleStructure.ts b/tools/forgeCLI/src/commands/locales/functions/validateLocaleStructure.ts new file mode 100644 index 000000000..6f358fcef --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/functions/validateLocaleStructure.ts @@ -0,0 +1,65 @@ +import fs from 'fs' +import path from 'path' + +import CLILoggingService from '@/utils/logging' + +interface LocalePackageJson { + name?: string + version?: string + lifeforge?: { + displayName?: string + icon?: string + } + [key: string]: unknown +} + +export function validateLocaleStructure(localePath: string) { + const errors: string[] = [] + + const warnings: string[] = [] + + const packageJsonPath = path.join(localePath, 'package.json') + + if (!fs.existsSync(packageJsonPath)) { + errors.push('Missing package.json') + + return { valid: false, errors, warnings } + } + + const packageJson: LocalePackageJson = JSON.parse( + fs.readFileSync(packageJsonPath, 'utf-8') + ) + + if (!packageJson.name) { + errors.push('package.json is missing "name" field') + } else if (!packageJson.name.startsWith('@lifeforge/lang-')) { + errors.push('Package name must start with "@lifeforge/lang-"') + } + + if (!packageJson.version) { + errors.push('package.json is missing "version" field') + } else if (!packageJson.version.match(/^\d+\.\d+\.\d+/)) { + errors.push('Version must be valid semver (e.g., 0.1.0)') + } + + if (!packageJson.lifeforge) { + errors.push('package.json is missing "lifeforge" field') + } else { + if (!packageJson.lifeforge.displayName) { + errors.push('lifeforge.displayName is required') + } + + if (!packageJson.lifeforge.icon) { + warnings.push('lifeforge.icon is missing (optional)') + } + } + + if (errors.length > 0) { + CLILoggingService.error('Locale validation failed:') + errors.forEach(err => CLILoggingService.error(` - ${err}`)) + + process.exit(1) + } + + warnings.forEach(warn => CLILoggingService.warn(` - ${warn}`)) +} diff --git a/tools/forgeCLI/src/commands/locales/handlers/addLocaleHandler.ts b/tools/forgeCLI/src/commands/locales/handlers/addLocaleHandler.ts deleted file mode 100644 index 07d9c2bf2..000000000 --- a/tools/forgeCLI/src/commands/locales/handlers/addLocaleHandler.ts +++ /dev/null @@ -1,191 +0,0 @@ -import chalk from 'chalk' -import fs from 'fs' - -import { updateGitSubmodules } from '@/commands/modules/functions' -import { executeCommand } from '@/utils/helpers' -import CLILoggingService from '@/utils/logging' -import getPBInstance from '@/utils/pocketbase' - -import { - type LocaleInstallConfig, - createLocaleConfig, - getInstalledLocales, - localeExists, - validateLocaleName -} from '../utils' - -const LOCALE_STRUCTURE_REQUIREMENTS = ['manifest.json'] - -/** - * Removes path from git index if it exists but is not a submodule - * This handles the case where locale directories were previously tracked directly - */ -function removeFromGitIndex(localeDir: string): void { - try { - executeCommand(`git rm -r --cached --ignore-unmatch ${localeDir}`, { - exitOnError: false, - stdio: ['ignore', 'ignore', 'ignore'] - }) - } catch { - // Ignore errors, path may not exist in index - } -} - -/** - * Clones locale repository from GitHub as a submodule - */ -function cloneLocaleRepository(config: LocaleInstallConfig): void { - if (!fs.existsSync('.gitmodules')) { - fs.writeFileSync('.gitmodules', '') - } - - CLILoggingService.progress('Cloning locale repository from GitHub') - - // Remove from git index if it was previously tracked (not as submodule) - removeFromGitIndex(config.localeDir) - - try { - executeCommand( - `git submodule add --force ${config.repoUrl} ${config.localeDir}`, - { - exitOnError: false, - stdio: ['ignore', 'ignore', 'ignore'] - } - ) - - CLILoggingService.success('Repository cloned successfully') - } catch (error) { - CLILoggingService.actionableError( - 'Failed to clone locale repository', - `Verify the repository lifeforge-app/lang-${config.langName} exists and is accessible` - ) - throw error - } -} - -/** - * Validates the locale structure - */ -function validateLocaleStructure(config: LocaleInstallConfig): void { - CLILoggingService.step('Validating locale structure') - - const missingFiles = LOCALE_STRUCTURE_REQUIREMENTS.filter( - file => !fs.existsSync(`${config.localeDir}/${file}`) - ) - - if (missingFiles.length > 0) { - CLILoggingService.actionableError( - 'Invalid locale structure detected', - `Missing required files: ${missingFiles.join(', ')}` - ) - throw new Error('Invalid locale structure') - } - - CLILoggingService.success('Locale structure validated') -} - -/** - * Cleans up on failure - */ -function cleanup(localeDir: string): void { - if (fs.existsSync(localeDir)) { - fs.rmSync(localeDir, { recursive: true }) - } -} - -/** - * Removes submodule entry from .gitmodules on failure - */ -function cleanupGitmodules(localeDir: string): void { - if (!fs.existsSync('.gitmodules')) return - - const content = fs.readFileSync('.gitmodules', 'utf-8') - - const lines = content.split('\n') - - const filteredLines: string[] = [] - - let skipSection = false - - for (const line of lines) { - if (line.startsWith('[submodule')) { - skipSection = line.includes(`"${localeDir}"`) - } - - if (!skipSection) { - filteredLines.push(line) - } - } - - fs.writeFileSync('.gitmodules', filteredLines.join('\n').trim() + '\n') -} - -/** - * Handles adding a new locale to the LifeForge system - */ -export async function addLocaleHandler(langName: string): Promise { - if (!validateLocaleName(langName)) { - CLILoggingService.actionableError( - 'Invalid language name format', - 'Use formats like "en", "ms", "zh-CN", "zh-TW" (lowercase language code, optionally with uppercase region)' - ) - process.exit(1) - } - - if (localeExists(langName)) { - CLILoggingService.actionableError( - `Language "${langName}" is already installed`, - 'Use "bun forge locales list" to see installed languages' - ) - process.exit(1) - } - - const installedLocales = getInstalledLocales() - - const isFirstLocale = installedLocales.length === 0 - - const config = createLocaleConfig(langName) - - CLILoggingService.step(`Adding language pack: ${langName}`) - - try { - cloneLocaleRepository(config) - validateLocaleStructure(config) - updateGitSubmodules(config.localeDir) - - executeCommand('git add .gitmodules') - - if (isFirstLocale) { - CLILoggingService.step( - 'First language pack - setting as default for all users' - ) - - const { pb, killPB } = await getPBInstance() - - const users = await pb.collection('users').getFullList() - - for (const user of users) { - await pb.collection('users').update(user.id, { language: langName }) - } - - CLILoggingService.info( - `Set ${chalk.bold.blue(langName)} as default language for ${chalk.bold.blue(users.length)} user(s)` - ) - - killPB?.() - } - - CLILoggingService.success( - `Language pack "${langName}" installed successfully! Restart the server to apply changes.` - ) - } catch (error) { - CLILoggingService.actionableError( - 'Locale installation failed', - 'Check the error details above and try again' - ) - CLILoggingService.debug(`Installation error: ${error}`) - cleanup(config.localeDir) - cleanupGitmodules(config.localeDir) - process.exit(1) - } -} diff --git a/tools/forgeCLI/src/commands/locales/handlers/install-locale.ts b/tools/forgeCLI/src/commands/locales/handlers/install-locale.ts new file mode 100644 index 000000000..0bbcc9497 --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/handlers/install-locale.ts @@ -0,0 +1,38 @@ +import fs from 'fs' + +import CLILoggingService from '@/utils/logging' + +import getLocalesMeta from '../functions/getLocalesMeta' +import installAndMoveLocales from '../functions/installAndMoveLocales' +import setFirstLangInDB from '../functions/setFirstLangInDB' + +export async function installLocaleHandler(langCode: string): Promise { + const { fullPackageName, shortName, targetDir } = getLocalesMeta(langCode) + + if (fs.existsSync(targetDir)) { + CLILoggingService.actionableError( + `Locale already exists at locales/${shortName}`, + `Remove it first with: bun forge locales uninstall ${shortName}` + ) + + process.exit(1) + } + + CLILoggingService.progress('Fetching locale from registry...') + + try { + installAndMoveLocales(fullPackageName, targetDir) + + await setFirstLangInDB(shortName) + + CLILoggingService.success( + `Locale ${fullPackageName} installed successfully!` + ) + } catch (error) { + CLILoggingService.actionableError( + `Failed to install ${fullPackageName}`, + 'Make sure the locale exists in the registry' + ) + throw error + } +} diff --git a/tools/forgeCLI/src/commands/locales/handlers/list-locales.ts b/tools/forgeCLI/src/commands/locales/handlers/list-locales.ts new file mode 100644 index 000000000..b8e85dbd5 --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/handlers/list-locales.ts @@ -0,0 +1,26 @@ +import chalk from 'chalk' + +import CLILoggingService from '@/utils/logging' + +import { getInstalledLocalesWithMeta } from '../functions/getInstalledLocales' + +export function listLocalesHandler(): void { + const locales = getInstalledLocalesWithMeta() + + if (locales.length === 0) { + CLILoggingService.info('No language packs installed') + CLILoggingService.info( + 'Use "bun forge locales install " to install a language pack' + ) + + return + } + + CLILoggingService.info(`Installed language packs (${locales.length}):`) + + for (const locale of locales.sort((a, b) => a.name.localeCompare(b.name))) { + console.log( + ` ${chalk.bold.blue(locale.name)} - ${locale.displayName} (v.${locale.version})` + ) + } +} diff --git a/tools/forgeCLI/src/commands/locales/handlers/listLocalesHandler.ts b/tools/forgeCLI/src/commands/locales/handlers/listLocalesHandler.ts deleted file mode 100644 index 308c80233..000000000 --- a/tools/forgeCLI/src/commands/locales/handlers/listLocalesHandler.ts +++ /dev/null @@ -1,43 +0,0 @@ -import chalk from 'chalk' -import fs from 'fs' -import path from 'path' - -import CLILoggingService from '@/utils/logging' - -import { getInstalledLocales } from '../utils' - -/** - * Lists all installed locales - */ -export function listLocalesHandler(): void { - const locales = getInstalledLocales() - - if (locales.length === 0) { - CLILoggingService.info('No language packs installed') - CLILoggingService.info( - 'Use "bun forge locales add " to install a language pack' - ) - - return - } - - CLILoggingService.info(`Installed language packs (${locales.length}):`) - - for (const locale of locales) { - const manifestPath = path.join('locales', locale, 'manifest.json') - - let displayName = locale - - if (fs.existsSync(manifestPath)) { - try { - const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) - - displayName = manifest.displayName || locale - } catch { - // Use locale name as fallback - } - } - - console.log(` ${chalk.bold.blue(locale)} - ${displayName}`) - } -} diff --git a/tools/forgeCLI/src/commands/locales/handlers/publish-locale.ts b/tools/forgeCLI/src/commands/locales/handlers/publish-locale.ts new file mode 100644 index 000000000..56dfb1545 --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/handlers/publish-locale.ts @@ -0,0 +1,48 @@ +import fs from 'fs' + +import { executeCommand } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' + +import { validateMaintainerAccess } from '../../../utils/github-cli' +import { checkAuth, getRegistryUrl } from '../../../utils/registry' +import getLocalesMeta from '../functions/getLocalesMeta' +import { validateLocaleStructure } from '../functions/validateLocaleStructure' + +export async function publishLocaleHandler( + langCode: string, + options?: { official?: boolean } +): Promise { + const { targetDir, shortName } = getLocalesMeta(langCode) + + if (!fs.existsSync(targetDir)) { + CLILoggingService.actionableError( + `Locale "${langCode}" not found in locales/`, + 'Run "bun forge locales list" to see available locales' + ) + + return + } + + validateLocaleStructure(targetDir) + + const auth = await checkAuth() + + if (options?.official) { + validateMaintainerAccess(auth.username ?? '') + } + + try { + executeCommand(`npm publish --registry ${getRegistryUrl()}`, { + cwd: targetDir, + stdio: 'inherit' + }) + + CLILoggingService.success(`Locale "${shortName}" published successfully!`) + } catch (error) { + CLILoggingService.actionableError( + 'Failed to publish locale', + 'Check if you are properly authenticated with the registry' + ) + throw error + } +} diff --git a/tools/forgeCLI/src/commands/locales/handlers/removeLocaleHandler.ts b/tools/forgeCLI/src/commands/locales/handlers/removeLocaleHandler.ts deleted file mode 100644 index e7d8a87ac..000000000 --- a/tools/forgeCLI/src/commands/locales/handlers/removeLocaleHandler.ts +++ /dev/null @@ -1,191 +0,0 @@ -import chalk from 'chalk' -import fs from 'fs' -import type PocketBase from 'pocketbase' - -import { executeCommand } from '@/utils/helpers' -import CLILoggingService from '@/utils/logging' -import getPBInstance from '@/utils/pocketbase' - -import { getInstalledLocales, localeExists, validateLocaleName } from '../utils' - -/** - * Updates users' language preferences when a locale is being removed - */ -async function updateUsersLanguage( - pb: PocketBase, - fromLang: string, - toLang: string -): Promise { - const users = await pb.collection('users').getFullList({ - filter: `language = "${fromLang}"` - }) - - if (users.length === 0) { - return 0 - } - - CLILoggingService.progress( - `Updating ${users.length} user(s) from "${fromLang}" to "${toLang}"` - ) - - for (const user of users) { - await pb.collection('users').update(user.id, { language: toLang }) - } - - return users.length -} - -/** - * Removes the git submodule for a locale - */ -function removeGitSubmodule(localeDir: string): void { - CLILoggingService.progress('Removing git submodule') - - // Step 1: Deinitialize the submodule - try { - executeCommand(`git submodule deinit -f ${localeDir}`, { - exitOnError: false, - stdio: ['ignore', 'ignore', 'ignore'] - }) - } catch { - // May fail if not a proper submodule - } - - // Step 2: Remove from git index (git rm) - try { - executeCommand(`git rm -rf ${localeDir}`, { - exitOnError: false, - stdio: ['ignore', 'ignore', 'ignore'] - }) - } catch { - // May fail if not tracked - } - - // Step 3: Force remove from git index (handles orphaned entries) - try { - executeCommand(`git update-index --force-remove ${localeDir}`, { - exitOnError: false, - stdio: ['ignore', 'ignore', 'ignore'] - }) - } catch { - // May fail if not in index - } - - // Step 4: Remove from .git/modules - const gitModulesPath = `.git/modules/${localeDir}` - - if (fs.existsSync(gitModulesPath)) { - fs.rmSync(gitModulesPath, { recursive: true }) - } - - // Step 5: Remove entry from .gitmodules file - if (fs.existsSync('.gitmodules')) { - const content = fs.readFileSync('.gitmodules', 'utf-8') - - const lines = content.split('\n') - - const filteredLines: string[] = [] - - let skipSection = false - - for (const line of lines) { - if (line.startsWith('[submodule')) { - skipSection = line.includes(`"${localeDir}"`) - } - - if (!skipSection) { - filteredLines.push(line) - } - } - - fs.writeFileSync('.gitmodules', filteredLines.join('\n').trim() + '\n') - - try { - executeCommand('git add .gitmodules', { - exitOnError: false, - stdio: ['ignore', 'ignore', 'ignore'] - }) - } catch { - // Ignore - } - } - - CLILoggingService.success('Git submodule removed successfully') -} - -/** - * Cleans up locale directory if it still exists - */ -function cleanupLocaleDir(localeDir: string): void { - if (fs.existsSync(localeDir)) { - fs.rmSync(localeDir, { recursive: true }) - } -} - -/** - * Handles removing a locale from the LifeForge system - */ -export async function removeLocaleHandler(langName: string): Promise { - if (!validateLocaleName(langName)) { - CLILoggingService.actionableError( - 'Invalid language name format', - 'Use formats like "en", "ms", "zh-CN", "zh-TW"' - ) - process.exit(1) - } - - if (!localeExists(langName)) { - CLILoggingService.actionableError( - `Language "${langName}" is not installed`, - 'Use "bun forge locales list" to see installed languages' - ) - process.exit(1) - } - - const installedLocales = getInstalledLocales() - - if (installedLocales.length <= 1) { - CLILoggingService.actionableError( - 'Cannot remove the last installed language', - 'At least one language must remain installed' - ) - process.exit(1) - } - - CLILoggingService.step(`Removing language pack: ${langName}`) - - const { pb, killPB } = await getPBInstance() - - try { - const remainingLocales = installedLocales.filter(l => l !== langName) - - const fallbackLang = remainingLocales[0] - - const affectedUsers = await updateUsersLanguage(pb, langName, fallbackLang) - - if (affectedUsers > 0) { - CLILoggingService.info( - `Updated ${chalk.bold.blue(affectedUsers)} user(s) to "${fallbackLang}"` - ) - } - - const localeDir = `locales/${langName}` - - removeGitSubmodule(localeDir) - cleanupLocaleDir(localeDir) - - CLILoggingService.success( - `Language pack "${langName}" removed successfully! Restart the server to apply changes.` - ) - killPB?.() - } catch (error) { - CLILoggingService.actionableError( - 'Locale removal failed', - 'Check the error details above and try again' - ) - CLILoggingService.debug(`Removal error: ${error}`) - killPB?.() - - process.exit(1) - } -} diff --git a/tools/forgeCLI/src/commands/locales/handlers/uninstall-locale.ts b/tools/forgeCLI/src/commands/locales/handlers/uninstall-locale.ts new file mode 100644 index 000000000..6cd2191e0 --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/handlers/uninstall-locale.ts @@ -0,0 +1,35 @@ +import fs from 'fs' + +import { executeCommand } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' +import { findPackageName, removeWorkspaceDependency } from '@/utils/package' + +import ensureLocaleNotInUse from '../functions/ensureLocaleNotInUse' +import getLocalesMeta from '../functions/getLocalesMeta' + +export async function uninstallLocaleHandler(langCode: string): Promise { + const { fullPackageName, shortName, targetDir } = getLocalesMeta(langCode) + + const found = findPackageName(fullPackageName) + + if (!found) { + CLILoggingService.actionableError( + `Locale "${shortName}" is not installed`, + 'Run "bun forge locales list" to see installed locales' + ) + + return + } + + await ensureLocaleNotInUse(shortName) + + CLILoggingService.info(`Uninstalling locale ${fullPackageName}...`) + + fs.rmSync(targetDir, { recursive: true, force: true }) + + removeWorkspaceDependency(fullPackageName) + + executeCommand('bun install', { cwd: process.cwd(), stdio: 'inherit' }) + + CLILoggingService.info(`Uninstalled locale ${fullPackageName}`) +} diff --git a/tools/forgeCLI/src/commands/locales/handlers/upgrade-locale.ts b/tools/forgeCLI/src/commands/locales/handlers/upgrade-locale.ts new file mode 100644 index 000000000..7ffed67de --- /dev/null +++ b/tools/forgeCLI/src/commands/locales/handlers/upgrade-locale.ts @@ -0,0 +1,39 @@ +import { confirmAction, executeCommand } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' + +import { checkAuth } from '../../../utils/registry' +import getLocalesMeta from '../functions/getLocalesMeta' +import getPackagesToCheck from '../functions/getPackagesToCheck' +import getUpgrades from '../functions/getUpgrades' +import installAndMoveLocales from '../functions/installAndMoveLocales' + +export async function upgradeLocaleHandler(langCode?: string): Promise { + const packagesToCheck = getPackagesToCheck(langCode) + + const upgrades = await getUpgrades(packagesToCheck) + + if (!(await confirmAction('Proceed with upgrades?'))) return + + await checkAuth() + + let upgradedCount = 0 + + for (const upgrade of upgrades) { + try { + installAndMoveLocales( + upgrade.name, + getLocalesMeta(upgrade.name).targetDir + ) + + CLILoggingService.success(`Upgraded ${upgrade.name} to ${upgrade.latest}`) + upgradedCount++ + } catch (error) { + CLILoggingService.error(`Failed to upgrade ${upgrade.name}: ${error}`) + } + } + + if (upgradedCount > 0) { + executeCommand('bun install', { cwd: process.cwd(), stdio: 'inherit' }) + CLILoggingService.success(`Upgraded ${upgradedCount} locale(s)`) + } +} diff --git a/tools/forgeCLI/src/commands/locales/index.ts b/tools/forgeCLI/src/commands/locales/index.ts index 27173df00..5fd7db1a4 100644 --- a/tools/forgeCLI/src/commands/locales/index.ts +++ b/tools/forgeCLI/src/commands/locales/index.ts @@ -1,28 +1,58 @@ -import { program } from 'commander' +import type { Command } from 'commander' -import { addLocaleHandler } from './handlers/addLocaleHandler' -import { listLocalesHandler } from './handlers/listLocalesHandler' -import { removeLocaleHandler } from './handlers/removeLocaleHandler' +import { loginModuleHandler } from '../modules/handlers/login-module' +import { installLocaleHandler } from './handlers/install-locale' +import { listLocalesHandler } from './handlers/list-locales' +import { publishLocaleHandler } from './handlers/publish-locale' +import { uninstallLocaleHandler } from './handlers/uninstall-locale' +import { upgradeLocaleHandler } from './handlers/upgrade-locale' -export default function setup(): void { +export default function setup(program: Command): void { const command = program .command('locales') .description('Manage LifeForge language packs') + command + .command('login') + .description('Login to the locale registry') + .action(loginModuleHandler) + command .command('list') .description('List all installed language packs') .action(listLocalesHandler) command - .command('add') - .description('Download and install a language pack') + .command('install') + .alias('i') + .description('Install a language pack from the registry') .argument('', 'Language code, e.g., en, ms, zh-CN, zh-TW') - .action(addLocaleHandler) + .action(installLocaleHandler) command - .command('remove') - .description('Remove an installed language pack') + .command('uninstall') + .alias('un') + .description('Uninstall a language pack') .argument('', 'Language code to remove') - .action(removeLocaleHandler) + .action(uninstallLocaleHandler) + + command + .command('upgrade') + .alias('up') + .description('Upgrade language packs to latest version') + .argument( + '[lang]', + 'Language code to upgrade (optional, checks all if omitted)' + ) + .action(upgradeLocaleHandler) + + command + .command('publish') + .description('Publish a language pack to the registry') + .argument('', 'Language code to publish from locales/') + .option( + '--official', + 'Publish as official locale (requires maintainer access)' + ) + .action(publishLocaleHandler) } diff --git a/tools/forgeCLI/src/commands/locales/utils.ts b/tools/forgeCLI/src/commands/locales/utils.ts deleted file mode 100644 index 928cb8767..000000000 --- a/tools/forgeCLI/src/commands/locales/utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import fs from 'fs' -import path from 'path' - -const LOCALES_DIR = 'locales' - -/** - * Checks if a locale already exists in the locales directory - */ -export function localeExists(langName: string): boolean { - return fs.existsSync(path.join(LOCALES_DIR, langName)) -} - -/** - * Gets list of installed locales from locales directory - */ -export function getInstalledLocales(): string[] { - if (!fs.existsSync(LOCALES_DIR)) { - return [] - } - - return fs - .readdirSync(LOCALES_DIR, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name) - .filter(name => !name.startsWith('.')) -} - -/** - * Validates language name format (lowercase, alphanumeric with hyphens) - */ -export function validateLocaleName(langName: string): boolean { - return /^[a-z]{2}(-[A-Z]{2})?$/.test(langName) -} - -export interface LocaleInstallConfig { - langName: string - localeDir: string - repoUrl: string - tempDir: string -} - -/** - * Creates locale installation configuration - */ -export function createLocaleConfig(langName: string): LocaleInstallConfig { - return { - langName, - localeDir: path.join(LOCALES_DIR, langName), - repoUrl: `https://github.com/lifeforge-app/lang-${langName}.git`, - tempDir: '.temp' - } -} diff --git a/tools/forgeCLI/src/commands/modules/functions/git/git-status.ts b/tools/forgeCLI/src/commands/modules/functions/git-status.ts similarity index 100% rename from tools/forgeCLI/src/commands/modules/functions/git/git-status.ts rename to tools/forgeCLI/src/commands/modules/functions/git-status.ts diff --git a/tools/forgeCLI/src/commands/modules/functions/git/clone-repository.ts b/tools/forgeCLI/src/commands/modules/functions/git/clone-repository.ts deleted file mode 100644 index 6850c5733..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/git/clone-repository.ts +++ /dev/null @@ -1,32 +0,0 @@ -import fs from 'fs' - -import { executeCommand } from '@/utils/helpers' -import CLILoggingService from '@/utils/logging' - -import type { ModuleInstallConfig } from '../../utils/constants' - -export function cloneModuleRepository(config: ModuleInstallConfig): void { - if (!fs.existsSync('.gitmodules')) { - fs.writeFileSync('.gitmodules', '') - } - - CLILoggingService.progress('Cloning module repository from GitHub') - - try { - executeCommand( - `git submodule add --force ${config.repoUrl} ${config.tempDir}/${config.moduleName}`, - { - exitOnError: false, - stdio: ['ignore', 'ignore', 'ignore'] - } - ) - - CLILoggingService.success('Repository cloned successfully') - } catch (error) { - CLILoggingService.actionableError( - 'Failed to clone module repository', - 'Verify the repository URL is correct and accessible, or check your internet connection' - ) - throw error - } -} diff --git a/tools/forgeCLI/src/commands/modules/functions/git/git-submodule.ts b/tools/forgeCLI/src/commands/modules/functions/git/git-submodule.ts deleted file mode 100644 index d6ce7d921..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/git/git-submodule.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { execSync } from 'child_process' -import fs from 'fs' -import path from 'path' - -import { executeCommand } from '@/utils/helpers' -import CLILoggingService from '@/utils/logging' - -export function updateGitSubmodules(modulePath?: string): void { - if (modulePath) { - CLILoggingService.progress(`Updating git submodule: ${modulePath}`) - } else { - CLILoggingService.progress('Updating all git submodules') - } - - try { - const command = modulePath - ? `git submodule update --init --recursive --remote ${modulePath}` - : 'git submodule update --init --recursive --remote' - - executeCommand(command, { - stdio: ['ignore', 'ignore', 'ignore'], - exitOnError: false - }) - CLILoggingService.success( - modulePath - ? `Git submodule updated: ${modulePath}` - : 'Git submodules updated successfully' - ) - } catch (error) { - CLILoggingService.actionableError( - 'Failed to update git submodules', - 'Check your git configuration and try again' - ) - throw error - } -} - -export function removeGitSubmodule(modulePath: string): void { - CLILoggingService.progress(`Removing git submodule: ${modulePath}`) - - try { - execSync(`git submodule deinit -f ${modulePath}`, { - cwd: process.cwd(), - stdio: ['pipe', 'pipe', 'pipe'] - }) - CLILoggingService.debug('Submodule deinitialized') - - execSync(`git rm -f ${modulePath}`, { - cwd: process.cwd(), - stdio: ['pipe', 'pipe', 'pipe'] - }) - CLILoggingService.debug('Submodule removed from git') - - const gitModulesDir = path.join( - process.cwd(), - '.git', - 'modules', - modulePath - ) - - if (fs.existsSync(gitModulesDir)) { - fs.rmSync(gitModulesDir, { recursive: true, force: true }) - CLILoggingService.debug('Submodule git directory removed') - } - - CLILoggingService.success(`Git submodule removed: ${modulePath}`) - } catch (error) { - CLILoggingService.warn( - `Git submodule removal failed, falling back to manual removal: ${error}` - ) - throw error - } -} - -export function removeGitModulesEntry(modulePath: string): void { - const gitModulesPath = path.join(process.cwd(), '.gitmodules') - - if (!fs.existsSync(gitModulesPath)) { - return - } - - CLILoggingService.progress('Updating .gitmodules file') - - try { - let gitModulesContent = fs.readFileSync(gitModulesPath, 'utf8') - - const moduleEntryRegex = new RegExp( - `\\[submodule "${modulePath.replace( - /[-/\\^$*+?.()|[\]{}]/g, - '\\$&' - )}"\\][^\\[]*`, - 'g' - ) - - gitModulesContent = gitModulesContent.replace(moduleEntryRegex, '') - gitModulesContent = gitModulesContent.replace(/\n{3,}/g, '\n\n').trim() - - if (gitModulesContent) { - fs.writeFileSync(gitModulesPath, gitModulesContent + '\n', 'utf8') - } else { - fs.unlinkSync(gitModulesPath) - } - - CLILoggingService.success('.gitmodules file updated') - } catch (error) { - CLILoggingService.warn(`Failed to update .gitmodules file: ${error}`) - } -} diff --git a/tools/forgeCLI/src/commands/modules/functions/git/github-cli.ts b/tools/forgeCLI/src/commands/modules/functions/git/github-cli.ts deleted file mode 100644 index 1de9ff9bb..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/git/github-cli.ts +++ /dev/null @@ -1,88 +0,0 @@ -import fs from 'fs' - -import { executeCommand } from '@/utils/helpers' -import CLILoggingService from '@/utils/logging' - -export function checkGithubCLI(): void { - try { - executeCommand('gh --version', { stdio: 'pipe' }) - } catch { - CLILoggingService.actionableError( - 'GitHub CLI is not installed or not found in PATH.', - 'Please install GitHub CLI from https://cli.github.com/ and ensure it is accessible from your command line.' - ) - process.exit(1) - } - - CLILoggingService.info('GitHub CLI is installed and ready to use.') - - const authCheck = executeCommand('gh auth status', { stdio: 'pipe' }) - - if (!authCheck.includes('Logged in to github.com')) { - CLILoggingService.actionableError( - 'GitHub CLI is not authenticated.', - 'Please authenticate by running "gh auth login" and follow the prompts.' - ) - process.exit(1) - } - - CLILoggingService.info('GitHub CLI is authenticated.') -} - -export function createGithubRepo(moduleName: string): string { - try { - executeCommand( - `gh repo create lifeforge-module-${moduleName} --public --source=./apps/${moduleName} --remote=origin --push`, - { stdio: 'pipe' } - ) - - const repoLinkResult = executeCommand(`git remote get-url origin`, { - stdio: 'pipe', - cwd: `apps/${moduleName}` - }) - - const repoLinkMatch = repoLinkResult.match( - /https:\/\/github\.com\/(.*?\/lifeforge-module-.*?)\.git/ - ) - - if (!repoLinkMatch) { - CLILoggingService.actionableError( - `Failed to parse GitHub repository link for module ${moduleName}.`, - 'Please check the output above for any errors.' - ) - process.exit(1) - } - - const repoLink = repoLinkMatch[1] - - CLILoggingService.success( - `GitHub repository for module ${moduleName} created and code pushed successfully.` - ) - - return repoLink - } catch { - CLILoggingService.actionableError( - `Failed to create GitHub repository for module ${moduleName}.`, - 'Refer to the error message above for more details.' - ) - process.exit(1) - } -} - -export function replaceRepoWithSubmodule( - moduleName: string, - repoLink: string -): void { - const modulePath = `apps/${moduleName}` - - try { - fs.rmSync(modulePath, { recursive: true, force: true }) - executeCommand(`bun forge modules add ${repoLink}`) - } catch (error) { - CLILoggingService.actionableError( - `Failed to replace local module ${moduleName} with Git submodule.`, - `Error: ${error instanceof Error ? error.message : String(error)}` - ) - process.exit(1) - } -} diff --git a/tools/forgeCLI/src/commands/modules/functions/git/index.ts b/tools/forgeCLI/src/commands/modules/functions/git/index.ts deleted file mode 100644 index e6ed2498a..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/git/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { cloneModuleRepository } from './clone-repository' - -export { - removeGitModulesEntry, - removeGitSubmodule, - updateGitSubmodules -} from './git-submodule' - -export { checkForUpdates, checkGitCleanliness } from './git-status' - -export type { CommitInfo } from './git-status' - -export { - checkGithubCLI, - createGithubRepo, - replaceRepoWithSubmodule -} from './github-cli' diff --git a/tools/forgeCLI/src/commands/modules/functions/index.ts b/tools/forgeCLI/src/commands/modules/functions/index.ts index 7afbe8088..b4ecb4eba 100644 --- a/tools/forgeCLI/src/commands/modules/functions/index.ts +++ b/tools/forgeCLI/src/commands/modules/functions/index.ts @@ -1,14 +1,16 @@ // Git operations -export * from './git' +export * from './git-status' +export * from '../../../utils/github-cli' -// Module lifecycle operations -export * from './module-lifecycle' - -// Migration operations -export * from './migrations' +// Module operations +export * from './install-dependencies' +export * from './module-migrations' // Interactive prompts export * from './prompts' // Template operations export * from './templates' + +// Registry generation +export * from './registry' diff --git a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/install-dependencies.ts b/tools/forgeCLI/src/commands/modules/functions/install-dependencies.ts similarity index 100% rename from tools/forgeCLI/src/commands/modules/functions/module-lifecycle/install-dependencies.ts rename to tools/forgeCLI/src/commands/modules/functions/install-dependencies.ts diff --git a/tools/forgeCLI/src/commands/modules/functions/migrations/index.ts b/tools/forgeCLI/src/commands/modules/functions/migrations/index.ts deleted file mode 100644 index 926fb9e25..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/migrations/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - generateDatabaseSchemas, - generateSchemaMigrations, - removeModuleMigrations -} from './module-migrations' diff --git a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/index.ts b/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/index.ts deleted file mode 100644 index 1350fb35e..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { installDependencies } from './install-dependencies' - -export { processServerInjection } from './process-server-injection' - -export { removeServerReferences } from './remove-server-references' - -export { validateModuleStructure } from './validate-module-structure' - -export { - moveModuleToApps, - removeModuleDirectory, - removeRegularDirectory -} from './move-module' diff --git a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/move-module.ts b/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/move-module.ts deleted file mode 100644 index 0b4d2e901..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/move-module.ts +++ /dev/null @@ -1,81 +0,0 @@ -import fs from 'fs' -import path from 'path' - -import { executeCommand } from '@/utils/helpers' -import CLILoggingService from '@/utils/logging' - -import type { ModuleInstallConfig } from '../../utils/constants' -import { removeGitModulesEntry, removeGitSubmodule } from '../git/git-submodule' - -export function moveModuleToApps(config: ModuleInstallConfig): void { - CLILoggingService.step('Installing module to workspace') - - executeCommand( - `git mv ${config.tempDir}/${config.moduleName} ${config.moduleDir}` - ) - CLILoggingService.success( - `Module ${config.author}/${config.moduleName} installed successfully` - ) - - let gitmodulesContent = fs.readFileSync('.gitmodules', 'utf-8') - - const modulePath = `${config.tempDir}/${config.moduleName}` - - gitmodulesContent = gitmodulesContent.replace( - `[submodule "${modulePath}"]`, - `[submodule "apps/${config.moduleName}"]` - ) - - fs.writeFileSync('.gitmodules', gitmodulesContent.trim() + '\n') - - executeCommand('git add .gitmodules') -} - -export function removeModuleDirectory(moduleName: string): void { - const modulePath = `apps/${moduleName}` - - const moduleDir = path.join(process.cwd(), modulePath) - - if (!fs.existsSync(moduleDir)) { - CLILoggingService.warn(`Module directory ${modulePath} does not exist`) - - return - } - - CLILoggingService.progress(`Removing module directory: ${modulePath}`) - - const gitModulesPath = path.join(process.cwd(), '.gitmodules') - - const isSubmodule = - fs.existsSync(gitModulesPath) && - fs - .readFileSync(gitModulesPath, 'utf8') - .includes(`[submodule "${modulePath}"]`) - - if (isSubmodule) { - try { - removeGitSubmodule(modulePath) - } catch { - removeRegularDirectory(moduleDir, modulePath) - removeGitModulesEntry(modulePath) - } - } else { - removeRegularDirectory(moduleDir, modulePath) - } -} - -export function removeRegularDirectory( - moduleDir: string, - modulePath: string -): void { - try { - fs.rmSync(moduleDir, { recursive: true, force: true }) - CLILoggingService.success(`Module directory removed: ${modulePath}`) - } catch (error) { - CLILoggingService.actionableError( - `Failed to remove module directory: ${modulePath}`, - 'Check file permissions and ensure no processes are using the module files' - ) - throw error - } -} diff --git a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/process-server-injection.ts b/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/process-server-injection.ts deleted file mode 100644 index 6361e46f9..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/process-server-injection.ts +++ /dev/null @@ -1,43 +0,0 @@ -import CLILoggingService from '@/utils/logging' - -import { hasServerComponents } from '../../utils/file-system' -import { injectModuleRoute } from '../../utils/route-injection' -import { injectModuleSchema } from '../../utils/schema-injection' - -export function processServerInjection(moduleName: string): void { - CLILoggingService.step('Checking for server components') - - const { hasServerDir, hasServerIndex } = hasServerComponents(moduleName) - - if (!hasServerDir) { - CLILoggingService.info( - `No server directory found - skipping server setup (UI-only module)` - ) - - return - } - - if (!hasServerIndex) { - CLILoggingService.info(`No server index.ts found - skipping server setup`) - - return - } - - CLILoggingService.progress('Setting up server components') - - try { - injectModuleRoute(moduleName) - CLILoggingService.success('Server routes configured') - } catch (error) { - CLILoggingService.warn(`Failed to inject route for ${moduleName}: ${error}`) - } - - try { - injectModuleSchema(moduleName) - CLILoggingService.success('Server schema configured') - } catch (error) { - CLILoggingService.warn( - `Failed to inject schema for ${moduleName}: ${error}` - ) - } -} diff --git a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/remove-server-references.ts b/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/remove-server-references.ts deleted file mode 100644 index 753fd1981..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/remove-server-references.ts +++ /dev/null @@ -1,24 +0,0 @@ -import CLILoggingService from '@/utils/logging' - -import { removeModuleRoute } from '../../utils/route-injection' -import { removeModuleSchema } from '../../utils/schema-injection' - -export function removeServerReferences(moduleName: string): void { - CLILoggingService.progress('Removing server references') - - try { - removeModuleRoute(moduleName) - CLILoggingService.success('Server routes removed') - } catch (error) { - CLILoggingService.warn(`Failed to remove route for ${moduleName}: ${error}`) - } - - try { - removeModuleSchema(moduleName) - CLILoggingService.success('Server schema references removed') - } catch (error) { - CLILoggingService.warn( - `Failed to remove schema for ${moduleName}: ${error}` - ) - } -} diff --git a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/validate-module-structure.ts b/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/validate-module-structure.ts deleted file mode 100644 index e7d2c629f..000000000 --- a/tools/forgeCLI/src/commands/modules/functions/module-lifecycle/validate-module-structure.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { type PathConfig, validateFilePaths } from '@/utils/helpers' -import CLILoggingService from '@/utils/logging' - -import { type ModuleInstallConfig } from '../../utils/constants' - -const MODULE_STRUCTURE_REQUIREMENTS: PathConfig[] = [ - { - path: 'client', - type: 'directory' - }, - { - path: 'package.json', - type: 'file' - }, - { - path: 'manifest.ts', - type: 'file' - }, - { - path: 'locales', - type: 'directory' - }, - { - path: 'tsconfig.json', - type: 'file' - } -] - -export function validateModuleStructure(config: ModuleInstallConfig): void { - CLILoggingService.step('Validating module structure') - - validateFilePaths( - MODULE_STRUCTURE_REQUIREMENTS, - `${config.tempDir}/${config.moduleName}` - ) - - CLILoggingService.success('Module structure validated') -} diff --git a/tools/forgeCLI/src/commands/modules/functions/migrations/module-migrations.ts b/tools/forgeCLI/src/commands/modules/functions/module-migrations.ts similarity index 100% rename from tools/forgeCLI/src/commands/modules/functions/migrations/module-migrations.ts rename to tools/forgeCLI/src/commands/modules/functions/module-migrations.ts diff --git a/tools/forgeCLI/src/commands/modules/functions/registry/client-registry.ts b/tools/forgeCLI/src/commands/modules/functions/registry/client-registry.ts new file mode 100644 index 000000000..1b97f9a90 --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/functions/registry/client-registry.ts @@ -0,0 +1,41 @@ +import { extractModuleName } from './module-utils' + +export function generateClientRegistry(modules: string[]): string { + if (modules.length === 0) { + return `// AUTO-GENERATED - DO NOT EDIT +import type { ModuleConfig } from 'shared' + +export const modules: ModuleConfig[] = [] +` + } + + const imports = modules + .map(mod => { + const name = extractModuleName(mod) + + const varName = name.replace(/-/g, '_') + + return `import ${varName}Manifest from '${mod}/manifest'` + }) + .join('\n') + + const exports = modules + .map(mod => { + const name = extractModuleName(mod) + + const varName = name.replace(/-/g, '_') + + return ` ${varName}Manifest,` + }) + .join('\n') + + return `// AUTO-GENERATED - DO NOT EDIT +import type { ModuleConfig } from 'shared' + +${imports} + +export const modules: ModuleConfig[] = [ +${exports} +] +` +} diff --git a/tools/forgeCLI/src/commands/modules/functions/registry/generator.ts b/tools/forgeCLI/src/commands/modules/functions/registry/generator.ts new file mode 100644 index 000000000..1895a320e --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/functions/registry/generator.ts @@ -0,0 +1,150 @@ +import fs from 'fs' +import path from 'path' + +import CLILoggingService from '@/utils/logging' + +import { generateClientRegistry } from './client-registry' +import { + getLifeforgeModules, + getModulePath, + moduleHasSchema +} from './module-utils' +import { generateSchemaRegistry } from './schema-registry' +import { generateServerRegistry } from './server-registry' + +interface ModulePackageJson { + exports?: Record + [key: string]: unknown +} + +function generateManifestDeclaration(): string { + return `// AUTO-GENERATED - DO NOT EDIT +// This declaration file allows TypeScript to type-check module imports +// without resolving internal module aliases like @ +import type { ModuleConfig } from 'shared' + +declare const manifest: ModuleConfig +export default manifest +` +} + +function updateModulePackageJson(modulePath: string): boolean { + const packageJsonPath = path.join(modulePath, 'package.json') + + if (!fs.existsSync(packageJsonPath)) { + return false + } + + const packageJson: ModulePackageJson = JSON.parse( + fs.readFileSync(packageJsonPath, 'utf-8') + ) + + if (!packageJson.exports) { + return false + } + + let updated = false + + if (packageJson.exports['./manifest']) { + const currentExport = packageJson.exports['./manifest'] + + if (typeof currentExport === 'string') { + packageJson.exports['./manifest'] = { + types: './manifest.d.ts', + default: currentExport + } + updated = true + } else if (typeof currentExport === 'object' && !currentExport.types) { + currentExport.types = './manifest.d.ts' + updated = true + } + } + + const schemaPath = path.join(modulePath, 'server', 'schema.ts') + + if (fs.existsSync(schemaPath) && !packageJson.exports['./server/schema']) { + packageJson.exports['./server/schema'] = './server/schema.ts' + updated = true + } + + if (updated) { + fs.writeFileSync( + packageJsonPath, + JSON.stringify(packageJson, null, 2) + '\n' + ) + } + + return updated +} + +export function generateModuleRegistries(): void { + CLILoggingService.progress('Generating module registries...') + + const modules = getLifeforgeModules() + + if (modules.length === 0) { + CLILoggingService.info('No @lifeforge/* modules found') + } else { + CLILoggingService.debug(`Found ${modules.length} module(s):`) + modules.forEach(mod => CLILoggingService.debug(` - ${mod}`)) + } + + const serverOutputPath = path.join( + process.cwd(), + 'server/src/core/routes/generated-routes.ts' + ) + + const clientOutputPath = path.join( + process.cwd(), + 'client/src/module-registry.ts' + ) + + const schemaOutputPath = path.join(process.cwd(), 'server/src/core/schema.ts') + + // Generate server registry + const serverContent = generateServerRegistry(modules) + + fs.mkdirSync(path.dirname(serverOutputPath), { recursive: true }) + fs.writeFileSync(serverOutputPath, serverContent) + CLILoggingService.debug(`Generated: ${serverOutputPath}`) + + // Generate client registry + const clientContent = generateClientRegistry(modules) + + fs.mkdirSync(path.dirname(clientOutputPath), { recursive: true }) + fs.writeFileSync(clientOutputPath, clientContent) + CLILoggingService.debug(`Generated: ${clientOutputPath}`) + + // Generate schema registry (only for modules with schema.ts) + const modulesWithSchema = modules.filter(mod => moduleHasSchema(mod)) + + const schemaContent = generateSchemaRegistry(modulesWithSchema) + + fs.mkdirSync(path.dirname(schemaOutputPath), { recursive: true }) + fs.writeFileSync(schemaOutputPath, schemaContent) + CLILoggingService.debug(`Generated: ${schemaOutputPath}`) + + // Generate manifest.d.ts for each module and update package.json + for (const mod of modules) { + const modulePath = getModulePath(mod) + + if (modulePath) { + const declarationPath = path.join(modulePath, 'manifest.d.ts') + + const declarationContent = generateManifestDeclaration() + + fs.writeFileSync(declarationPath, declarationContent) + CLILoggingService.debug(`Generated: ${declarationPath}`) + + const updated = updateModulePackageJson(modulePath) + + if (updated) { + CLILoggingService.debug( + `Updated: ${path.join(modulePath, 'package.json')}` + ) + } + } + } + + CLILoggingService.success('Module registries generated') +} diff --git a/tools/forgeCLI/src/commands/modules/functions/registry/index.ts b/tools/forgeCLI/src/commands/modules/functions/registry/index.ts new file mode 100644 index 000000000..fe597802c --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/functions/registry/index.ts @@ -0,0 +1,11 @@ +export { generateClientRegistry } from './client-registry' + +export { + getLifeforgeModules, + getModulePath, + moduleHasSchema +} from './module-utils' + +export { generateSchemaRegistry } from './schema-registry' + +export { generateServerRegistry } from './server-registry' diff --git a/tools/forgeCLI/src/commands/modules/functions/registry/module-utils.ts b/tools/forgeCLI/src/commands/modules/functions/registry/module-utils.ts new file mode 100644 index 000000000..8a5d41661 --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/functions/registry/module-utils.ts @@ -0,0 +1,62 @@ +import fs from 'fs' +import path from 'path' + +const LIFEFORGE_SCOPE = '@lifeforge/' + +interface PackageJson { + dependencies?: Record + devDependencies?: Record +} + +export function getLifeforgeModules(): string[] { + const packageJsonPath = path.join(process.cwd(), 'package.json') + + const packageJson: PackageJson = JSON.parse( + fs.readFileSync(packageJsonPath, 'utf-8') + ) + + const allDeps = { + ...packageJson.dependencies, + ...packageJson.devDependencies + } + + return Object.keys(allDeps).filter(dep => dep.startsWith(LIFEFORGE_SCOPE)) +} + +export function extractModuleName(packageName: string): string { + const withoutScope = packageName.replace(LIFEFORGE_SCOPE, '') + + if (withoutScope.startsWith('lifeforge--')) { + return withoutScope.replace('lifeforge--', '') + } + + return withoutScope +} + +export function getModulePath(packageName: string): string | null { + const nodeModulesPath = path.join(process.cwd(), 'node_modules', packageName) + + try { + const realPath = fs.realpathSync(nodeModulesPath) + + if (realPath.includes('/apps/')) { + return realPath + } + + return nodeModulesPath + } catch { + return null + } +} + +export function moduleHasSchema(packageName: string): boolean { + const modulePath = getModulePath(packageName) + + if (!modulePath) { + return false + } + + const schemaPath = path.join(modulePath, 'server', 'schema.ts') + + return fs.existsSync(schemaPath) +} diff --git a/tools/forgeCLI/src/commands/modules/functions/registry/schema-registry.ts b/tools/forgeCLI/src/commands/modules/functions/registry/schema-registry.ts new file mode 100644 index 000000000..90ebfc5c2 --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/functions/registry/schema-registry.ts @@ -0,0 +1,25 @@ +import { extractModuleName } from './module-utils' + +export function generateSchemaRegistry(modulesWithSchema: string[]): string { + const moduleSchemas = modulesWithSchema + .map(mod => { + const name = extractModuleName(mod) + + return ` ${name}: (await import('${mod}/server/schema')).default,` + }) + .join('\n') + + return `// AUTO-GENERATED - DO NOT EDIT +import flattenSchemas from '@functions/utils/flattenSchema' + +export const SCHEMAS = { + user: (await import('@lib/user/schema')).default, + api_keys: (await import('@lib/apiKeys/schema')).default, +${moduleSchemas} +} + +const COLLECTION_SCHEMAS = flattenSchemas(SCHEMAS) + +export default COLLECTION_SCHEMAS +` +} diff --git a/tools/forgeCLI/src/commands/modules/functions/registry/server-registry.ts b/tools/forgeCLI/src/commands/modules/functions/registry/server-registry.ts new file mode 100644 index 000000000..75892861d --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/functions/registry/server-registry.ts @@ -0,0 +1,31 @@ +import { extractModuleName } from './module-utils' + +export function generateServerRegistry(modules: string[]): string { + if (modules.length === 0) { + return `// AUTO-GENERATED - DO NOT EDIT +import { forgeRouter } from '@functions/routes' + +const appRoutes = forgeRouter({}) + +export default appRoutes +` + } + + const imports = modules + .map(mod => { + const name = extractModuleName(mod) + + return ` ${name}: (await import('${mod}/server')).default,` + }) + .join('\n') + + return `// AUTO-GENERATED - DO NOT EDIT +import { forgeRouter } from '@functions/routes' + +const appRoutes = forgeRouter({ +${imports} +}) + +export default appRoutes +` +} diff --git a/tools/forgeCLI/src/commands/modules/handlers/add-module.ts b/tools/forgeCLI/src/commands/modules/handlers/add-module.ts deleted file mode 100644 index 1354aea8c..000000000 --- a/tools/forgeCLI/src/commands/modules/handlers/add-module.ts +++ /dev/null @@ -1,68 +0,0 @@ -import fs from 'fs' - -import CLILoggingService from '@/utils/logging' -import { checkRunningPBInstances } from '@/utils/pocketbase' - -import { cloneModuleRepository, updateGitSubmodules } from '../functions/git' -import { generateSchemaMigrations } from '../functions/migrations' -import { - installDependencies, - moveModuleToApps, - processServerInjection, - validateModuleStructure -} from '../functions/module-lifecycle' -import { cleanup, moduleExists } from '../utils/file-system' -import { createModuleConfig, validateRepositoryPath } from '../utils/validation' - -export async function addModuleHandler(repoPath: string): Promise { - checkRunningPBInstances() - - if (!validateRepositoryPath(repoPath)) { - CLILoggingService.actionableError( - 'Invalid module repository path format', - 'Use the format /, e.g., "lifeforge-app/wallet"' - ) - process.exit(1) - } - - const config = createModuleConfig(repoPath) - - CLILoggingService.step(`Adding module ${repoPath} from ${config.author}`) - - cleanup(config.tempDir) - fs.mkdirSync(config.tempDir) - - try { - if (moduleExists(config.moduleName)) { - CLILoggingService.actionableError( - `Module "${config.moduleName}" already exists in workspace`, - `Remove it first with "bun forge module remove ${config.moduleName}" if you want to re-add it` - ) - throw new Error('Module already exists') - } - - cloneModuleRepository(config) - validateModuleStructure(config) - moveModuleToApps(config) - updateGitSubmodules(`apps/${config.moduleName}`) - processServerInjection(config.moduleName) - installDependencies() - - if (fs.existsSync(`${config.moduleDir}/server/schema.ts`)) { - generateSchemaMigrations(config.moduleName) - } - - CLILoggingService.success( - `Module ${repoPath} setup completed successfully! Start the system with "bun forge dev"` - ) - cleanup(config.tempDir) - } catch (error) { - CLILoggingService.actionableError( - 'Module installation failed', - 'Check the error details above and try again' - ) - CLILoggingService.debug(`Installation error: ${error}`) - cleanup(config.tempDir) - process.exit(1) - } -} diff --git a/tools/forgeCLI/src/commands/modules/handlers/create-module.ts b/tools/forgeCLI/src/commands/modules/handlers/create-module.ts index 21f458ce4..4791993e6 100644 --- a/tools/forgeCLI/src/commands/modules/handlers/create-module.ts +++ b/tools/forgeCLI/src/commands/modules/handlers/create-module.ts @@ -5,8 +5,8 @@ import { runDatabaseMigrations } from '@/commands/db/functions/database-initiali import CLILoggingService from '@/utils/logging' import { checkRunningPBInstances } from '@/utils/pocketbase' -import { generateDatabaseSchemas } from '../functions/migrations' -import { installDependencies } from '../functions/module-lifecycle' +import { installDependencies } from '../functions/install-dependencies' +import { generateDatabaseSchemas } from '../functions/module-migrations' import { checkModuleTypeAvailability, promptForModuleName, @@ -15,14 +15,13 @@ import { promptModuleType, selectIcon } from '../functions/prompts' +import { generateModuleRegistries } from '../functions/registry/generator' import { type ModuleMetadata, copyTemplateFiles, initializeGitRepository, registerHandlebarsHelpers } from '../functions/templates' -import { injectModuleRoute } from '../utils/route-injection' -import { injectModuleSchema } from '../utils/schema-injection' registerHandlebarsHelpers() @@ -57,8 +56,8 @@ export async function createModuleHandler(moduleName?: string): Promise { installDependencies(`${process.cwd()}/apps`) - injectModuleRoute(camelizedModuleName) - injectModuleSchema(camelizedModuleName) + // Regenerate registries to include the new module + generateModuleRegistries() if ( fs.existsSync( diff --git a/tools/forgeCLI/src/commands/modules/handlers/install-module.ts b/tools/forgeCLI/src/commands/modules/handlers/install-module.ts new file mode 100644 index 000000000..ce6462e54 --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/handlers/install-module.ts @@ -0,0 +1,155 @@ +import fs from 'fs' +import path from 'path' + +import { executeCommand } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' + +import { generateModuleRegistries } from '../functions/registry/generator' + +interface PackageJson { + name?: string + version?: string + dependencies?: Record + [key: string]: unknown +} + +function extractModuleName(packageName: string): string { + // @lifeforge/lifeforge--calendar -> lifeforge--calendar + // @lifeforge/melvin--myapp -> melvin--myapp + return packageName.replace('@lifeforge/', '') +} + +export async function installModuleHandler(moduleName: string): Promise { + // Normalize module name + const fullPackageName = moduleName.startsWith('@lifeforge/') + ? moduleName + : `@lifeforge/${moduleName}` + + const shortName = extractModuleName(fullPackageName) + + const appsDir = path.join(process.cwd(), 'apps') + + const targetDir = path.join(appsDir, shortName) + + CLILoggingService.info(`Installing module ${fullPackageName}...`) + + // Check if module already exists in apps/ + if (fs.existsSync(targetDir)) { + CLILoggingService.actionableError( + `Module already exists at apps/${shortName}`, + `Remove it first with: bun forge modules remove ${shortName}` + ) + + return + } + + // Create apps directory if it doesn't exist + if (!fs.existsSync(appsDir)) { + fs.mkdirSync(appsDir, { recursive: true }) + } + + CLILoggingService.progress('Fetching module from registry...') + + try { + // Use bun to install the package to node_modules + executeCommand(`bun add ${fullPackageName}@latest`, { + cwd: process.cwd(), + stdio: 'inherit' + }) + + // Find the installed package in node_modules + const installedPath = path.join( + process.cwd(), + 'node_modules', + fullPackageName + ) + + if (!fs.existsSync(installedPath)) { + throw new Error(`Failed to install ${fullPackageName}`) + } + + CLILoggingService.progress('Moving module to apps/...') + + // Copy from node_modules to apps/ + fs.cpSync(installedPath, targetDir, { recursive: true }) + + CLILoggingService.success(`Module copied to apps/${shortName}`) + + // Update root package.json to use workspace:* + CLILoggingService.progress('Updating package.json...') + + const rootPackageJsonPath = path.join(process.cwd(), 'package.json') + + const rootPackageJson: PackageJson = JSON.parse( + fs.readFileSync(rootPackageJsonPath, 'utf-8') + ) + + if (!rootPackageJson.dependencies) { + rootPackageJson.dependencies = {} + } + + // Change to workspace reference + rootPackageJson.dependencies[fullPackageName] = 'workspace:*' + + fs.writeFileSync( + rootPackageJsonPath, + JSON.stringify(rootPackageJson, null, 2) + '\n' + ) + + CLILoggingService.success('Updated root package.json') + + // Run bun install to create symlinks + CLILoggingService.progress('Linking workspace...') + + // Remove the node_modules copy so bun creates a proper symlink + const nodeModulesPath = path.join( + process.cwd(), + 'node_modules', + fullPackageName + ) + + if (fs.existsSync(nodeModulesPath)) { + fs.rmSync(nodeModulesPath, { recursive: true, force: true }) + } + + executeCommand('bun install', { + cwd: process.cwd(), + stdio: 'inherit' + }) + + // Generate module registries + CLILoggingService.progress('Generating module registries...') + generateModuleRegistries() + + // Generate database migrations if the module has a schema + const schemaPath = path.join(targetDir, 'server', 'schema.ts') + + if (fs.existsSync(schemaPath)) { + CLILoggingService.progress('Generating database migrations...') + + try { + executeCommand(`bun forge db push ${shortName}`, { + cwd: process.cwd(), + stdio: 'inherit' + }) + + CLILoggingService.success('Database migrations generated') + } catch { + CLILoggingService.warn( + 'Failed to generate database migrations. You may need to run "bun forge db migrations generate" manually.' + ) + } + } + + CLILoggingService.success( + `Module ${fullPackageName} installed successfully!` + ) + CLILoggingService.info(`Location: apps/${shortName}`) + } catch (error) { + CLILoggingService.actionableError( + `Failed to install ${fullPackageName}`, + 'Make sure the module exists in the registry' + ) + throw error + } +} diff --git a/tools/forgeCLI/src/commands/modules/handlers/login-module.ts b/tools/forgeCLI/src/commands/modules/handlers/login-module.ts new file mode 100644 index 000000000..5138ce5ba --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/handlers/login-module.ts @@ -0,0 +1,38 @@ +import { confirmAction } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' + +import { + checkAuth, + getRegistryUrl, + openRegistryLogin +} from '../../../utils/registry' + +export async function loginModuleHandler(): Promise { + CLILoggingService.progress('Checking registry authentication...') + + const auth = await checkAuth() + + if (auth.authenticated && auth.username) { + CLILoggingService.success(`Already authenticated as ${auth.username}`) + + const reLogin = await confirmAction('Would you like to login again?') + + if (!reLogin) { + return + } + } + + CLILoggingService.info('Opening registry login page...') + + openRegistryLogin() + + const registry = getRegistryUrl() + + CLILoggingService.info('Please follow these steps to complete login:') + CLILoggingService.info('1. Log in with GitHub on the registry page') + CLILoggingService.info('2. Copy your token from the registry UI') + CLILoggingService.info('3. Run the following command:') + CLILoggingService.info( + `npm config set //${registry.replace('http://', '').replace(/\/$/, '')}/:_authToken "YOUR_TOKEN"` + ) +} diff --git a/tools/forgeCLI/src/commands/modules/handlers/migrate-module.ts b/tools/forgeCLI/src/commands/modules/handlers/migrate-module.ts new file mode 100644 index 000000000..61a8e48c5 --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/handlers/migrate-module.ts @@ -0,0 +1,283 @@ +import fs from 'fs' +import kebabCase from 'lodash/kebabCase' +import path from 'path' + +import { executeCommand } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' + +import { + getGithubUser, + validateMaintainerAccess +} from '../../../utils/github-cli' +import { checkAuth } from '../../../utils/registry' +import { generateModuleRegistries } from '../functions/registry/generator' + +interface PackageJson { + name?: string + version?: string + exports?: Record + [key: string]: unknown +} + +function toNewFolderName(oldName: string, username?: string): string { + // codeTime -> lifeforge--code-time (kebab-case) + // or with username: invoiceMaker -> melvinchia3636--invoice-maker + const normalized = kebabCase(oldName) + + if (username) { + return `${username}--${normalized}` + } + + if (normalized.startsWith('lifeforge-')) { + // Already has lifeforge prefix, just add the extra dash + return normalized.replace('lifeforge-', 'lifeforge--') + } + + return `lifeforge--${normalized}` +} + +function toPackageName(folderName: string): string { + // lifeforge--code-time -> @lifeforge/lifeforge--code-time + // melvinchia3636--invoice-maker -> @lifeforge/melvinchia3636--invoice-maker + return `@lifeforge/${folderName}` +} + +function getUnmigratedModules(): string[] { + const appsDir = path.join(process.cwd(), 'apps') + + if (!fs.existsSync(appsDir)) { + return [] + } + + const entries = fs.readdirSync(appsDir, { withFileTypes: true }) + + return entries + .filter(entry => entry.isDirectory()) + .filter(entry => !entry.name.includes('--')) + .filter(entry => !entry.name.startsWith('.')) + .map(entry => entry.name) +} + +async function migrateSingleModule( + moduleName: string, + username?: string, + skipGenHandler = false +): Promise { + const appsDir = path.join(process.cwd(), 'apps') + + const oldPath = path.join(appsDir, moduleName) + + // Check if module exists + if (!fs.existsSync(oldPath)) { + CLILoggingService.warn(`Module "${moduleName}" not found in apps/`) + + return false + } + + // Check if already migrated + if (moduleName.startsWith('lifeforge--')) { + CLILoggingService.debug(`Module "${moduleName}" already migrated, skipping`) + + return false + } + + const newFolderName = toNewFolderName(moduleName, username) + + const newPath = path.join(appsDir, newFolderName) + + const packageName = toPackageName(newFolderName) + + CLILoggingService.step(`Migrating "${moduleName}" → "${newFolderName}"`) + + try { + // Step 1: Rename folder + if (fs.existsSync(newPath)) { + CLILoggingService.warn( + `Target folder "${newFolderName}" already exists, skipping` + ) + + return false + } + + fs.renameSync(oldPath, newPath) + + // Step 2: Remove .git submodule reference + const gitPath = path.join(newPath, '.git') + + if (fs.existsSync(gitPath)) { + const gitStat = fs.statSync(gitPath) + + if (gitStat.isFile()) { + fs.unlinkSync(gitPath) + } else { + fs.rmSync(gitPath, { recursive: true, force: true }) + } + } + + // Step 3: Update package.json + const packageJsonPath = path.join(newPath, 'package.json') + + const packageJson: PackageJson = JSON.parse( + fs.readFileSync(packageJsonPath, 'utf-8') + ) + + packageJson.name = packageName + + if (packageJson.version && !packageJson.version.match(/^\d+\.\d+\.\d+/)) { + packageJson.version = '0.1.0' + } + + // Populate author if missing + if (!packageJson.author) { + CLILoggingService.progress( + 'Fetching GitHub user details for author field...' + ) + + const user = getGithubUser() + + if (user) { + packageJson.author = `${user.name} <${user.email}>` + CLILoggingService.success(`Set author to: ${packageJson.author}`) + } else { + CLILoggingService.warn( + 'Could not fetch GitHub user details for author field' + ) + } + } + + const hasServerIndex = fs.existsSync( + path.join(newPath, 'server', 'index.ts') + ) + + const hasSchema = fs.existsSync(path.join(newPath, 'server', 'schema.ts')) + + packageJson.exports = { + ...(hasServerIndex && { './server': './server/index.ts' }), + './manifest': './manifest.ts', + ...(hasSchema && { './server/schema': './server/schema.ts' }) + } + + fs.writeFileSync( + packageJsonPath, + JSON.stringify(packageJson, null, 2) + '\n' + ) + + // Step 4: Add to root package.json + const rootPackageJsonPath = path.join(process.cwd(), 'package.json') + + const rootPackageJson = JSON.parse( + fs.readFileSync(rootPackageJsonPath, 'utf-8') + ) + + if (!rootPackageJson.dependencies) { + rootPackageJson.dependencies = {} + } + + rootPackageJson.dependencies[packageName] = 'workspace:*' + + fs.writeFileSync( + rootPackageJsonPath, + JSON.stringify(rootPackageJson, null, 2) + '\n' + ) + + CLILoggingService.success(`Migrated "${moduleName}" → "${packageName}"`) + + // Only run bun install and gen if not batching + if (!skipGenHandler) { + executeCommand('bun install', { + cwd: process.cwd(), + stdio: 'inherit' + }) + + generateModuleRegistries() + } + + return true + } catch (error) { + CLILoggingService.error(`Failed to migrate "${moduleName}": ${error}`) + + return false + } +} + +export async function migrateModuleHandler( + folderName?: string, + options?: { official?: boolean } +): Promise { + // Check authentication first + CLILoggingService.progress('Checking registry authentication...') + + const auth = await checkAuth() + + if (!auth.authenticated || !auth.username) { + CLILoggingService.actionableError( + 'Authentication required to migrate modules', + 'Run: bun forge modules login' + ) + process.exit(1) + } + + CLILoggingService.success(`Authenticated as ${auth.username}`) + + let username = auth.username + + if (options?.official) { + const isMaintainer = validateMaintainerAccess(auth.username) + + if (!isMaintainer) { + CLILoggingService.actionableError( + 'Maintainer access required', + 'You must have maintainer access to lifeforge-app/lifeforge to migrate as official module' + ) + process.exit(1) + } + + username = 'lifeforge' // Use lifeforge as the "username" prefix for official modules + } + + // If no folder specified, migrate all unmigrated modules + if (!folderName) { + const unmigrated = getUnmigratedModules() + + if (unmigrated.length === 0) { + CLILoggingService.info('No unmigrated modules found in apps/') + + return + } + + CLILoggingService.step(`Found ${unmigrated.length} unmigrated module(s):`) + unmigrated.forEach(mod => CLILoggingService.info(` - ${mod}`)) + + let migratedCount = 0 + + for (const mod of unmigrated) { + const success = await migrateSingleModule(mod, username, true) + + if (success) { + migratedCount++ + } + } + + // Run bun install and gen once at the end + if (migratedCount > 0) { + CLILoggingService.progress('Linking workspaces...') + + executeCommand('bun install', { + cwd: process.cwd(), + stdio: 'inherit' + }) + + CLILoggingService.progress('Generating registries...') + generateModuleRegistries() + + CLILoggingService.success(`Migrated ${migratedCount} module(s)`) + } + + return + } + + // Normalize folder name (remove apps/ prefix if present) + const moduleName = folderName.replace(/^apps\//, '') + + await migrateSingleModule(moduleName, username) +} diff --git a/tools/forgeCLI/src/commands/modules/handlers/publish-module.ts b/tools/forgeCLI/src/commands/modules/handlers/publish-module.ts index 6ac8f61d3..3ee065274 100644 --- a/tools/forgeCLI/src/commands/modules/handlers/publish-module.ts +++ b/tools/forgeCLI/src/commands/modules/handlers/publish-module.ts @@ -1,30 +1,281 @@ +import fs from 'fs' +import path from 'path' + +import { confirmAction, executeCommand } from '@/utils/helpers' import CLILoggingService from '@/utils/logging' -import { checkRunningPBInstances } from '@/utils/pocketbase' import { - checkGitCleanliness, - checkGithubCLI, - createGithubRepo, - replaceRepoWithSubmodule -} from '../functions/git' -import { getInstalledModules } from '../utils/file-system' + checkAuth, + getRegistryUrl, + openRegistryLogin +} from '../../../utils/registry' +import { validateMaintainerAccess } from '../functions' -export async function publishModuleHandler(moduleName: string): Promise { - const availableModules = getInstalledModules() +const LIFEFORGE_SCOPE = '@lifeforge' - if (!availableModules.includes(moduleName)) { +interface PackageJson { + name?: string + version?: string + description?: string + exports?: Record + [key: string]: unknown +} + +interface ModuleValidationResult { + valid: boolean + errors: string[] + warnings: string[] +} + +function validateModuleStructure(modulePath: string): ModuleValidationResult { + const errors: string[] = [] + + const warnings: string[] = [] + + // Check package.json + const packageJsonPath = path.join(modulePath, 'package.json') + + if (!fs.existsSync(packageJsonPath)) { + errors.push('Missing package.json') + + return { valid: false, errors, warnings } + } + + const packageJson: PackageJson = JSON.parse( + fs.readFileSync(packageJsonPath, 'utf-8') + ) + + // Check name follows @lifeforge/-- pattern + if (!packageJson.name) { + errors.push('package.json is missing "name" field') + } else if (!packageJson.name.startsWith(`${LIFEFORGE_SCOPE}/`)) { + errors.push(`Package name must start with "${LIFEFORGE_SCOPE}/"`) + } else { + const nameWithoutScope = packageJson.name.replace(`${LIFEFORGE_SCOPE}/`, '') + + if (!nameWithoutScope.includes('--')) { + errors.push( + 'Package name must follow format @lifeforge/--' + ) + } + } + + // Check version is semver + if (!packageJson.version) { + errors.push('package.json is missing "version" field') + } else if (!packageJson.version.match(/^\d+\.\d+\.\d+/)) { + errors.push('Version must be valid semver (e.g., 0.1.0)') + } + + // Check exports field + if (!packageJson.exports) { + errors.push('package.json is missing "exports" field') + } else { + if (!packageJson.exports['./manifest']) { + errors.push('exports must include "./manifest"') + } + } + + // Check manifest.ts exists + const manifestPath = path.join(modulePath, 'manifest.ts') + + if (!fs.existsSync(manifestPath)) { + errors.push('Missing manifest.ts') + } + + // Check client directory + const clientPath = path.join(modulePath, 'client') + + if (!fs.existsSync(clientPath)) { + warnings.push('No client/ directory found') + } + + // Check locales directory + const localesPath = path.join(modulePath, 'locales') + + if (!fs.existsSync(localesPath)) { + warnings.push('No locales/ directory found') + } + + // Check server if exports reference it + if (packageJson.exports?.['./server']) { + const serverIndexPath = path.join(modulePath, 'server', 'index.ts') + + if (!fs.existsSync(serverIndexPath)) { + errors.push('exports references "./server" but server/index.ts not found') + } + } + + if (packageJson.exports?.['./server/schema']) { + const schemaPath = path.join(modulePath, 'server', 'schema.ts') + + if (!fs.existsSync(schemaPath)) { + errors.push( + 'exports references "./server/schema" but server/schema.ts not found' + ) + } + } + + return { + valid: errors.length === 0, + errors, + warnings + } +} + +async function promptNpmLogin(): Promise { + CLILoggingService.info('You need to authenticate with the registry first.') + CLILoggingService.info( + 'The new authentication flow requires a browser login.' + ) + + const shouldLogin = await confirmAction( + 'Would you like to open the login page now?' + ) + + if (shouldLogin) { + openRegistryLogin() + + CLILoggingService.info('After logging in and copying your token, run:') + CLILoggingService.info(' bun forge modules login') + + return false // Return false to stop execution and let user run login command + } + + return false +} + +export async function publishModuleHandler( + folderName: string, + options?: { official?: boolean } +): Promise { + // Normalize folder name + const moduleName = folderName.replace(/^apps\//, '') + + const modulePath = path.join(process.cwd(), 'apps', moduleName) + + // Check module exists + if (!fs.existsSync(modulePath)) { CLILoggingService.actionableError( - `Module ${moduleName} is not installed.`, - `Available modules: ${availableModules.join(', ')}` + `Module "${moduleName}" not found in apps/`, + 'Make sure the module exists in the apps directory' ) process.exit(1) } - checkRunningPBInstances() - checkGitCleanliness(moduleName) - checkGithubCLI() + CLILoggingService.step(`Validating module "${moduleName}"...`) - const repoLink = createGithubRepo(moduleName) + // Validate structure + const validation = validateModuleStructure(modulePath) - replaceRepoWithSubmodule(moduleName, repoLink) + if (validation.warnings.length > 0) { + validation.warnings.forEach(warning => { + CLILoggingService.warn(` ⚠ ${warning}`) + }) + } + + if (!validation.valid) { + CLILoggingService.error('Module validation failed:') + validation.errors.forEach(error => { + CLILoggingService.error(` ✗ ${error}`) + }) + process.exit(1) + } + + CLILoggingService.success('Module structure is valid') + + // Check authentication + CLILoggingService.progress('Checking registry authentication...') + + let auth = await checkAuth() + + if (!auth.authenticated) { + const loggedIn = await promptNpmLogin() + + if (!loggedIn) { + CLILoggingService.actionableError( + 'Authentication required to publish', + `Run: bun forge modules login` + ) + process.exit(1) + } + + auth = await checkAuth() + + if (!auth.authenticated) { + CLILoggingService.error('Authentication failed') + process.exit(1) + } + } + + CLILoggingService.success(`Authenticated as ${auth.username}`) + + // Read package.json for display + const packageJson: PackageJson = JSON.parse( + fs.readFileSync(path.join(modulePath, 'package.json'), 'utf-8') + ) + + // Verify authenticated user matches package name prefix + const nameWithoutScope = (packageJson.name || '').replace( + `${LIFEFORGE_SCOPE}/`, + '' + ) + + const usernamePrefix = nameWithoutScope.split('--')[0] + + if (usernamePrefix && usernamePrefix !== auth.username) { + // Check if publishing as official module and prefix is lifeforge + if (options?.official && usernamePrefix === 'lifeforge') { + const isMaintainer = validateMaintainerAccess(auth.username || '') + + if (!isMaintainer) { + CLILoggingService.actionableError( + 'Maintainer access required', + 'You must have maintainer access to lifeforge-app/lifeforge to publish official modules' + ) + process.exit(1) + } + // Pass validation if maintainer + } else { + CLILoggingService.actionableError( + `Cannot publish as "${auth.username}" - package belongs to "${usernamePrefix}"`, + `You can only publish packages starting with @lifeforge/${auth.username}--` + ) + process.exit(1) + } + } + + CLILoggingService.info(`Package: ${packageJson.name}@${packageJson.version}`) + CLILoggingService.info(`Description: ${packageJson.description || '(none)'}`) + + // Confirm publish + const shouldPublish = await confirmAction( + `Publish ${packageJson.name}@${packageJson.version} to registry?` + ) + + if (!shouldPublish) { + CLILoggingService.info('Publish cancelled') + + return + } + + // Publish to registry + CLILoggingService.progress('Publishing to registry...') + + try { + executeCommand(`npm publish --registry ${getRegistryUrl()}`, { + cwd: modulePath, + stdio: 'inherit' + }) + + CLILoggingService.success( + `Published ${packageJson.name}@${packageJson.version} to registry!` + ) + CLILoggingService.info('') + CLILoggingService.info('Others can install with:') + CLILoggingService.info(` bun forge modules install ${packageJson.name}`) + } catch (error) { + CLILoggingService.error(`Publish failed: ${error}`) + process.exit(1) + } } diff --git a/tools/forgeCLI/src/commands/modules/handlers/remove-module.ts b/tools/forgeCLI/src/commands/modules/handlers/remove-module.ts deleted file mode 100644 index 04b2e97fb..000000000 --- a/tools/forgeCLI/src/commands/modules/handlers/remove-module.ts +++ /dev/null @@ -1,47 +0,0 @@ -import CLILoggingService from '@/utils/logging' - -import { removeModuleMigrations } from '../functions/migrations' -import { - removeModuleDirectory, - removeServerReferences -} from '../functions/module-lifecycle' -import { selectModuleToRemove } from '../functions/prompts' -import { moduleExists } from '../utils/file-system' - -export async function removeModuleHandler(moduleName?: string): Promise { - CLILoggingService.step('Starting module removal process') - - if (!moduleName) { - moduleName = await selectModuleToRemove() - } - - if (!moduleExists(moduleName)) { - CLILoggingService.actionableError( - `Module "${moduleName}" does not exist in workspace`, - 'Use "bun forge module list" to see available modules' - ) - process.exit(1) - } - - CLILoggingService.step(`Removing module: ${moduleName}`) - - try { - removeServerReferences(moduleName) - - await removeModuleMigrations(moduleName) - - removeModuleDirectory(moduleName) - - CLILoggingService.success(`Module "${moduleName}" removed successfully`) - CLILoggingService.info( - 'Restart the system with "bun forge dev" to see the changes' - ) - } catch (error) { - CLILoggingService.actionableError( - 'Module removal failed', - 'Check the error details above and ensure you have proper file permissions' - ) - CLILoggingService.debug(`Removal error: ${error}`) - process.exit(1) - } -} diff --git a/tools/forgeCLI/src/commands/modules/handlers/uninstall-module.ts b/tools/forgeCLI/src/commands/modules/handlers/uninstall-module.ts new file mode 100644 index 000000000..b3bcda676 --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/handlers/uninstall-module.ts @@ -0,0 +1,125 @@ +import fs from 'fs' +import path from 'path' + +import { executeCommand } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' + +import { generateModuleRegistries } from '../functions/registry/generator' + +interface PackageJson { + dependencies?: Record + [key: string]: unknown +} + +function extractModuleName(packageName: string): string { + // @lifeforge/lifeforge--calendar -> lifeforge--calendar + // @lifeforge/melvin--myapp -> melvin--myapp + return packageName.replace('@lifeforge/', '') +} + +function findModulePackageName( + shortName: string, + dependencies: Record +): string | null { + // Try to find the full package name from dependencies + for (const dep of Object.keys(dependencies)) { + if (dep.startsWith('@lifeforge/') && extractModuleName(dep) === shortName) { + return dep + } + } + + return null +} + +export async function uninstallModuleHandler( + moduleName: string +): Promise { + const rootPackageJsonPath = path.join(process.cwd(), 'package.json') + + const rootPackageJson: PackageJson = JSON.parse( + fs.readFileSync(rootPackageJsonPath, 'utf-8') + ) + + // Determine the full package name and short name + let fullPackageName: string + let shortName: string + + if (moduleName.startsWith('@lifeforge/')) { + fullPackageName = moduleName + shortName = extractModuleName(moduleName) + } else { + shortName = moduleName + + const found = findModulePackageName( + shortName, + rootPackageJson.dependencies || {} + ) + + if (!found) { + CLILoggingService.actionableError( + `Module "${shortName}" is not installed`, + 'Run "bun forge modules list" to see installed modules' + ) + + return + } + + fullPackageName = found + } + + const appsDir = path.join(process.cwd(), 'apps') + + const targetDir = path.join(appsDir, shortName) + + CLILoggingService.info(`Uninstalling module ${fullPackageName}...`) + + // Check if module exists in apps/ + if (!fs.existsSync(targetDir)) { + CLILoggingService.warn(`Module not found in apps/${shortName}`) + } else { + // Remove from apps/ + CLILoggingService.progress('Removing module files...') + fs.rmSync(targetDir, { recursive: true, force: true }) + CLILoggingService.success(`Removed apps/${shortName}`) + } + + // Remove from package.json + if (rootPackageJson.dependencies?.[fullPackageName]) { + CLILoggingService.progress('Updating package.json...') + delete rootPackageJson.dependencies[fullPackageName] + + fs.writeFileSync( + rootPackageJsonPath, + JSON.stringify(rootPackageJson, null, 2) + '\n' + ) + + CLILoggingService.success('Updated root package.json') + } + + // Remove from node_modules + const nodeModulesPath = path.join( + process.cwd(), + 'node_modules', + fullPackageName + ) + + if (fs.existsSync(nodeModulesPath)) { + fs.rmSync(nodeModulesPath, { recursive: true, force: true }) + } + + // Run bun install to clean up + CLILoggingService.progress('Cleaning up...') + + executeCommand('bun install', { + cwd: process.cwd(), + stdio: 'inherit' + }) + + // Regenerate module registries + CLILoggingService.progress('Regenerating module registries...') + generateModuleRegistries() + + CLILoggingService.success( + `Module ${fullPackageName} uninstalled successfully!` + ) +} diff --git a/tools/forgeCLI/src/commands/modules/handlers/update-module.ts b/tools/forgeCLI/src/commands/modules/handlers/update-module.ts deleted file mode 100644 index b3b0b581b..000000000 --- a/tools/forgeCLI/src/commands/modules/handlers/update-module.ts +++ /dev/null @@ -1,90 +0,0 @@ -import fs from 'fs' - -import { confirmAction, executeCommand } from '@/utils/helpers' -import CLILoggingService from '@/utils/logging' - -import { type CommitInfo, checkForUpdates } from '../functions/git' -import { getInstalledModules, moduleExists } from '../utils/file-system' - -async function updateSingleModule(moduleName: string): Promise { - CLILoggingService.step(`Checking for updates in module: ${moduleName}`) - - const availableUpdates: CommitInfo[] = await checkForUpdates(moduleName) - - if (availableUpdates.length === 0) { - CLILoggingService.info(`Module "${moduleName}" is already up to date`) - - return - } - - CLILoggingService.info( - `Found ${availableUpdates.length} new commit(s) for "${moduleName}":` - ) - CLILoggingService.newline() - - availableUpdates.forEach((commit, index) => { - console.log( - ` ${(index + 1).toString().padStart(2)}. ${commit.hash} - ${commit.message}` - ) - }) - - CLILoggingService.newline() - - const shouldUpdate = await confirmAction( - `Do you want to update module "${moduleName}"?` - ) - - if (!shouldUpdate) { - CLILoggingService.info(`Skipping update for module "${moduleName}"`) - - return - } - - CLILoggingService.progress(`Updating module: ${moduleName}`) - - try { - executeCommand( - `cd apps/${moduleName} && git pull origin main && bun install --linker isolated` - ) - - if (fs.existsSync(`apps/${moduleName}/server/schema.ts`)) { - executeCommand(`bun forge db push ${moduleName}`) - } - - CLILoggingService.success(`Successfully updated module: ${moduleName}`) - } catch (error) { - CLILoggingService.error(`Failed to update module "${moduleName}": ${error}`) - } -} - -export async function updateModuleHandler(moduleName?: string): Promise { - if (!moduleName) { - const modules = getInstalledModules() - - if (modules.length === 0) { - CLILoggingService.info('No modules installed to update') - - return - } - - for (const mod of modules) { - await updateSingleModule(mod) - - if (mod !== modules[modules.length - 1]) { - CLILoggingService.newline() - } - } - - return - } - - if (!moduleExists(moduleName)) { - CLILoggingService.actionableError( - `Module "${moduleName}" does not exist in workspace`, - 'Use "bun forge module list" to see available modules' - ) - process.exit(1) - } - - await updateSingleModule(moduleName) -} diff --git a/tools/forgeCLI/src/commands/modules/handlers/upgrade-module.ts b/tools/forgeCLI/src/commands/modules/handlers/upgrade-module.ts new file mode 100644 index 000000000..b73f1ba28 --- /dev/null +++ b/tools/forgeCLI/src/commands/modules/handlers/upgrade-module.ts @@ -0,0 +1,292 @@ +import fs from 'fs' +import path from 'path' + +import { confirmAction, executeCommand } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' + +import { + checkAuth, + getRegistryUrl, + openRegistryLogin +} from '../../../utils/registry' +import { generateModuleRegistries } from '../functions/registry/generator' + +const LIFEFORGE_SCOPE = '@lifeforge/' + +interface PackageJson { + name?: string + version?: string + dependencies?: Record + [key: string]: unknown +} + +function getInstalledModules(): { + name: string + version: string + folder: string +}[] { + const rootPackageJsonPath = path.join(process.cwd(), 'package.json') + + const rootPackageJson: PackageJson = JSON.parse( + fs.readFileSync(rootPackageJsonPath, 'utf-8') + ) + + const modules: { name: string; version: string; folder: string }[] = [] + + for (const [dep, version] of Object.entries( + rootPackageJson.dependencies || {} + )) { + if (dep.startsWith(LIFEFORGE_SCOPE) && version === 'workspace:*') { + // Get version from the module's package.json + const folderName = dep.replace(LIFEFORGE_SCOPE, '') + + const modulePath = path.join( + process.cwd(), + 'apps', + folderName, + 'package.json' + ) + + if (fs.existsSync(modulePath)) { + const modulePackageJson = JSON.parse( + fs.readFileSync(modulePath, 'utf-8') + ) + + modules.push({ + name: dep, + version: modulePackageJson.version || '0.0.0', + folder: folderName + }) + } + } + } + + return modules +} + +async function getLatestVersion(packageName: string): Promise { + const registry = getRegistryUrl() + + try { + // Query local Verdaccio registry using bun + const response = await fetch(`${registry}${packageName}`) + + // If unauthorized, check auth and prompt login + if (response.status === 401 || response.status === 403) { + const auth = await checkAuth() + + if (!auth.authenticated) { + CLILoggingService.info( + `Authentication required to check updates for ${packageName}` + ) + + const shouldLogin = await confirmAction( + 'Would you like to open the login page now?' + ) + + if (shouldLogin) { + openRegistryLogin() + CLILoggingService.info( + 'After logging in and copying your token, run: bun forge modules login' + ) + CLILoggingService.info('Then try upgrading again.') + + return null + } + } + } + + if (!response.ok) { + return null + } + + const data = (await response.json()) as { + 'dist-tags'?: { latest?: string } + } + + return data['dist-tags']?.latest || null + } catch { + return null + } +} + +function compareVersions(current: string, latest: string): number { + const currentParts = current.split('.').map(Number) + + const latestParts = latest.split('.').map(Number) + + for (let i = 0; i < 3; i++) { + const c = currentParts[i] || 0 + + const l = latestParts[i] || 0 + + if (l > c) { + return 1 + } + + if (l < c) { + return -1 + } + } + + return 0 +} + +async function upgradeModule( + packageName: string, + folder: string, + currentVersion: string +): Promise { + const latestVersion = await getLatestVersion(packageName) + + if (!latestVersion) { + CLILoggingService.warn(`Could not check registry for ${packageName}`) + + return false + } + + if (compareVersions(currentVersion, latestVersion) >= 0) { + CLILoggingService.info(`${packageName}@${currentVersion} is up to date`) + + return false + } + + CLILoggingService.info( + `Update available: ${packageName} ${currentVersion} → ${latestVersion}` + ) + + const shouldUpgrade = await confirmAction( + `Upgrade ${packageName}? This will replace your local copy.` + ) + + if (!shouldUpgrade) { + CLILoggingService.info(`Skipping ${packageName}`) + + return false + } + + const appsDir = path.join(process.cwd(), 'apps') + + const modulePath = path.join(appsDir, folder) + + const backupPath = path.join(appsDir, `${folder}.backup`) + + try { + // Backup current module + CLILoggingService.progress(`Backing up ${folder}...`) + + if (fs.existsSync(backupPath)) { + fs.rmSync(backupPath, { recursive: true, force: true }) + } + + fs.cpSync(modulePath, backupPath, { recursive: true }) + + // Remove current module + fs.rmSync(modulePath, { recursive: true, force: true }) + + // Fetch latest from registry + CLILoggingService.progress(`Fetching ${packageName}@${latestVersion}...`) + + executeCommand(`bun add ${packageName}@latest`, { + cwd: process.cwd(), + stdio: 'inherit' + }) + + // Find installed path in node_modules + const installedPath = path.join(process.cwd(), 'node_modules', packageName) + + if (!fs.existsSync(installedPath)) { + throw new Error(`Failed to fetch ${packageName} from registry`) + } + + // Copy to apps/ + fs.cpSync(installedPath, modulePath, { recursive: true }) + + // Remove node_modules copy so bun creates symlink + fs.rmSync(installedPath, { recursive: true, force: true }) + + // Run bun install + executeCommand('bun install', { + cwd: process.cwd(), + stdio: 'inherit' + }) + + // Remove backup on success + fs.rmSync(backupPath, { recursive: true, force: true }) + + CLILoggingService.success(`Upgraded ${packageName} to ${latestVersion}`) + + return true + } catch (error) { + CLILoggingService.error(`Failed to upgrade ${packageName}: ${error}`) + + // Restore from backup if exists + if (fs.existsSync(backupPath)) { + CLILoggingService.progress('Restoring from backup...') + + if (fs.existsSync(modulePath)) { + fs.rmSync(modulePath, { recursive: true, force: true }) + } + + fs.renameSync(backupPath, modulePath) + CLILoggingService.info('Restored previous version') + } + + return false + } +} + +export async function upgradeModuleHandler(moduleName?: string): Promise { + const modules = getInstalledModules() + + if (modules.length === 0) { + CLILoggingService.info('No @lifeforge/* modules installed') + + return + } + + let upgradedCount = 0 + + if (moduleName) { + // Upgrade specific module + const normalizedName = moduleName.startsWith(LIFEFORGE_SCOPE) + ? moduleName + : `${LIFEFORGE_SCOPE}${moduleName}` + + const mod = modules.find( + m => m.name === normalizedName || m.folder === moduleName + ) + + if (!mod) { + CLILoggingService.actionableError( + `Module "${moduleName}" not found`, + 'Run "bun forge modules list" to see installed modules' + ) + process.exit(1) + } + + const upgraded = await upgradeModule(mod.name, mod.folder, mod.version) + + if (upgraded) { + upgradedCount++ + } + } else { + // Check all modules for updates + CLILoggingService.step('Checking for updates...') + + for (const mod of modules) { + const upgraded = await upgradeModule(mod.name, mod.folder, mod.version) + + if (upgraded) { + upgradedCount++ + } + } + } + + if (upgradedCount > 0) { + CLILoggingService.progress('Regenerating registries...') + generateModuleRegistries() + + CLILoggingService.success(`Upgraded ${upgradedCount} module(s)`) + } +} diff --git a/tools/forgeCLI/src/commands/modules/index.ts b/tools/forgeCLI/src/commands/modules/index.ts index 65d098b2a..551ea7ec1 100644 --- a/tools/forgeCLI/src/commands/modules/index.ts +++ b/tools/forgeCLI/src/commands/modules/index.ts @@ -1,42 +1,53 @@ import type { Command } from 'commander' -import { addModuleHandler } from './handlers/add-module' import { createModuleHandler } from './handlers/create-module' +import { installModuleHandler } from './handlers/install-module' import { listModulesHandler } from './handlers/list-modules' +import { loginModuleHandler } from './handlers/login-module' +import { migrateModuleHandler } from './handlers/migrate-module' import { publishModuleHandler } from './handlers/publish-module' -import { removeModuleHandler } from './handlers/remove-module' -import { updateModuleHandler } from './handlers/update-module' +import { uninstallModuleHandler } from './handlers/uninstall-module' +import { upgradeModuleHandler } from './handlers/upgrade-module' export default function setup(program: Command): void { const command = program .command('modules') .description('Manage LifeForge modules') + command + .command('login') + .description('Login to the module registry') + .action(loginModuleHandler) + command .command('list') .description('List all installed modules') .action(listModulesHandler) + command - .command('add') - .description('Download and install a module') - .argument('', 'Module to add, e.g., lifeforge-app/wallet') - .action(addModuleHandler) - command - .command('update') - .description('Update an installed module') + .command('install') + .alias('i') + .description('Install a module from the LifeForge registry') .argument( - '[module]', - 'Module to update, e.g., wallet (optional, will update all if not provided)' + '', + 'Module to install, e.g., @lifeforge/lifeforge--calendar' ) - .action(updateModuleHandler) + .action(installModuleHandler) + command - .command('remove') - .description('Remove an installed module') - .argument( - '[module]', - 'Module to remove, e.g., wallet (optional, will show list if not provided)' - ) - .action(removeModuleHandler) + .command('uninstall') + .alias('un') + .description('Uninstall a module') + .argument('', 'Module to uninstall, e.g., achievements') + .action(uninstallModuleHandler) + + command + .command('upgrade') + .alias('up') + .description('Upgrade modules to latest version from registry') + .argument('[module]', 'Module to upgrade (optional, checks all if omitted)') + .action(upgradeModuleHandler) + command .command('create') .description('Create a new LifeForge module scaffold') @@ -45,9 +56,27 @@ export default function setup(program: Command): void { 'Name of the module to create. Leave empty to prompt.' ) .action(createModuleHandler) + command .command('publish') - .description('Publish a LifeForge module to your GitHub account') - .argument('', 'Unpublished installed module to publish') + .description('Publish a LifeForge module to the registry') + .argument('', 'Module to publish from apps/') + .option( + '--official', + 'Publish as official module (requires maintainer access)' + ) .action(publishModuleHandler) + + command + .command('migrate') + .description('Migrate legacy modules to the new package architecture') + .argument( + '[folder]', + 'Module folder name (optional, migrates all if omitted)' + ) + .option( + '--official', + 'Migrate as official module (requires maintainer access)' + ) + .action(migrateModuleHandler) } diff --git a/tools/forgeCLI/src/commands/modules/utils/ast-utils.ts b/tools/forgeCLI/src/commands/modules/utils/ast-utils.ts deleted file mode 100644 index 2cde1117d..000000000 --- a/tools/forgeCLI/src/commands/modules/utils/ast-utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as t from '@babel/types' - -/** - * AST manipulation utilities for Babel transformations - */ - -/** - * Creates a dynamic import expression for module loading - */ -export function createDynamicImport(modulePath: string): t.MemberExpression { - const awaitImport = t.awaitExpression( - t.callExpression(t.import(), [t.stringLiteral(modulePath)]) - ) - - return t.memberExpression(awaitImport, t.identifier('default')) -} diff --git a/tools/forgeCLI/src/commands/modules/utils/constants.ts b/tools/forgeCLI/src/commands/modules/utils/constants.ts deleted file mode 100644 index f4d01b05c..000000000 --- a/tools/forgeCLI/src/commands/modules/utils/constants.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Module installation configuration - */ -export interface ModuleInstallConfig { - tempDir: string - moduleDir: string - author: string - moduleName: string - repoUrl: string -} - - -/** - * Babel AST generation options - */ -export const AST_GENERATION_OPTIONS = { - retainLines: false, - compact: false, - jsescOption: { - quotes: 'single' as const - } -} as const diff --git a/tools/forgeCLI/src/commands/modules/utils/route-injection.ts b/tools/forgeCLI/src/commands/modules/utils/route-injection.ts deleted file mode 100644 index 83437a9e2..000000000 --- a/tools/forgeCLI/src/commands/modules/utils/route-injection.ts +++ /dev/null @@ -1,177 +0,0 @@ -import generate from '@babel/generator' -import { parse } from '@babel/parser' -import traverse from '@babel/traverse' -import * as t from '@babel/types' -import type { - ObjectExpression, - ObjectMethod, - ObjectProperty, - SpreadElement -} from '@babel/types' -import fs from 'fs' -import path from 'path' - -import initRouteAndSchemaFiles from '@/utils/initRouteAndSchemaFiles' -import CLILoggingService from '@/utils/logging' - -import { createDynamicImport } from './ast-utils' -import { AST_GENERATION_OPTIONS } from './constants' - -/** - * Route injection utilities for server configuration - */ - -/** - * Injects a module's server route import into the app.routes.ts file - */ -export function injectModuleRoute(moduleName: string): void { - const { appRoutesPath } = initRouteAndSchemaFiles() - - if (!fs.existsSync(appRoutesPath)) { - CLILoggingService.warn(`Routes config file not found at ${appRoutesPath}`) - - return - } - - const moduleServerPath = path.resolve(`apps/${moduleName}/server/index.ts`) - - if (!fs.existsSync(moduleServerPath)) { - CLILoggingService.info( - `No server entry file found for module "${moduleName}", skipping route injection` - ) - - return - } - - const routesContent = fs.readFileSync(appRoutesPath, 'utf8') - - try { - const ast = parse(routesContent, { - sourceType: 'module', - plugins: ['typescript'] - }) - - let routerObject: ObjectExpression | null = null - - // Find the forgeRouter call expression - traverse(ast, { - CallExpression(path) { - if ( - t.isIdentifier(path.node.callee, { name: 'forgeRouter' }) && - path.node.arguments.length > 0 && - t.isObjectExpression(path.node.arguments[0]) - ) { - routerObject = path.node.arguments[0] - } - } - }) - - // Check if module already exists and add if not - if (routerObject) { - const obj = routerObject as ObjectExpression - - const hasExistingProperty = obj.properties.some( - (prop: ObjectMethod | ObjectProperty | SpreadElement) => - t.isObjectProperty(prop) && - t.isIdentifier(prop.key) && - prop.key.name === moduleName - ) - - if (!hasExistingProperty) { - const moduleImport = createDynamicImport(`@lib/${moduleName}/server`) - - const newProperty = t.objectProperty( - t.identifier(moduleName), - moduleImport - ) - - obj.properties.push(newProperty) - } - } - - const { code } = generate(ast, AST_GENERATION_OPTIONS) - - fs.writeFileSync(appRoutesPath, code) - - CLILoggingService.info( - `Injected route for module "${moduleName}" into ${appRoutesPath}` - ) - } catch (error) { - CLILoggingService.error( - `Failed to inject route for module "${moduleName}": ${error}` - ) - } -} - -/** - * Removes a module's server route from the app.routes.ts file - */ -export function removeModuleRoute(moduleName: string): void { - const { appRoutesPath } = initRouteAndSchemaFiles() - - if (!fs.existsSync(appRoutesPath)) { - CLILoggingService.warn(`Routes config file not found at ${appRoutesPath}`) - - return - } - - const routesContent = fs.readFileSync(appRoutesPath, 'utf8') - - try { - const ast = parse(routesContent, { - sourceType: 'module', - plugins: ['typescript'] - }) - - let modified = false - - // Find and remove the module property from forgeRouter object - traverse(ast, { - CallExpression(path) { - if ( - t.isIdentifier(path.node.callee, { name: 'forgeRouter' }) && - path.node.arguments.length > 0 && - t.isObjectExpression(path.node.arguments[0]) - ) { - const routerObject = path.node.arguments[0] - - const originalLength = routerObject.properties.length - - routerObject.properties = routerObject.properties.filter(prop => { - if ( - t.isObjectProperty(prop) && - t.isIdentifier(prop.key) && - prop.key.name === moduleName - ) { - return false // Remove this property - } - - return true // Keep other properties - }) - - if (routerObject.properties.length < originalLength) { - modified = true - } - } - } - }) - - if (modified) { - const { code } = generate(ast, AST_GENERATION_OPTIONS) - - fs.writeFileSync(appRoutesPath, code) - - CLILoggingService.info( - `Removed route for module "${moduleName}" from ${appRoutesPath}` - ) - } else { - CLILoggingService.info( - `No route found for module "${moduleName}" in ${appRoutesPath}` - ) - } - } catch (error) { - CLILoggingService.error( - `Failed to remove route for module "${moduleName}": ${error}` - ) - } -} diff --git a/tools/forgeCLI/src/commands/modules/utils/schema-injection.ts b/tools/forgeCLI/src/commands/modules/utils/schema-injection.ts deleted file mode 100644 index dfe354c2c..000000000 --- a/tools/forgeCLI/src/commands/modules/utils/schema-injection.ts +++ /dev/null @@ -1,206 +0,0 @@ -import generate from '@babel/generator' -import { parse } from '@babel/parser' -import traverse from '@babel/traverse' -import * as t from '@babel/types' -import type { - ObjectExpression, - ObjectMethod, - ObjectProperty, - SpreadElement -} from '@babel/types' -import fs from 'fs' -import path from 'path' - -import initRouteAndSchemaFiles from '@/utils/initRouteAndSchemaFiles' -import CLILoggingService from '@/utils/logging' - -import { AST_GENERATION_OPTIONS } from './constants' - -/** - * Schema injection utilities for server configuration - */ - -/** - * Injects a module's schema import into the schema.ts file - */ -export function injectModuleSchema(moduleName: string): void { - const { schemaPath } = initRouteAndSchemaFiles() - - if (!fs.existsSync(schemaPath)) { - CLILoggingService.warn(`Schema config file not found at ${schemaPath}`) - - return - } - - // Check if module has a schema file first - const moduleSchemaPath = path.resolve(`apps/${moduleName}/server/schema.ts`) - - if (!fs.existsSync(moduleSchemaPath)) { - CLILoggingService.info( - `No schema file found for module "${moduleName}", skipping schema injection` - ) - - return - } - - const schemaContent = fs.readFileSync(schemaPath, 'utf8') - - try { - const ast = parse(schemaContent, { - sourceType: 'module', - plugins: ['typescript'] - }) - - let schemasObject: ObjectExpression | null = null - - // Find the SCHEMAS object - traverse(ast, { - VariableDeclarator(path) { - if ( - t.isIdentifier(path.node.id, { name: 'SCHEMAS' }) && - t.isObjectExpression(path.node.init) - ) { - schemasObject = path.node.init - } - } - }) - - if (schemasObject) { - const obj = schemasObject as ObjectExpression - - // Convert module name to snake_case for the key - const snakeCaseModuleName = moduleName - .replace(/([A-Z])/g, '_$1') - .toLowerCase() - .replace(/^_/, '') - - // Check if module is already imported - const hasExistingProperty = obj.properties.some( - (prop: ObjectMethod | ObjectProperty | SpreadElement) => - t.isObjectProperty(prop) && - t.isIdentifier(prop.key) && - prop.key.name === snakeCaseModuleName - ) - - if (!hasExistingProperty) { - const moduleImport = t.awaitExpression( - t.callExpression(t.import(), [ - t.stringLiteral(`@lib/${moduleName}/server/schema`) - ]) - ) - - const memberExpression = t.memberExpression( - moduleImport, - t.identifier('default') - ) - - const newProperty = t.objectProperty( - t.identifier(snakeCaseModuleName), - memberExpression - ) - - obj.properties.push(newProperty) - } - } - - const { code } = generate(ast, AST_GENERATION_OPTIONS) - - fs.writeFileSync(schemaPath, code) - - CLILoggingService.info( - `Injected schema for module "${moduleName}" into ${schemaPath}` - ) - } catch (error) { - CLILoggingService.error( - `Failed to inject schema for module "${moduleName}": ${error}` - ) - } -} - -/** - * Removes a module's schema from the schema.ts file - */ -export function removeModuleSchema(moduleName: string): void { - const { schemaPath } = initRouteAndSchemaFiles() - - if (!fs.existsSync(schemaPath)) { - CLILoggingService.warn(`Schema config file not found at ${schemaPath}`) - - return - } - - const schemaContent = fs.readFileSync(schemaPath, 'utf8') - - try { - const ast = parse(schemaContent, { - sourceType: 'module', - plugins: ['typescript'] - }) - - let modified = false - - // Find and remove the module property from the SCHEMAS object - traverse(ast, { - VariableDeclarator(path) { - if ( - t.isIdentifier(path.node.id, { name: 'SCHEMAS' }) && - t.isObjectExpression(path.node.init) - ) { - const objectExpression = path.node.init - - const originalLength = objectExpression.properties.length - - // Convert module name to snake_case for the key - const snakeCaseModuleName = moduleName - .replace(/([A-Z])/g, '_$1') - .toLowerCase() - .replace(/^_/, '') - - objectExpression.properties = objectExpression.properties.filter( - prop => { - if ( - t.isObjectProperty(prop) && - t.isIdentifier(prop.key) && - (prop.key.name === snakeCaseModuleName || - (t.isAwaitExpression(prop.value) && - t.isCallExpression(prop.value.argument) && - t.isImport(prop.value.argument.callee) && - prop.value.argument.arguments.length > 0 && - t.isStringLiteral(prop.value.argument.arguments[0]) && - prop.value.argument.arguments[0].value.includes( - moduleName - ))) - ) { - return false // Remove this property - } - - return true // Keep other properties - } - ) - - if (objectExpression.properties.length < originalLength) { - modified = true - } - } - } - }) - - if (modified) { - const { code } = generate(ast, AST_GENERATION_OPTIONS) - - fs.writeFileSync(schemaPath, code) - - CLILoggingService.info( - `Removed schema for module "${moduleName}" from ${schemaPath}` - ) - } else { - CLILoggingService.info( - `No schema found for module "${moduleName}" in ${schemaPath}` - ) - } - } catch (error) { - CLILoggingService.error( - `Failed to remove schema for module "${moduleName}": ${error}` - ) - } -} diff --git a/tools/forgeCLI/src/commands/modules/utils/validation.ts b/tools/forgeCLI/src/commands/modules/utils/validation.ts deleted file mode 100644 index 1ca9218db..000000000 --- a/tools/forgeCLI/src/commands/modules/utils/validation.ts +++ /dev/null @@ -1,39 +0,0 @@ -import CLILoggingService from '@/utils/logging' - -import type { ModuleInstallConfig } from './constants' - -/** - * Validation utilities for module operations - */ - -/** - * Validates module repository path format - */ -export function validateRepositoryPath(repoPath: string): boolean { - return /^[\w-]+\/[\w-]+$/.test(repoPath) -} - -/** - * Creates module installation configuration - */ -export function createModuleConfig(repoPath: string): ModuleInstallConfig { - const [author, moduleName] = repoPath.split('/') - - if (!moduleName.startsWith('lifeforge-module-')) { - CLILoggingService.error( - `Module name must start with 'lifeforge-module-'. Received: ${moduleName}` - ) - - process.exit(1) - } - - const finalModuleName = moduleName.replace(/^lifeforge-module-/, '') - - return { - tempDir: '.temp', - moduleDir: `apps/${finalModuleName}`, - author, - moduleName: finalModuleName, - repoUrl: `https://github.com/${author}/${moduleName}.git` - } -} diff --git a/tools/forgeCLI/src/templates/bare-bones/_tsconfig.json b/tools/forgeCLI/src/templates/bare-bones/_tsconfig.json new file mode 100644 index 000000000..6f0692c3d --- /dev/null +++ b/tools/forgeCLI/src/templates/bare-bones/_tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "./client/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "composite": false, + "paths": { + "@": [ + "./client/index" + ], + "@/*": [ + "./client/*" + ] + } + }, + "include": [ + "./manifest.ts" + ] +} \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/bare-bones/client/_tsconfig.json b/tools/forgeCLI/src/templates/bare-bones/client/_tsconfig.json index 851b6be91..e8064d794 100644 --- a/tools/forgeCLI/src/templates/bare-bones/client/_tsconfig.json +++ b/tools/forgeCLI/src/templates/bare-bones/client/_tsconfig.json @@ -3,7 +3,12 @@ // JSX and Language Settings "jsx": "react-jsx", "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2024.Object"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable", + "ES2024.Object" + ], // Module Resolution "module": "ESNext", "moduleResolution": "bundler", @@ -22,15 +27,23 @@ "noFallthroughCasesInSwitch": true, // Path Mapping "paths": { - "@": ["./src/index"], - "@/*": ["./src/*"], - "@server/*": ["../../../server/src/*"] + "@": [ + "./index" + ], + "@/*": [ + "./*" + ], + "@server/*": [ + "../../../server/src/*" + ] } }, - "include": ["./src/**/*", "./manifest.ts"], + "include": [ + "./**/*" + ], "references": [ { "path": "../../../server/tsconfig.json" } ] -} +} \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/bare-bones/client/src/index.tsx b/tools/forgeCLI/src/templates/bare-bones/client/index.tsx similarity index 100% rename from tools/forgeCLI/src/templates/bare-bones/client/src/index.tsx rename to tools/forgeCLI/src/templates/bare-bones/client/index.tsx diff --git a/tools/forgeCLI/src/templates/bare-bones/client/src/utils/forgeAPI.ts b/tools/forgeCLI/src/templates/bare-bones/client/utils/forgeAPI.ts similarity index 100% rename from tools/forgeCLI/src/templates/bare-bones/client/src/utils/forgeAPI.ts rename to tools/forgeCLI/src/templates/bare-bones/client/utils/forgeAPI.ts diff --git a/tools/forgeCLI/src/templates/bare-bones/client/src/vite-env.d.ts b/tools/forgeCLI/src/templates/bare-bones/client/vite-env.d.ts similarity index 100% rename from tools/forgeCLI/src/templates/bare-bones/client/src/vite-env.d.ts rename to tools/forgeCLI/src/templates/bare-bones/client/vite-env.d.ts diff --git a/tools/forgeCLI/src/templates/bare-bones/client/manifest.ts b/tools/forgeCLI/src/templates/bare-bones/manifest.ts similarity index 100% rename from tools/forgeCLI/src/templates/bare-bones/client/manifest.ts rename to tools/forgeCLI/src/templates/bare-bones/manifest.ts diff --git a/tools/forgeCLI/src/templates/bare-bones/package.json b/tools/forgeCLI/src/templates/bare-bones/package.json index 785be6e73..0452876c7 100644 --- a/tools/forgeCLI/src/templates/bare-bones/package.json +++ b/tools/forgeCLI/src/templates/bare-bones/package.json @@ -1,5 +1,5 @@ { - "name": "{{kebab moduleName.en}}", + "name": "@lifeforge/lifeforge--{{kebab moduleName.en}}", "version": "0.0.0", "description": "{{moduleDesc.en}}", "scripts": { @@ -7,23 +7,29 @@ }, "dependencies": { "@iconify/react": "^6.0.2", - "@tanstack/react-query": "^5.90.11", + "@tanstack/react-query": "^5.90.2", "@uidotdev/usehooks": "^2.4.1", "clsx": "^2.1.1", "dayjs": "^1.11.18", "lifeforge-ui": "workspace:*", - "lodash": "^4.17.21", "react": "^19.2.0", "react-i18next": "^15.1.1", "react-toastify": "^11.0.5", "shared": "workspace:*", + "tailwindcss": "^4.1.14", "vite": "^7.1.9", "zod": "^4.1.12" }, "devDependencies": { - "@types/lodash": "^4.17.21", "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "vite": "^7.1.9" + "@types/react-dom": "^19.2.0" + }, + "exports": { + "./server": "./server/index.ts", + "./manifest": { + "types": "./manifest.d.ts", + "default": "./manifest.ts" + }, + "./server/schema": "./server/schema.ts" } } \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/client-only/_tsconfig.json b/tools/forgeCLI/src/templates/client-only/_tsconfig.json new file mode 100644 index 000000000..6f0692c3d --- /dev/null +++ b/tools/forgeCLI/src/templates/client-only/_tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "./client/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "composite": false, + "paths": { + "@": [ + "./client/index" + ], + "@/*": [ + "./client/*" + ] + } + }, + "include": [ + "./manifest.ts" + ] +} \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/client-only/client/_tsconfig.json b/tools/forgeCLI/src/templates/client-only/client/_tsconfig.json index 851b6be91..e8064d794 100644 --- a/tools/forgeCLI/src/templates/client-only/client/_tsconfig.json +++ b/tools/forgeCLI/src/templates/client-only/client/_tsconfig.json @@ -3,7 +3,12 @@ // JSX and Language Settings "jsx": "react-jsx", "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2024.Object"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable", + "ES2024.Object" + ], // Module Resolution "module": "ESNext", "moduleResolution": "bundler", @@ -22,15 +27,23 @@ "noFallthroughCasesInSwitch": true, // Path Mapping "paths": { - "@": ["./src/index"], - "@/*": ["./src/*"], - "@server/*": ["../../../server/src/*"] + "@": [ + "./index" + ], + "@/*": [ + "./*" + ], + "@server/*": [ + "../../../server/src/*" + ] } }, - "include": ["./src/**/*", "./manifest.ts"], + "include": [ + "./**/*" + ], "references": [ { "path": "../../../server/tsconfig.json" } ] -} +} \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/client-only/client/src/index.tsx b/tools/forgeCLI/src/templates/client-only/client/index.tsx similarity index 100% rename from tools/forgeCLI/src/templates/client-only/client/src/index.tsx rename to tools/forgeCLI/src/templates/client-only/client/index.tsx diff --git a/tools/forgeCLI/src/templates/client-only/client/src/vite-env.d.ts b/tools/forgeCLI/src/templates/client-only/client/vite-env.d.ts similarity index 100% rename from tools/forgeCLI/src/templates/client-only/client/src/vite-env.d.ts rename to tools/forgeCLI/src/templates/client-only/client/vite-env.d.ts diff --git a/tools/forgeCLI/src/templates/client-only/client/manifest.ts b/tools/forgeCLI/src/templates/client-only/manifest.ts similarity index 100% rename from tools/forgeCLI/src/templates/client-only/client/manifest.ts rename to tools/forgeCLI/src/templates/client-only/manifest.ts diff --git a/tools/forgeCLI/src/templates/client-only/package.json b/tools/forgeCLI/src/templates/client-only/package.json index adc9fea94..c646189d2 100644 --- a/tools/forgeCLI/src/templates/client-only/package.json +++ b/tools/forgeCLI/src/templates/client-only/package.json @@ -1,29 +1,33 @@ { - "name": "{{kebab moduleName.en}}", - "version": "0.0.0", - "description": "{{moduleDesc.en}}", - "scripts": { - "types": "cd client && bun tsc" - }, - "dependencies": { - "@iconify/react": "^6.0.2", - "@tanstack/react-query": "^5.90.11", - "@uidotdev/usehooks": "^2.4.1", - "clsx": "^2.1.1", - "dayjs": "^1.11.18", - "lodash": "^4.17.21", - "lifeforge-ui": "workspace:*", - "react": "^19.2.0", - "react-i18next": "^15.1.1", - "react-toastify": "^11.0.5", - "shared": "workspace:*", - "vite": "^7.1.9", - "zod": "^4.1.12" - }, - "devDependencies": { - "@types/lodash": "^4.17.21", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "vite": "^7.1.9" - } + "name": "@lifeforge/lifeforge--{{kebab moduleName.en}}", + "version": "0.0.0", + "description": "{{moduleDesc.en}}", + "scripts": { + "types": "cd client && bun tsc" + }, + "dependencies": { + "@iconify/react": "^6.0.2", + "@tanstack/react-query": "^5.90.2", + "@uidotdev/usehooks": "^2.4.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.18", + "lifeforge-ui": "workspace:*", + "react": "^19.2.0", + "react-i18next": "^15.1.1", + "react-toastify": "^11.0.5", + "shared": "workspace:*", + "tailwindcss": "^4.1.14", + "vite": "^7.1.9", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0" + }, + "exports": { + "./manifest": { + "types": "./manifest.d.ts", + "default": "./manifest.ts" + } + } } \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/widget/.gitignore b/tools/forgeCLI/src/templates/widget/.gitignore deleted file mode 100644 index 4fc1232d9..000000000 --- a/tools/forgeCLI/src/templates/widget/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -node_modules -.env -dist -build -tsbuild -*.js -*.tsbuildinfo -.DS_Store -*.local -.DS_Store -.idea -.vscode/* -!.vscode/extensions.json -!.vscode/settings.json -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/widget/client/_tsconfig.json b/tools/forgeCLI/src/templates/widget/client/_tsconfig.json deleted file mode 100644 index 851b6be91..000000000 --- a/tools/forgeCLI/src/templates/widget/client/_tsconfig.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "compilerOptions": { - // JSX and Language Settings - "jsx": "react-jsx", - "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2024.Object"], - // Module Resolution - "module": "ESNext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "verbatimModuleSyntax": true, - // Build and Output - "composite": true, - "noEmit": true, - "skipLibCheck": true, - // Type Checking - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "useDefineForClassFields": true, - "noFallthroughCasesInSwitch": true, - // Path Mapping - "paths": { - "@": ["./src/index"], - "@/*": ["./src/*"], - "@server/*": ["../../../server/src/*"] - } - }, - "include": ["./src/**/*", "./manifest.ts"], - "references": [ - { - "path": "../../../server/tsconfig.json" - } - ] -} diff --git a/tools/forgeCLI/src/templates/widget/client/src/widgets/ExampleWidget.tsx b/tools/forgeCLI/src/templates/widget/client/src/widgets/ExampleWidget.tsx deleted file mode 100644 index 4ad195468..000000000 --- a/tools/forgeCLI/src/templates/widget/client/src/widgets/ExampleWidget.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Widget } from 'lifeforge-ui' -import type { WidgetConfig } from 'shared' - -function ExampleWidget() { - return ( - - Hello World! - - ) -} - -export default ExampleWidget - -export const config: WidgetConfig = { - namespace: 'apps.{{camel moduleName.en}}', - id: '{{kebab moduleName.en}}', - icon: '{{moduleIcon}}', - minH: 1, - minW: 1 -} diff --git a/tools/forgeCLI/src/templates/widget/locales/en.json b/tools/forgeCLI/src/templates/widget/locales/en.json deleted file mode 100644 index 8764405c0..000000000 --- a/tools/forgeCLI/src/templates/widget/locales/en.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "widgets": { - "{{camel moduleName.en}}": { - "title": "{{moduleName.en}}", - "description": "Widget for {{moduleDesc.en}}" - } - } -} diff --git a/tools/forgeCLI/src/templates/widget/locales/ms.json b/tools/forgeCLI/src/templates/widget/locales/ms.json deleted file mode 100644 index fcbe87561..000000000 --- a/tools/forgeCLI/src/templates/widget/locales/ms.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "widgets": { - "{{camel moduleName.en}}": { - "title": "{{moduleName.ms}}", - "description": "Widget untuk {{moduleDesc.ms}}" - } - } -} diff --git a/tools/forgeCLI/src/templates/widget/locales/zh-CN.json b/tools/forgeCLI/src/templates/widget/locales/zh-CN.json deleted file mode 100644 index 9316fe4b5..000000000 --- a/tools/forgeCLI/src/templates/widget/locales/zh-CN.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "widgets": { - "{{camel moduleName.en}}": { - "title": "{{moduleName.zhCN}}", - "description": "为{{moduleDesc.zhCN}}而量身定制的小部件" - } - } -} diff --git a/tools/forgeCLI/src/templates/widget/locales/zh-TW.json b/tools/forgeCLI/src/templates/widget/locales/zh-TW.json deleted file mode 100644 index 2d603850f..000000000 --- a/tools/forgeCLI/src/templates/widget/locales/zh-TW.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "widgets": { - "{{camel moduleName.en}}": { - "title": "{{moduleName.zhTW}}", - "description": "為{{moduleDesc.zhTW}}量身定制的小工具" - } - } -} diff --git a/tools/forgeCLI/src/templates/widget/package.json b/tools/forgeCLI/src/templates/widget/package.json deleted file mode 100644 index 785be6e73..000000000 --- a/tools/forgeCLI/src/templates/widget/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "{{kebab moduleName.en}}", - "version": "0.0.0", - "description": "{{moduleDesc.en}}", - "scripts": { - "types": "cd client && bun tsc" - }, - "dependencies": { - "@iconify/react": "^6.0.2", - "@tanstack/react-query": "^5.90.11", - "@uidotdev/usehooks": "^2.4.1", - "clsx": "^2.1.1", - "dayjs": "^1.11.18", - "lifeforge-ui": "workspace:*", - "lodash": "^4.17.21", - "react": "^19.2.0", - "react-i18next": "^15.1.1", - "react-toastify": "^11.0.5", - "shared": "workspace:*", - "vite": "^7.1.9", - "zod": "^4.1.12" - }, - "devDependencies": { - "@types/lodash": "^4.17.21", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "vite": "^7.1.9" - } -} \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/with-crud/_tsconfig.json b/tools/forgeCLI/src/templates/with-crud/_tsconfig.json new file mode 100644 index 000000000..6f0692c3d --- /dev/null +++ b/tools/forgeCLI/src/templates/with-crud/_tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "./client/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "composite": false, + "paths": { + "@": [ + "./client/index" + ], + "@/*": [ + "./client/*" + ] + } + }, + "include": [ + "./manifest.ts" + ] +} \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/with-crud/client/_tsconfig.json b/tools/forgeCLI/src/templates/with-crud/client/_tsconfig.json index 851b6be91..dbcc39350 100644 --- a/tools/forgeCLI/src/templates/with-crud/client/_tsconfig.json +++ b/tools/forgeCLI/src/templates/with-crud/client/_tsconfig.json @@ -3,7 +3,12 @@ // JSX and Language Settings "jsx": "react-jsx", "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2024.Object"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable", + "ES2024.Object" + ], // Module Resolution "module": "ESNext", "moduleResolution": "bundler", @@ -22,15 +27,23 @@ "noFallthroughCasesInSwitch": true, // Path Mapping "paths": { - "@": ["./src/index"], - "@/*": ["./src/*"], - "@server/*": ["../../../server/src/*"] + "@": [ + "./index" + ], + "@/*": [ + "./index/*" + ], + "@server/*": [ + "../../../server/src/*" + ] } }, - "include": ["./src/**/*", "./manifest.ts"], + "include": [ + "./**/*" + ], "references": [ { "path": "../../../server/tsconfig.json" } ] -} +} \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/with-crud/client/src/components/EntryItem.tsx b/tools/forgeCLI/src/templates/with-crud/client/components/EntryItem.tsx similarity index 100% rename from tools/forgeCLI/src/templates/with-crud/client/src/components/EntryItem.tsx rename to tools/forgeCLI/src/templates/with-crud/client/components/EntryItem.tsx diff --git a/tools/forgeCLI/src/templates/with-crud/client/src/components/ModifyEntryModal.tsx b/tools/forgeCLI/src/templates/with-crud/client/components/ModifyEntryModal.tsx similarity index 100% rename from tools/forgeCLI/src/templates/with-crud/client/src/components/ModifyEntryModal.tsx rename to tools/forgeCLI/src/templates/with-crud/client/components/ModifyEntryModal.tsx diff --git a/tools/forgeCLI/src/templates/with-crud/client/src/index.tsx b/tools/forgeCLI/src/templates/with-crud/client/index.tsx similarity index 100% rename from tools/forgeCLI/src/templates/with-crud/client/src/index.tsx rename to tools/forgeCLI/src/templates/with-crud/client/index.tsx diff --git a/tools/forgeCLI/src/templates/with-crud/client/src/utils/forgeAPI.ts b/tools/forgeCLI/src/templates/with-crud/client/utils/forgeAPI.ts similarity index 100% rename from tools/forgeCLI/src/templates/with-crud/client/src/utils/forgeAPI.ts rename to tools/forgeCLI/src/templates/with-crud/client/utils/forgeAPI.ts diff --git a/tools/forgeCLI/src/templates/widget/client/src/vite-env.d.ts b/tools/forgeCLI/src/templates/with-crud/client/vite-env.d.ts similarity index 100% rename from tools/forgeCLI/src/templates/widget/client/src/vite-env.d.ts rename to tools/forgeCLI/src/templates/with-crud/client/vite-env.d.ts diff --git a/tools/forgeCLI/src/templates/with-crud/client/manifest.ts b/tools/forgeCLI/src/templates/with-crud/manifest.ts similarity index 100% rename from tools/forgeCLI/src/templates/with-crud/client/manifest.ts rename to tools/forgeCLI/src/templates/with-crud/manifest.ts diff --git a/tools/forgeCLI/src/templates/with-crud/package.json b/tools/forgeCLI/src/templates/with-crud/package.json index 48057c379..eb9a89dad 100644 --- a/tools/forgeCLI/src/templates/with-crud/package.json +++ b/tools/forgeCLI/src/templates/with-crud/package.json @@ -1,29 +1,35 @@ { - "name": "{{kebab moduleName.en}}", - "version": "0.0.0", - "description": "Nice", - "scripts": { - "types": "cd client && bun tsc" - }, - "dependencies": { - "@iconify/react": "^6.0.2", - "@tanstack/react-query": "^5.90.11", - "@uidotdev/usehooks": "^2.4.1", - "clsx": "^2.1.1", - "dayjs": "^1.11.18", - "lodash": "^4.17.21", - "lifeforge-ui": "workspace:*", - "react": "^19.2.0", - "react-i18next": "^15.1.1", - "react-toastify": "^11.0.5", - "shared": "workspace:*", - "vite": "^7.1.9", - "zod": "^4.1.12" - }, - "devDependencies": { - "@types/lodash": "^4.17.21", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "vite": "^7.1.9" - } + "name": "@lifeforge/lifeforge--{{kebab moduleName.en}}", + "version": "0.0.0", + "description": "{{moduleDesc.en}}", + "scripts": { + "types": "cd client && bun tsc" + }, + "dependencies": { + "@iconify/react": "^6.0.2", + "@tanstack/react-query": "^5.90.2", + "@uidotdev/usehooks": "^2.4.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.18", + "lifeforge-ui": "workspace:*", + "react": "^19.2.0", + "react-i18next": "^15.1.1", + "react-toastify": "^11.0.5", + "shared": "workspace:*", + "tailwindcss": "^4.1.14", + "vite": "^7.1.9", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0" + }, + "exports": { + "./server": "./server/index.ts", + "./manifest": { + "types": "./manifest.d.ts", + "default": "./manifest.ts" + }, + "./server/schema": "./server/schema.ts" + } } \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/with-routes/_tsconfig.json b/tools/forgeCLI/src/templates/with-routes/_tsconfig.json new file mode 100644 index 000000000..6f0692c3d --- /dev/null +++ b/tools/forgeCLI/src/templates/with-routes/_tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "./client/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "composite": false, + "paths": { + "@": [ + "./client/index" + ], + "@/*": [ + "./client/*" + ] + } + }, + "include": [ + "./manifest.ts" + ] +} \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/with-routes/client/_tsconfig.json b/tools/forgeCLI/src/templates/with-routes/client/_tsconfig.json index 851b6be91..e8064d794 100644 --- a/tools/forgeCLI/src/templates/with-routes/client/_tsconfig.json +++ b/tools/forgeCLI/src/templates/with-routes/client/_tsconfig.json @@ -3,7 +3,12 @@ // JSX and Language Settings "jsx": "react-jsx", "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2024.Object"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable", + "ES2024.Object" + ], // Module Resolution "module": "ESNext", "moduleResolution": "bundler", @@ -22,15 +27,23 @@ "noFallthroughCasesInSwitch": true, // Path Mapping "paths": { - "@": ["./src/index"], - "@/*": ["./src/*"], - "@server/*": ["../../../server/src/*"] + "@": [ + "./index" + ], + "@/*": [ + "./*" + ], + "@server/*": [ + "../../../server/src/*" + ] } }, - "include": ["./src/**/*", "./manifest.ts"], + "include": [ + "./**/*" + ], "references": [ { "path": "../../../server/tsconfig.json" } ] -} +} \ No newline at end of file diff --git a/tools/forgeCLI/src/templates/with-routes/client/src/pages/EntryDetails/index.tsx b/tools/forgeCLI/src/templates/with-routes/client/pages/EntryDetails/index.tsx similarity index 100% rename from tools/forgeCLI/src/templates/with-routes/client/src/pages/EntryDetails/index.tsx rename to tools/forgeCLI/src/templates/with-routes/client/pages/EntryDetails/index.tsx diff --git a/tools/forgeCLI/src/templates/with-routes/client/src/pages/EntryList/index.tsx b/tools/forgeCLI/src/templates/with-routes/client/pages/EntryList/index.tsx similarity index 100% rename from tools/forgeCLI/src/templates/with-routes/client/src/pages/EntryList/index.tsx rename to tools/forgeCLI/src/templates/with-routes/client/pages/EntryList/index.tsx diff --git a/tools/forgeCLI/src/templates/with-routes/client/src/vite-env.d.ts b/tools/forgeCLI/src/templates/with-routes/client/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a..000000000 --- a/tools/forgeCLI/src/templates/with-routes/client/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/tools/forgeCLI/src/templates/with-routes/client/src/utils/forgeAPI.ts b/tools/forgeCLI/src/templates/with-routes/client/utils/forgeAPI.ts similarity index 100% rename from tools/forgeCLI/src/templates/with-routes/client/src/utils/forgeAPI.ts rename to tools/forgeCLI/src/templates/with-routes/client/utils/forgeAPI.ts diff --git a/tools/forgeCLI/src/templates/with-crud/client/src/vite-env.d.ts b/tools/forgeCLI/src/templates/with-routes/client/vite-env.d.ts similarity index 100% rename from tools/forgeCLI/src/templates/with-crud/client/src/vite-env.d.ts rename to tools/forgeCLI/src/templates/with-routes/client/vite-env.d.ts diff --git a/tools/forgeCLI/src/templates/with-routes/client/manifest.ts b/tools/forgeCLI/src/templates/with-routes/manifest.ts similarity index 100% rename from tools/forgeCLI/src/templates/with-routes/client/manifest.ts rename to tools/forgeCLI/src/templates/with-routes/manifest.ts diff --git a/tools/forgeCLI/src/templates/with-routes/package.json b/tools/forgeCLI/src/templates/with-routes/package.json index adc9fea94..eb9a89dad 100644 --- a/tools/forgeCLI/src/templates/with-routes/package.json +++ b/tools/forgeCLI/src/templates/with-routes/package.json @@ -1,29 +1,35 @@ { - "name": "{{kebab moduleName.en}}", - "version": "0.0.0", - "description": "{{moduleDesc.en}}", - "scripts": { - "types": "cd client && bun tsc" - }, - "dependencies": { - "@iconify/react": "^6.0.2", - "@tanstack/react-query": "^5.90.11", - "@uidotdev/usehooks": "^2.4.1", - "clsx": "^2.1.1", - "dayjs": "^1.11.18", - "lodash": "^4.17.21", - "lifeforge-ui": "workspace:*", - "react": "^19.2.0", - "react-i18next": "^15.1.1", - "react-toastify": "^11.0.5", - "shared": "workspace:*", - "vite": "^7.1.9", - "zod": "^4.1.12" - }, - "devDependencies": { - "@types/lodash": "^4.17.21", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "vite": "^7.1.9" - } + "name": "@lifeforge/lifeforge--{{kebab moduleName.en}}", + "version": "0.0.0", + "description": "{{moduleDesc.en}}", + "scripts": { + "types": "cd client && bun tsc" + }, + "dependencies": { + "@iconify/react": "^6.0.2", + "@tanstack/react-query": "^5.90.2", + "@uidotdev/usehooks": "^2.4.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.18", + "lifeforge-ui": "workspace:*", + "react": "^19.2.0", + "react-i18next": "^15.1.1", + "react-toastify": "^11.0.5", + "shared": "workspace:*", + "tailwindcss": "^4.1.14", + "vite": "^7.1.9", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0" + }, + "exports": { + "./server": "./server/index.ts", + "./manifest": { + "types": "./manifest.d.ts", + "default": "./manifest.ts" + }, + "./server/schema": "./server/schema.ts" + } } \ No newline at end of file diff --git a/tools/forgeCLI/src/utils/github-cli.ts b/tools/forgeCLI/src/utils/github-cli.ts new file mode 100644 index 000000000..a921b0854 --- /dev/null +++ b/tools/forgeCLI/src/utils/github-cli.ts @@ -0,0 +1,84 @@ +import { executeCommand } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' + +export function validateMaintainerAccess(username: string) { + try { + CLILoggingService.progress( + `Checking maintainer privileges for ${username}...` + ) + + // Check permission level on the official repo + const result = executeCommand( + `gh api repos/lifeforge-app/lifeforge/collaborators/${username}/permission`, + { stdio: 'pipe' } + ) + + const response = JSON.parse(result) as { + permission: string + user: { login: string } + } + + // Check if permission is admin, maintain, or write + const allowedPermissions = ['admin', 'maintain', 'write'] + + if (allowedPermissions.includes(response.permission)) { + CLILoggingService.success(`Verified maintainer access for ${username}`) + } + + CLILoggingService.warn( + 'Failed to verify maintainer access. Ensure you are authenticated with "gh auth login".' + ) + + process.exit(1) + } catch (error) { + CLILoggingService.actionableError( + `Failed to check maintainer access for ${username}.`, + `Error: ${error instanceof Error ? error.message : String(error)}` + ) + + process.exit(1) + } +} + +export function getGithubUser(): { name: string; email: string } | null { + try { + // Try getting basic user info first + const basicInfo = executeCommand('gh api user', { stdio: 'pipe' }) + + const user = JSON.parse(basicInfo) as { name: string; email: string | null } + + let email = user.email + + // If email is private/null, try fetching from /user/emails + if (!email) { + try { + const emailsJson = executeCommand('gh api user/emails', { + stdio: 'pipe' + }) + + const emails = JSON.parse(emailsJson) as Array<{ + email: string + primary: boolean + }> + + const primary = emails.find(e => e.primary) + + if (primary) { + email = primary.email + } + } catch { + // Ignore error if can't fetch emails, just fallback + } + } + + if (user.name && email) { + return { name: user.name, email } + } + + return null + } catch (error) { + CLILoggingService.debug(`Failed to fetch GitHub user info: ${error}`) + + return null + } +} diff --git a/tools/forgeCLI/src/utils/package.ts b/tools/forgeCLI/src/utils/package.ts new file mode 100644 index 000000000..91a6b2542 --- /dev/null +++ b/tools/forgeCLI/src/utils/package.ts @@ -0,0 +1,82 @@ +import fs from 'fs' +import path from 'path' + +import CLILoggingService from './logging' + +interface PackageJson { + name?: string + version?: string + dependencies?: Record + [key: string]: unknown +} + +export function readRootPackageJson(): PackageJson { + const rootPackageJsonPath = path.join(process.cwd(), 'package.json') + + return JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf-8')) +} + +export function writeRootPackageJson(packageJson: PackageJson): void { + CLILoggingService.debug(`Writing root package.json`) + + const rootPackageJsonPath = path.join(process.cwd(), 'package.json') + + fs.writeFileSync( + rootPackageJsonPath, + JSON.stringify(packageJson, null, 2) + '\n' + ) + + CLILoggingService.debug(`Wrote root package.json`) +} + +export function addWorkspaceDependency( + packageName: string, + version = 'workspace:*' +): void { + CLILoggingService.debug(`Adding workspace dependency: ${packageName}`) + + const packageJson = readRootPackageJson() + + if (!packageJson.dependencies) { + packageJson.dependencies = {} + } + + packageJson.dependencies[packageName] = version + + writeRootPackageJson(packageJson) + + CLILoggingService.debug(`Added workspace dependency: ${packageName}`) +} + +export function removeWorkspaceDependency(packageName: string): void { + CLILoggingService.debug(`Removing workspace dependency: ${packageName}`) + + const packageJson = readRootPackageJson() + + if (packageJson.dependencies?.[packageName]) { + delete packageJson.dependencies[packageName] + writeRootPackageJson(packageJson) + } + + CLILoggingService.debug(`Removed workspace dependency: ${packageName}`) +} + +export function findPackageName(name: string): string | null { + CLILoggingService.debug(`Finding package name: ${name}`) + + const packageJson = readRootPackageJson() + + const dependencies = packageJson.dependencies || {} + + for (const dep of Object.keys(dependencies)) { + if (dep === name) { + CLILoggingService.debug(`Found package name: ${name}`) + + return dep + } + } + + CLILoggingService.debug(`Package name not found: ${name}`) + + return null +} diff --git a/tools/forgeCLI/src/utils/registry.ts b/tools/forgeCLI/src/utils/registry.ts new file mode 100644 index 000000000..83d295340 --- /dev/null +++ b/tools/forgeCLI/src/utils/registry.ts @@ -0,0 +1,79 @@ +import fs from 'fs' +import path from 'path' + +import { executeCommand } from '@/utils/helpers' +import CLILoggingService from '@/utils/logging' + +export function getRegistryUrl(): string { + const bunfigPath = path.join(process.cwd(), 'bunfig.toml') + + if (fs.existsSync(bunfigPath)) { + const content = fs.readFileSync(bunfigPath, 'utf-8') + + const match = content.match(/"@lifeforge"\s*=\s*"([^"]+)"/) + + if (match) { + return match[1] + } + } + + return 'https://registry.lifeforge.dev/' +} + +export async function checkPackageExists( + packageName: string +): Promise { + const registry = getRegistryUrl() + + try { + executeCommand(`npm view ${packageName} --registry ${registry}`, { + cwd: process.cwd(), + stdio: 'pipe' + }) + + return true + } catch { + return false + } +} + +export async function checkAuth(): Promise<{ + authenticated: boolean + username?: string +}> { + const registry = getRegistryUrl() + + try { + const result = executeCommand( + `npm whoami --registry ${registry} 2>/dev/null`, + { + cwd: process.cwd(), + stdio: 'pipe' + } + ) + + const username = result?.toString().trim() + + if (username) { + return { authenticated: true, username } + } + + throw new Error('Not authenticated') + } catch { + CLILoggingService.warn('Not authenticated. Please login first.') + openRegistryLogin() + + process.exit(1) + } +} + +export function openRegistryLogin(): void { + const registry = getRegistryUrl() + + const loginUrl = registry.replace(/\/$/, '') + + executeCommand(`open "${loginUrl}"`, { + cwd: process.cwd(), + stdio: 'ignore' + }) +}