From 4a0ffcba1b85aa9ee0bf1316d2d98977a7b5efde Mon Sep 17 00:00:00 2001 From: Melvin Chia Date: Sun, 4 Jan 2026 20:47:10 +0800 Subject: [PATCH] refactor(forgeCLI): force myself to be a vibe code cleanup specialist --- tools/package.json | 2 +- .../handlers/createChangelogHandler.ts | 2 +- .../download-pocketbase.ts | 28 ++- .../database-initialization/migrations.ts | 27 --- .../database-initialization/superuser.ts | 22 +- .../migration-generation/applyMigrations.ts | 21 ++ .../migration-generation/buildIdToNameMap.ts | 51 ++++ .../createSingleMigration.ts | 71 ++++++ .../generateContent/skeleton.ts | 71 ++++++ .../generateContent/structure.ts | 134 +++++++++++ .../generateContent/views.ts | 52 ++++ .../migration-generation/migration-content.ts | 224 ------------------ .../migration-generation/migration-file.ts | 191 --------------- .../migration-generation/stageMigrations.ts | 64 +++++ .../buildModuleCollectionsMap.ts | 60 +++++ .../schema-generation/content-generator.ts | 95 -------- .../schema-generation/field-converter.ts | 96 -------- .../schema-generation/filterCollectionsMap.ts | 41 ++++ .../generateSchemaContent.ts | 79 ++++++ .../schema-generation/generateZodSchema.ts | 113 +++++++++ .../schema-generation/getCollectionsFromPB.ts | 41 ++++ .../schema-generation/listSchemaPaths.ts | 46 ++++ .../matchCollectionToModule.ts | 54 +++++ .../schema-generation/module-mapper.ts | 117 --------- .../schema-generation/schema-processor.ts | 80 ------- .../schema-generation/stripCollectionIds.ts | 68 ++++++ .../db/handlers/generateMigrationsHandler.ts | 155 +----------- .../db/handlers/generateSchemasHandler.ts | 102 ++++---- .../db/handlers/initializeDatabaseHandler.ts | 12 +- tools/src/commands/db/utils/constants.ts | 40 ---- tools/src/commands/db/utils/file-utils.ts | 4 +- tools/src/commands/db/utils/index.ts | 6 +- .../src/commands/db/utils/pocketbase-utils.ts | 10 +- tools/src/commands/dev/config/commands.ts | 11 +- .../commands/dev/functions/startServices.ts | 5 +- .../dev/functions/validateServices.ts | 3 - tools/src/commands/dev/handlers/devHandler.ts | 5 +- .../locales/functions/ensureLocaleNotInUse.ts | 20 +- .../locales/functions/getLocalesMeta.ts | 35 --- .../locales/functions/getPackagesToCheck.ts | 4 +- .../commands/locales/functions/getUpgrades.ts | 9 +- .../functions/installAndMoveLocales.ts | 15 +- .../locales/functions/setFirstLangInDB.ts | 4 +- ...tall-locale.ts => installLocaleHandler.ts} | 15 +- ...{list-locales.ts => listLocalesHandler.ts} | 6 +- ...lish-locale.ts => publishLocaleHandler.ts} | 14 +- ...ll-locale.ts => uninstallLocaleHandler.ts} | 19 +- ...ade-locale.ts => upgradeLocalesHandler.ts} | 21 +- tools/src/commands/locales/index.ts | 10 +- .../modules/functions/getFsMetadata.ts | 24 -- .../modules/functions/installModulePackage.ts | 14 +- .../functions/linkModuleToWorkspace.ts | 12 +- .../commands/modules/functions/listModules.ts | 4 +- .../namespaceUtils.ts => parsePackageName.ts} | 18 +- .../modules/functions/prompts/module-type.ts | 3 +- .../registry/generateSchemaRegistry.ts | 22 +- .../registry/generateServerRegistry.ts | 12 +- .../functions/templates/copy-template.ts | 8 - .../functions/templates/init-git-repo.ts | 5 +- .../modules/handlers/installModuleHandler.ts | 13 +- .../modules/handlers/listModuleHandler.ts | 6 +- .../modules/handlers/publishModuleHandler.ts | 15 +- .../handlers/uninstallModuleHandler.ts | 22 +- .../modules/handlers/upgradeModuleHandler.ts | 28 ++- .../functions/executeProjectCommand.ts | 17 +- .../functions/validateProjectArguments.ts | 4 +- tools/src/constants/constants.ts | 7 +- tools/src/index.ts | 2 +- tools/src/types/index.ts | 8 - tools/src/utils/commands.ts | 76 ++++++ tools/src/utils/github-cli.ts | 7 +- tools/src/utils/helpers.ts | 164 +------------ tools/src/utils/logging.ts | 124 +++++----- tools/src/utils/normalizePackage.ts | 65 +++++ tools/src/utils/pocketbase.ts | 76 ++++-- tools/src/utils/registry.ts | 5 +- 76 files changed, 1538 insertions(+), 1598 deletions(-) delete mode 100644 tools/src/commands/db/functions/database-initialization/migrations.ts create mode 100644 tools/src/commands/db/functions/migration-generation/applyMigrations.ts create mode 100644 tools/src/commands/db/functions/migration-generation/buildIdToNameMap.ts create mode 100644 tools/src/commands/db/functions/migration-generation/createSingleMigration.ts create mode 100644 tools/src/commands/db/functions/migration-generation/generateContent/skeleton.ts create mode 100644 tools/src/commands/db/functions/migration-generation/generateContent/structure.ts create mode 100644 tools/src/commands/db/functions/migration-generation/generateContent/views.ts delete mode 100644 tools/src/commands/db/functions/migration-generation/migration-content.ts delete mode 100644 tools/src/commands/db/functions/migration-generation/migration-file.ts create mode 100644 tools/src/commands/db/functions/migration-generation/stageMigrations.ts create mode 100644 tools/src/commands/db/functions/schema-generation/buildModuleCollectionsMap.ts delete mode 100644 tools/src/commands/db/functions/schema-generation/content-generator.ts delete mode 100644 tools/src/commands/db/functions/schema-generation/field-converter.ts create mode 100644 tools/src/commands/db/functions/schema-generation/filterCollectionsMap.ts create mode 100644 tools/src/commands/db/functions/schema-generation/generateSchemaContent.ts create mode 100644 tools/src/commands/db/functions/schema-generation/generateZodSchema.ts create mode 100644 tools/src/commands/db/functions/schema-generation/getCollectionsFromPB.ts create mode 100644 tools/src/commands/db/functions/schema-generation/listSchemaPaths.ts create mode 100644 tools/src/commands/db/functions/schema-generation/matchCollectionToModule.ts delete mode 100644 tools/src/commands/db/functions/schema-generation/module-mapper.ts delete mode 100644 tools/src/commands/db/functions/schema-generation/schema-processor.ts create mode 100644 tools/src/commands/db/functions/schema-generation/stripCollectionIds.ts delete mode 100644 tools/src/commands/locales/functions/getLocalesMeta.ts rename tools/src/commands/locales/handlers/{install-locale.ts => installLocaleHandler.ts} (63%) rename tools/src/commands/locales/handlers/{list-locales.ts => listLocalesHandler.ts} (80%) rename tools/src/commands/locales/handlers/{publish-locale.ts => publishLocaleHandler.ts} (68%) rename tools/src/commands/locales/handlers/{uninstall-locale.ts => uninstallLocaleHandler.ts} (55%) rename tools/src/commands/locales/handlers/{upgrade-locale.ts => upgradeLocalesHandler.ts} (57%) delete mode 100644 tools/src/commands/modules/functions/getFsMetadata.ts rename tools/src/commands/modules/functions/{registry/namespaceUtils.ts => parsePackageName.ts} (62%) create mode 100644 tools/src/utils/commands.ts create mode 100644 tools/src/utils/normalizePackage.ts diff --git a/tools/package.json b/tools/package.json index 668069834..9eeafad19 100644 --- a/tools/package.json +++ b/tools/package.json @@ -23,7 +23,7 @@ "prompts": "^2.4.2", "semver": "^7.7.3", "shared": "workspace:*", - "zod": "^4.3.4" + "zod": "^4.1.12" }, "devDependencies": { "@types/crypto-js": "^4.2.2", diff --git a/tools/src/commands/changelog/handlers/createChangelogHandler.ts b/tools/src/commands/changelog/handlers/createChangelogHandler.ts index 04ce02c36..a76f1dc76 100644 --- a/tools/src/commands/changelog/handlers/createChangelogHandler.ts +++ b/tools/src/commands/changelog/handlers/createChangelogHandler.ts @@ -51,5 +51,5 @@ export default function createChangelogHandler(year?: string, week?: string) { fs.writeFileSync(filePath, boilerPlate) - Logging.success(`Changelog file created at path: ${filePath}`) + Logging.success(`Created changelog file at path: ${filePath}`) } diff --git a/tools/src/commands/db/functions/database-initialization/download-pocketbase.ts b/tools/src/commands/db/functions/database-initialization/download-pocketbase.ts index 7b9cf0112..d92658222 100644 --- a/tools/src/commands/db/functions/database-initialization/download-pocketbase.ts +++ b/tools/src/commands/db/functions/database-initialization/download-pocketbase.ts @@ -1,10 +1,10 @@ -import chalk from 'chalk' import fs from 'fs' import os from 'os' import path from 'path' import { PB_BINARY_PATH, PB_DIR } from '@/constants/db' -import { executeCommand, isDockerMode } from '@/utils/helpers' +import executeCommand from '@/utils/commands' +import { isDockerMode } from '@/utils/helpers' import Logging from '@/utils/logging' const PB_VERSION = '0.35.0' @@ -27,9 +27,6 @@ export async function downloadPocketBaseBinary(): Promise { return } - Logging.step('PocketBase binary not found, downloading...') - - // Detect OS const platform = os.platform() let osName: string @@ -45,7 +42,10 @@ export async function downloadPocketBaseBinary(): Promise { osName = 'windows' break default: - Logging.error(`Unsupported platform: ${platform}`) + Logging.actionableError( + `Unsupported platform: ${platform}`, + 'PocketBase supports darwin, linux, and windows' + ) process.exit(1) } @@ -62,14 +62,17 @@ export async function downloadPocketBaseBinary(): Promise { archName = 'amd64' break default: - Logging.error(`Unsupported architecture: ${arch}`) + Logging.actionableError( + `Unsupported architecture: ${arch}`, + 'PocketBase supports arm64 and amd64' + ) process.exit(1) } const downloadUrl = `https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_${osName}_${archName}.zip` Logging.info( - `Downloading PocketBase v${PB_VERSION} for ${osName}/${archName}...` + `Downloading PocketBase ${Logging.highlight(`v${PB_VERSION}`)} for ${osName}/${archName}...` ) try { @@ -98,7 +101,7 @@ export async function downloadPocketBaseBinary(): Promise { // Clean up zip file and unnecessary files fs.unlinkSync(zipPath) - const changelogPath = path.join(PB_DIR, 'CHANGELOG.md') + const changelogPath = path.join(PB_DIR, 'CHANGELogging.md') const licensePath = path.join(PB_DIR, 'LICENSE.md') @@ -111,11 +114,12 @@ export async function downloadPocketBaseBinary(): Promise { } Logging.success( - `PocketBase v${PB_VERSION} downloaded to ${chalk.bold.blue(PB_BINARY_PATH)}` + `Downloaded PocketBase ${Logging.highlight(`v${PB_VERSION}`)}` ) } catch (error) { - Logging.error( - `Failed to download PocketBase: ${error instanceof Error ? error.message : 'Unknown error'}` + Logging.actionableError( + `Failed to download PocketBase: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'Check your internet connection and try again' ) process.exit(1) } diff --git a/tools/src/commands/db/functions/database-initialization/migrations.ts b/tools/src/commands/db/functions/database-initialization/migrations.ts deleted file mode 100644 index d5a15324b..000000000 --- a/tools/src/commands/db/functions/database-initialization/migrations.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { PB_BINARY_PATH, PB_KWARGS } from '@/constants/db' -import { executeCommand } from '@/utils/helpers' -import Logging from '@/utils/logging' - -/** - * Runs database migrations - */ -export function runDatabaseMigrations(): void { - try { - Logging.step('Migrating database schema to latest state...') - executeCommand(`bun forge db push`, { - stdio: ['pipe', 'pipe', 'pipe'] - }) - Logging.success('Initial migration generated successfully.') - executeCommand(`${PB_BINARY_PATH} migrate up ${PB_KWARGS.join(' ')}`, { - stdio: ['pipe', 'pipe', 'pipe'] - }) - Logging.success('Database schema migrated successfully.') - } catch (error) { - Logging.error( - `Failed to generate initial migration: ${ - error instanceof Error ? error.message : 'Unknown error' - }` - ) - process.exit(1) - } -} diff --git a/tools/src/commands/db/functions/database-initialization/superuser.ts b/tools/src/commands/db/functions/database-initialization/superuser.ts index 246b7848c..086e1c878 100644 --- a/tools/src/commands/db/functions/database-initialization/superuser.ts +++ b/tools/src/commands/db/functions/database-initialization/superuser.ts @@ -1,8 +1,7 @@ -import chalk from 'chalk' import fs from 'fs' import { PB_BINARY_PATH, PB_DATA_DIR, PB_KWARGS } from '@/constants/db' -import { executeCommand } from '@/utils/helpers' +import executeCommand from '@/utils/commands' import Logging from '@/utils/logging' /** @@ -15,7 +14,7 @@ export function validatePocketBaseNotInitialized( if (pbInitialized && exitOnFailure) { Logging.actionableError( - `PocketBase is already initialized in ${PB_DATA_DIR}, aborting.`, + `PocketBase is already initialized in ${Logging.highlight(PB_DATA_DIR)}, aborting.`, 'If you want to re-initialize, please remove the existing pb_data folder in the database directory.' ) process.exit(1) @@ -31,11 +30,9 @@ export function createPocketBaseSuperuser( email: string, password: string ): void { - try { - Logging.step( - `Initializing PocketBase database for ${chalk.bold.blue(email)}` - ) + Logging.debug(`Creating superuser with email ${Logging.highlight(email)}...`) + try { const result = executeCommand( `${PB_BINARY_PATH} superuser create ${PB_KWARGS.join(' ')}`, { @@ -48,14 +45,11 @@ export function createPocketBaseSuperuser( throw new Error(result.replace(/^Error:\s*/, '')) } - Logging.success( - 'PocketBase initialized and superuser created successfully.' - ) + Logging.success('Created superuser') } catch (error) { - Logging.error( - `Failed to create superuser: ${ - error instanceof Error ? error.message : 'Unknown error' - }` + Logging.actionableError( + `Failed to create superuser: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'Check your PocketBase configuration and try again' ) process.exit(1) } diff --git a/tools/src/commands/db/functions/migration-generation/applyMigrations.ts b/tools/src/commands/db/functions/migration-generation/applyMigrations.ts new file mode 100644 index 000000000..b019096fd --- /dev/null +++ b/tools/src/commands/db/functions/migration-generation/applyMigrations.ts @@ -0,0 +1,21 @@ +import { PB_BINARY_PATH, PB_KWARGS } from '@/constants/db' +import executeCommand from '@/utils/commands' +import Logging, { LEVEL_ORDER } from '@/utils/logging' + +/** + * Runs `pocketbase migrate up` to apply all pending migrations in the migrations directory. + * + * In debug mode, outputs the full migration log to the console. + * Otherwise, suppresses output for cleaner logs. + * + * @throws Error if the migration command fails + */ +export default function applyStagedMigration(): void { + Logging.debug('Applying pending migrations...') + + executeCommand(`${PB_BINARY_PATH} migrate up ${PB_KWARGS.join(' ')}`, { + stdio: Logging.level > LEVEL_ORDER.debug ? 'pipe' : 'inherit' + }) + + Logging.debug('Migrations applied successfully') +} diff --git a/tools/src/commands/db/functions/migration-generation/buildIdToNameMap.ts b/tools/src/commands/db/functions/migration-generation/buildIdToNameMap.ts new file mode 100644 index 000000000..30c3d3e45 --- /dev/null +++ b/tools/src/commands/db/functions/migration-generation/buildIdToNameMap.ts @@ -0,0 +1,51 @@ +import path from 'path' + +import Logging from '@/utils/logging' + +import { getSchemaFiles } from '../../utils' + +/** + * Builds a mapping from PocketBase collection IDs to their human-readable names. + * + * This map is essential for resolving relation fields during migration generation, + * as relations store collection IDs but we need to reference collections by name + * in the generated `schema.ts` for portability across environments. + * + * @param targetModule - Optional module name to filter schemas. If omitted, processes all modules. + * @returns A Map where keys are collection IDs and values are collection names + * + * @example + * const idMap = await buildIdToNameMap() + * // Map { 'abc123' => 'users', 'def456' => 'posts' } + */ +export default async function buildIdToNameMap( + targetModule?: string +): Promise> { + const schemaFiles = getSchemaFiles(targetModule) + + const idToNameMap = new Map() + + for (const schemaPath of schemaFiles) { + const schema: Record< + string, + { + raw: { + id: string + name: string + } + } + > = await import(path.resolve(schemaPath)) + + for (const entry of Object.values(schema)) { + const raw = entry?.raw + + if (raw?.id && raw?.name) { + idToNameMap.set(raw.id as string, raw.name as string) + } + } + } + + Logging.debug(`Built ID-to-name map with ${idToNameMap.size} collections`) + + return idToNameMap +} diff --git a/tools/src/commands/db/functions/migration-generation/createSingleMigration.ts b/tools/src/commands/db/functions/migration-generation/createSingleMigration.ts new file mode 100644 index 000000000..5f1f10fd3 --- /dev/null +++ b/tools/src/commands/db/functions/migration-generation/createSingleMigration.ts @@ -0,0 +1,71 @@ +import { execSync } from 'child_process' +import fs from 'fs' +import prettier from 'prettier' + +import { PB_BINARY_PATH, PB_KWARGS } from '@/constants/db' + +import { PRETTIER_OPTIONS } from '../../utils' + +/** + * Creates a single PocketBase migration file with custom up migration content. + * + * This function: + * 1. Uses PocketBase CLI to generate a new migration file scaffold + * 2. Parses the output to find the created file path + * 3. Replaces the placeholder comments with the provided migration content + * 4. Formats the file with Prettier for consistent code style + * + * Down migrations are intentionally left as comments since automatic rollback + * could cause data loss - users should manually handle rollbacks. + * + * @param name - Name for the migration file (e.g., 'skeleton_users', 'structure_posts') + * @param upContent - JavaScript code to execute during `migrate up`. If empty, skips creation. + * + * @throws Error if PocketBase CLI fails to create the migration file + * @throws Error if the created file path cannot be parsed from the CLI output + * @throws Error if the created file doesn't exist at the expected path + */ +export default async function createSingleMigration( + name: string, + upContent: string +): Promise { + if (!upContent) { + return + } + + const response = execSync( + `${PB_BINARY_PATH} migrate create ${name} ${PB_KWARGS.join(' ')}`, + { + input: 'y\n', + stdio: ['pipe', 'pipe', 'pipe'] + } + ) + + const resString = response.toString() + + const match = resString.match(/Successfully created file "(.*)"/) + + if (!match || match.length < 2) { + throw new Error('Failed to parse migration file path from response.') + } + + const migrationFilePath = match[1] + + if (!fs.existsSync(migrationFilePath)) { + throw new Error(`Migration file not found at path: ${migrationFilePath}`) + } + + let content = fs.readFileSync(migrationFilePath, 'utf-8') + + content = content + .replace('../pb_data/types.d.ts', '../types.d.ts') + .replace('// add up queries...', upContent) + .replace( + '// add down queries...', + '// Users need to manually undo changes to prevent data loss' + ) + + const formattedContent = await prettier.format(content, PRETTIER_OPTIONS) + + fs.writeFileSync(migrationFilePath, formattedContent, 'utf-8') +} diff --git a/tools/src/commands/db/functions/migration-generation/generateContent/skeleton.ts b/tools/src/commands/db/functions/migration-generation/generateContent/skeleton.ts new file mode 100644 index 000000000..b1dcedbf9 --- /dev/null +++ b/tools/src/commands/db/functions/migration-generation/generateContent/skeleton.ts @@ -0,0 +1,71 @@ +/** + * Generates skeleton migration content for the first phase of migration. + * + * Skeleton migrations create minimal stub collections that only include: + * - Collection name and type + * - Access rules (list, view, create, update, delete) + * - Placeholder viewQuery for view collections + * + * This phase runs first to ensure all collections exist before the structure + * phase attempts to create relations between them. Without skeleton collections, + * relations would fail because the target collection doesn't exist yet. If the + * collection already exists, the migration will be skipped. + * + * @param schema - Module schema with collection definitions + * @returns JavaScript code string for the up migration + * + * @example + * // Generated migration creates collections only if they don't exist: + * // let usersExisting = null; + * // try { usersExisting = app.findCollectionByNameOrId('users'); } catch (e) {} + * // if (!usersExisting) { app.importCollections([usersStub], false); } + */ +export default function generateContent( + schema: Record }> +): string { + const lines: string[] = [] + + lines.push('// Create skeleton collections (only if they do not exist)') + + for (const [name, { raw }] of Object.entries(schema)) { + const collectionName = raw.name as string + + lines.push(` + // ${collectionName} + let ${name}Existing = null; + try { + ${name}Existing = app.findCollectionByNameOrId('${collectionName}'); + } catch (e) { + // Collection doesn't exist yet + } + + if (!${name}Existing) {`) + + const stubCollection: Record = { + name: collectionName, + type: raw.type || 'base', + listRule: raw.listRule, + viewRule: raw.viewRule, + createRule: raw.createRule, + updateRule: raw.updateRule, + deleteRule: raw.deleteRule + } + + // View collections require a viewQuery + if (raw.type === 'view') { + // Use placeholder query with required id column - real query will be set in structure migration + stubCollection.viewQuery = 'SELECT (ROW_NUMBER() OVER()) as id' + } + + lines.push( + ` const ${name}Stub = ${JSON.stringify(stubCollection, null, 2) + .split('\n') + .map((l, i) => (i === 0 ? l : ' ' + l)) + .join('\n')};` + ) + lines.push(` app.importCollections([${name}Stub], false);`) + lines.push(` }`) + } + + return lines.join('\n') +} diff --git a/tools/src/commands/db/functions/migration-generation/generateContent/structure.ts b/tools/src/commands/db/functions/migration-generation/generateContent/structure.ts new file mode 100644 index 000000000..cc9b5ffe6 --- /dev/null +++ b/tools/src/commands/db/functions/migration-generation/generateContent/structure.ts @@ -0,0 +1,134 @@ +/** + * Removes auto-generated IDs and timestamps from a raw collection config. + * + * PocketBase generates unique IDs for collections and fields, but these + * aren't portable across environments. By stripping them, the migration + * code works consistently regardless of the source database. + * + * Normally in generated `schema.ts`, these fields are already stripped. + * But in case the schema is generated from a different source, this serves + * as another layer of safety to ensure the migration code works. + * + * @param raw - The raw collection configuration from the schema + * @returns Cleaned config without id, created, updated fields + */ +function stripIdsFromRaw( + raw: Record +): Record { + const cleaned = { ...raw } + + delete cleaned.id + delete cleaned.created + delete cleaned.updated + + if (cleaned.fields && Array.isArray(cleaned.fields)) { + cleaned.fields = cleaned.fields.map((field: Record) => { + const cleanedField = { ...field } + + delete cleanedField.id + + return cleanedField + }) + } + + return cleaned +} + +/** + * Extracts relation fields from a collection that need dynamic collectionId resolution. + * + * Relation fields store the target collection's ID, but IDs aren't portable. + * This function identifies which fields need their collectionId resolved + * at migration runtime using `findCollectionByNameOrId`. + * + * @param raw - The raw collection configuration + * @returns Array of relation field info with field name and target collection name + */ +function extractRelationFields( + raw: Record +): Array<{ fieldName: string; collectionName: string }> { + const relations: Array<{ fieldName: string; collectionName: string }> = [] + + const fields = raw.fields as Array> | undefined + + if (fields) { + for (const field of fields) { + if (field.type === 'relation' && field.collectionId) { + relations.push({ + fieldName: field.name as string, + collectionName: field.collectionId as string // Already converted to name in schema + }) + } + } + } + + return relations +} + +/** + * Generates structure migration content for the second phase of migration. + * + * Structure migrations update existing collections with their full schema including: + * - All field definitions (text, number, relation, file, etc.) + * - Field options and validation rules + * - Dynamically resolved relation collectionIds using `findCollectionByNameOrId` + * + * View collections are EXCLUDED from this phase because their viewQuery may + * reference other collections that aren't fully set up yet. Views are handled + * in the separate views phase. + * + * @param schema - Module schema with collection definitions + * @param idToNameMap - Map of collection IDs to names for resolving relations + * @returns JavaScript code string for the up migration + */ +export default function generateContent( + schema: Record }>, + idToNameMap: Map +): string { + // Filter out view collections - they are handled in view query migration + const nonViewCollections = Object.entries(schema).filter( + ([, { raw }]) => raw.type !== 'view' + ) + + const lines: string[] = [] + + lines.push('// Update collections with full schema (excluding views)') + + for (const [name, { raw }] of nonViewCollections) { + const cleanedRaw = stripIdsFromRaw(raw) + + const relations = extractRelationFields(cleanedRaw) + + lines.push('') + lines.push(` // Get existing collection and update it`) + lines.push( + ` const ${name}Existing = app.findCollectionByNameOrId('${raw.name}');` + ) + lines.push( + ` const ${name}Collection = ${JSON.stringify(cleanedRaw, null, 2) + .split('\n') + .map((l, i) => (i === 0 ? l : ' ' + l)) + .join('\n')};` + ) + + // For each relation field, resolve the collectionId dynamically using NAME not ID + for (const { fieldName, collectionName } of relations) { + // Convert ID to name if needed + const resolvedName = idToNameMap.get(collectionName) || collectionName + + lines.push( + ` const ${name}_${fieldName}_rel = app.findCollectionByNameOrId('${resolvedName}');` + ) + lines.push( + ` ${name}Collection.fields.find(f => f.name === '${fieldName}').collectionId = ${name}_${fieldName}_rel.id;` + ) + } + + lines.push(` ${name}Collection.id = ${name}Existing.id;`) + lines.push(` app.importCollections([${name}Collection], false);`) + } + + const upContent = lines.join('\n') + + return upContent +} diff --git a/tools/src/commands/db/functions/migration-generation/generateContent/views.ts b/tools/src/commands/db/functions/migration-generation/generateContent/views.ts new file mode 100644 index 000000000..8153dd6a0 --- /dev/null +++ b/tools/src/commands/db/functions/migration-generation/generateContent/views.ts @@ -0,0 +1,52 @@ +/** + * Generates view query migration content for the third phase of migration. + * + * View collections in PocketBase are defined by SQL queries that reference + * other collections. This phase runs LAST because: + * 1. All referenced collections must exist (created in skeleton phase) + * 2. All fields must be defined (set in structure phase) + * 3. The viewQuery can now safely reference any table/field + * + * During skeleton phase, views are created with a placeholder query: + * `SELECT (ROW_NUMBER() OVER()) as id` + * + * This phase replaces that placeholder with the actual viewQuery from the schema. + * + * @param schema - Module schema with collection definitions + * @returns JavaScript code string for the up migration, or empty string if no views + * + * @example + * // For a view collection 'users_aggregated': + * // const users_aggregated = app.findCollectionByNameOrId('users_aggregated'); + * // users_aggregated.viewQuery = 'SELECT ... FROM users GROUP BY ...'; + * // app.save(users_aggregated); + */ +export default function generateContent( + schema: Record }> +): string { + const viewCollections = Object.entries(schema).filter( + ([, { raw }]) => raw.type === 'view' + ) + + if (viewCollections.length === 0) { + return '' + } + + const lines: string[] = [] + + lines.push('// Update view collections with their actual viewQuery') + + for (const [name, { raw }] of viewCollections) { + lines.push('') + lines.push(` // ${raw.name}`) + lines.push( + ` const ${name}Collection = app.findCollectionByNameOrId('${raw.name}');` + ) + lines.push( + ` ${name}Collection.viewQuery = ${JSON.stringify(raw.viewQuery)};` + ) + lines.push(` app.save(${name}Collection);`) + } + + return lines.join('\n') +} diff --git a/tools/src/commands/db/functions/migration-generation/migration-content.ts b/tools/src/commands/db/functions/migration-generation/migration-content.ts deleted file mode 100644 index 7e9b0f55a..000000000 --- a/tools/src/commands/db/functions/migration-generation/migration-content.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Generates skeleton migration content - creates stub collections if they don't exist - */ -export function generateSkeletonMigrationContent( - schema: Record }> -): { - upContent: string - downContent: string -} { - const lines: string[] = [] - - lines.push('// Create skeleton collections (only if they do not exist)') - - for (const [name, { raw }] of Object.entries(schema)) { - const collectionName = raw.name as string - - lines.push('') - lines.push(` // ${collectionName}`) - lines.push(` let ${name}Existing = null;`) - lines.push(` try {`) - lines.push( - ` ${name}Existing = app.findCollectionByNameOrId('${collectionName}');` - ) - lines.push(` } catch (e) {`) - lines.push(` // Collection doesn't exist yet`) - lines.push(` }`) - lines.push('') - lines.push(` if (!${name}Existing) {`) - - const stubCollection: Record = { - name: collectionName, - type: raw.type || 'base', - listRule: raw.listRule, - viewRule: raw.viewRule, - createRule: raw.createRule, - updateRule: raw.updateRule, - deleteRule: raw.deleteRule - } - - // View collections require a viewQuery - if (raw.type === 'view') { - // Use placeholder query with required id column - real query will be set in structure migration - stubCollection.viewQuery = 'SELECT (ROW_NUMBER() OVER()) as id' - } - - lines.push( - ` const ${name}Stub = ${JSON.stringify(stubCollection, null, 2) - .split('\n') - .map((l, i) => (i === 0 ? l : ' ' + l)) - .join('\n')};` - ) - lines.push(` app.importCollections([${name}Stub], false);`) - lines.push(` }`) - } - - const upContent = lines.join('\n') - - // Down migration: delete only if we created them (no-op for safety) - const downContent = '// Skeleton collections are not deleted to preserve data' - - return { upContent, downContent } -} - -/** - * Strips IDs from raw collection config - */ -function stripIdsFromRaw( - raw: Record -): Record { - const cleaned = { ...raw } - - delete cleaned.id - delete cleaned.created - delete cleaned.updated - - if (cleaned.fields && Array.isArray(cleaned.fields)) { - cleaned.fields = cleaned.fields.map((field: Record) => { - const cleanedField = { ...field } - - delete cleanedField.id - - return cleanedField - }) - } - - return cleaned -} - -/** - * Extracts relation fields that need dynamic collectionId resolution - */ -function extractRelationFields( - raw: Record -): Array<{ fieldName: string; collectionName: string }> { - const relations: Array<{ fieldName: string; collectionName: string }> = [] - - const fields = raw.fields as Array> | undefined - - if (fields) { - for (const field of fields) { - if (field.type === 'relation' && field.collectionId) { - relations.push({ - fieldName: field.name as string, - collectionName: field.collectionId as string // Already converted to name in schema - }) - } - } - } - - return relations -} - -/** - * Generates structure migration content - updates collections with full schema - * Uses findCollectionByNameOrId to resolve relation collectionIds dynamically - * NOTE: View collections are excluded - they are handled separately in view query migration - */ -export function generateStructureMigrationContent( - schema: Record }>, - idToNameMap: Map -): { - upContent: string - downContent: string -} { - // Filter out view collections - they are handled in view query migration - const nonViewCollections = Object.entries(schema).filter( - ([, { raw }]) => raw.type !== 'view' - ) - - const lines: string[] = [] - - lines.push('// Update collections with full schema (excluding views)') - - for (const [name, { raw }] of nonViewCollections) { - const cleanedRaw = stripIdsFromRaw(raw) - - const relations = extractRelationFields(cleanedRaw) - - lines.push('') - lines.push(` // Get existing collection and update it`) - lines.push( - ` const ${name}Existing = app.findCollectionByNameOrId('${raw.name}');` - ) - lines.push( - ` const ${name}Collection = ${JSON.stringify(cleanedRaw, null, 2) - .split('\n') - .map((l, i) => (i === 0 ? l : ' ' + l)) - .join('\n')};` - ) - - // For each relation field, resolve the collectionId dynamically using NAME not ID - for (const { fieldName, collectionName } of relations) { - // Convert ID to name if needed - const resolvedName = idToNameMap.get(collectionName) || collectionName - - lines.push( - ` const ${name}_${fieldName}_rel = app.findCollectionByNameOrId('${resolvedName}');` - ) - lines.push( - ` ${name}Collection.fields.find(f => f.name === '${fieldName}').collectionId = ${name}_${fieldName}_rel.id;` - ) - } - - lines.push(` ${name}Collection.id = ${name}Existing.id;`) - lines.push(` app.importCollections([${name}Collection], false);`) - } - - const upContent = lines.join('\n') - - const downContent = nonViewCollections - .map(([, { raw }]) => { - return ` const ${raw.name}Collection = app.findCollectionByNameOrId("${raw.name}");\n app.delete(${raw.name}Collection);` - }) - .join('\n\n') - - return { upContent, downContent } -} - -/** - * Generates view query migration content - updates view collections with their actual viewQuery - * This must run AFTER all normal collection structures are in place - */ -export function generateViewQueryMigrationContent( - schema: Record }> -): { - upContent: string - downContent: string - hasViews: boolean -} { - const viewCollections = Object.entries(schema).filter( - ([, { raw }]) => raw.type === 'view' - ) - - if (viewCollections.length === 0) { - return { upContent: '', downContent: '', hasViews: false } - } - - const lines: string[] = [] - - lines.push('// Update view collections with their actual viewQuery') - - for (const [name, { raw }] of viewCollections) { - lines.push('') - lines.push(` // ${raw.name}`) - lines.push( - ` const ${name}Collection = app.findCollectionByNameOrId('${raw.name}');` - ) - lines.push( - ` ${name}Collection.viewQuery = ${JSON.stringify(raw.viewQuery)};` - ) - lines.push(` app.save(${name}Collection);`) - } - - const upContent = lines.join('\n') - - // Down migration: revert to placeholder query - const downContent = viewCollections - .map(([name, { raw }]) => { - return ` const ${raw.name}Collection = app.findCollectionByNameOrId("${raw.name}");\n ${name}Collection.viewQuery = 'SELECT (ROW_NUMBER() OVER()) as id';\n app.save(${name}Collection);` - }) - .join('\n\n') - - return { upContent, downContent, hasViews: true } -} diff --git a/tools/src/commands/db/functions/migration-generation/migration-file.ts b/tools/src/commands/db/functions/migration-generation/migration-file.ts deleted file mode 100644 index 61a9ad62b..000000000 --- a/tools/src/commands/db/functions/migration-generation/migration-file.ts +++ /dev/null @@ -1,191 +0,0 @@ -import chalk from 'chalk' -import { execSync } from 'child_process' -import fs from 'fs' -import prettier from 'prettier' - -import { PB_BINARY_PATH, PB_KWARGS } from '@/constants/db' -import Logging from '@/utils/logging' - -import { PRETTIER_OPTIONS } from '../../utils' -import { - generateSkeletonMigrationContent, - generateStructureMigrationContent, - generateViewQueryMigrationContent -} from './migration-content' - -/** - * Creates a single migration file with custom content - */ -async function createSingleMigration( - name: string, - upContent: string, - downContent: string -): Promise<{ success: boolean; path?: string; error?: string }> { - try { - const response = execSync( - `${PB_BINARY_PATH} migrate create ${name} ${PB_KWARGS.join(' ')}`, - { - input: 'y\n', - stdio: ['pipe', 'pipe', 'pipe'] - } - ) - - const resString = response.toString() - - const match = resString.match(/Successfully created file "(.*)"/) - - if (!match || match.length < 2) { - throw new Error('Failed to parse migration file path from response.') - } - - const migrationFilePath = match[1] - - if (!fs.existsSync(migrationFilePath)) { - throw new Error(`Migration file not found at path: ${migrationFilePath}`) - } - - let content = fs.readFileSync(migrationFilePath, 'utf-8') - - content = content - .replace('../pb_data/types.d.ts', '../types.d.ts') - .replace('// add up queries...', upContent) - .replace('// add down queries...', downContent) - - const formattedContent = await prettier.format(content, PRETTIER_OPTIONS) - - fs.writeFileSync(migrationFilePath, formattedContent, 'utf-8') - - return { success: true, path: migrationFilePath } - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error) - } - } -} - -/** - * Creates skeleton migration for a module (stub collections) - */ -export async function createSkeletonMigration( - moduleName: string, - schema: Record }> -): Promise<{ success: boolean; error?: string }> { - try { - const skeleton = generateSkeletonMigrationContent(schema) - - const result = await createSingleMigration( - `01_${moduleName}_skeleton`, - skeleton.upContent, - skeleton.downContent - ) - - if (!result.success) { - throw new Error(`Skeleton migration failed: ${result.error}`) - } - - Logging.debug( - `Created skeleton migration for ${chalk.bold.blue(moduleName)}` - ) - - return { success: true } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - - Logging.error( - `Failed to create skeleton migration for ${chalk.bold.blue(moduleName)}: ${errorMessage}` - ) - - return { success: false, error: errorMessage } - } -} - -/** - * Creates structure migration for a module (full schema with relations) - */ -export async function createStructureMigration( - moduleName: string, - schema: Record }>, - idToNameMap: Map -): Promise<{ success: boolean; error?: string }> { - try { - const structure = generateStructureMigrationContent(schema, idToNameMap) - - const result = await createSingleMigration( - `02_${moduleName}_structure`, - structure.upContent, - structure.downContent - ) - - if (!result.success) { - throw new Error(`Structure migration failed: ${result.error}`) - } - - Logging.debug( - `Created structure migration for ${chalk.bold.blue(moduleName)}` - ) - - return { success: true } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - - Logging.error( - `Failed to create structure migration for ${chalk.bold.blue(moduleName)}: ${errorMessage}` - ) - - return { success: false, error: errorMessage } - } -} - -/** - * Runs migrate up to apply pending migrations - */ -export function runMigrateUp(): void { - Logging.debug('Applying pending migrations...') - - execSync(`${PB_BINARY_PATH} migrate up ${PB_KWARGS.join(' ')}`, { - stdio: ['pipe', 'pipe', 'pipe'] - }) - - Logging.debug('Migrations applied successfully') -} - -/** - * Creates view query migration for a module (updates viewQuery on view collections) - */ -export async function createViewQueryMigration( - moduleName: string, - schema: Record }> -): Promise<{ success: boolean; hasViews: boolean; error?: string }> { - try { - const viewQuery = generateViewQueryMigrationContent(schema) - - if (!viewQuery.hasViews) { - return { success: true, hasViews: false } - } - - const result = await createSingleMigration( - `03_${moduleName}_views`, - viewQuery.upContent, - viewQuery.downContent - ) - - if (!result.success) { - throw new Error(`View query migration failed: ${result.error}`) - } - - Logging.debug( - `Created view query migration for ${chalk.bold.blue(moduleName)}` - ) - - return { success: true, hasViews: true } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - - Logging.error( - `Failed to create view query migration for ${chalk.bold.blue(moduleName)}: ${errorMessage}` - ) - - return { success: false, hasViews: false, error: errorMessage } - } -} diff --git a/tools/src/commands/db/functions/migration-generation/stageMigrations.ts b/tools/src/commands/db/functions/migration-generation/stageMigrations.ts new file mode 100644 index 000000000..b8ac807b0 --- /dev/null +++ b/tools/src/commands/db/functions/migration-generation/stageMigrations.ts @@ -0,0 +1,64 @@ +import path from 'path' + +import Logging from '@/utils/logging' + +import applyMigrations from './applyMigrations' +import createSingleMigration from './createSingleMigration' + +/** + * Generates and applies migrations for a specific phase across all modules. + * + * The migration process is split into three phases that must run in order: + * 1. **skeleton** - Creates stub collections if they don't exist (minimal structure) + * 2. **structure** - Updates collections with full schema, fields, and relations + * 3. **views** - Sets the actual viewQuery on view collections + * + * This phased approach ensures: + * - Collections exist before relations reference them (skeleton phase) + * - Relations can be resolved by name after all collections are created (structure phase) + * - View queries can reference all tables after schemas are complete (views phase) + * + * @param phase - The migration phase: 'skeleton', 'structure', or 'views' + * @param index - Zero-based index of the phase (used for logging) + * @param importedSchemas - Array of module schemas to process + * @param idToNameMap - Map of collection IDs to names for resolving relations + * + * @throws Error if any module's migration fails to generate + */ +export default async function stageMigration( + phase: 'skeleton' | 'structure' | 'views', + index: number, + importedSchemas: Array<{ + moduleName: string + schema: Record }> + }>, + idToNameMap: Map +) { + Logging.debug(`Phase ${index + 1}: Creating ${phase} migrations...`) + + const { default: phaseFn } = await import( + path.resolve(import.meta.dirname, 'generateContent', `${phase}.ts`) + ) + + for (const { moduleName, schema } of importedSchemas) { + try { + await createSingleMigration( + `${phase}_${moduleName}`, + phaseFn(schema, idToNameMap) + ) + + Logging.debug( + `Created ${phase} migration for ${Logging.highlight(moduleName)}` + ) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error) + + throw new Error( + `Failed to create ${phase} migration for ${Logging.highlight(moduleName)}: ${errorMessage}` + ) + } + } + + applyMigrations() +} diff --git a/tools/src/commands/db/functions/schema-generation/buildModuleCollectionsMap.ts b/tools/src/commands/db/functions/schema-generation/buildModuleCollectionsMap.ts new file mode 100644 index 000000000..0e770fd47 --- /dev/null +++ b/tools/src/commands/db/functions/schema-generation/buildModuleCollectionsMap.ts @@ -0,0 +1,60 @@ +import Logging from '@/utils/logging' + +import { listSchemaPaths } from './listSchemaPaths' +import { matchCollectionToModule } from './matchCollectionToModule' + +/** + * Creates a mapping of module paths to their associated PocketBase collections. + * + * This function is the core of the schema generation process. It: + * 1. Discovers all modules with schema files in the project + * 2. Iterates through PocketBase collections + * 3. Matches each collection to its owning module based on naming conventions + * 4. Groups collections by their module path + * + * Collections that don't match any module (e.g., orphaned or third-party) are + * logged in debug mode but excluded from the result. + * + * @param collections - Array of PocketBase collection objects from the database + * @returns Map where keys are module directory paths and values are arrays of collections + */ +export default async function buildModuleCollectionsMap( + collections: Array> +): Promise[]>> { + const moduleCollectionsMap: Record[]> = {} + + const allModules = listSchemaPaths() + + Logging.debug( + `Found ${Logging.highlight(String(allModules.length))} modules with schema files` + ) + + let matchedCount = 0 + let unmatchedCount = 0 + + for (const collection of collections) { + const matchingModuleDir = matchCollectionToModule(allModules, collection) + + if (!matchingModuleDir) { + unmatchedCount++ + Logging.debug( + `Collection ${Logging.highlight(collection.name as string)} has no matching module` + ) + continue + } + + matchedCount++ + + if (!moduleCollectionsMap[matchingModuleDir]) { + moduleCollectionsMap[matchingModuleDir] = [] + } + + moduleCollectionsMap[matchingModuleDir].push(collection) + } + + Logging.debug( + `Matched ${Logging.highlight(String(matchedCount))} collections to modules, ${unmatchedCount} unmatched` + ) + + return moduleCollectionsMap +} diff --git a/tools/src/commands/db/functions/schema-generation/content-generator.ts b/tools/src/commands/db/functions/schema-generation/content-generator.ts deleted file mode 100644 index 8b2367d00..000000000 --- a/tools/src/commands/db/functions/schema-generation/content-generator.ts +++ /dev/null @@ -1,95 +0,0 @@ -import chalk from 'chalk' -import _ from 'lodash' -import path from 'path' - -import { parseCollectionName } from '@/commands/modules/functions/registry/namespaceUtils' -import Logging from '@/utils/logging' - -import { generateCollectionSchema, stripCollectionIds } from './field-converter' - -/** - * Generates schema content for a module - * @param moduleName - The name of the module - * @param collections - Collections belonging to this module - * @param idToNameMap - Map of collection IDs to names for relation resolution - */ -export function generateModuleSchemaContent( - moduleName: string, - collections: Array>, - idToNameMap?: Map -): string { - const schemaEntries: string[] = [] - - for (const collection of collections) { - const collectionName = collection.name as string - - const fields = collection.fields as Array> - - Logging.debug( - `Processing collection ${chalk.bold(collectionName)} with ${fields.length} fields` - ) - - const zodSchemaObject = generateCollectionSchema(collection) - - const schemaObjectString = Object.entries(zodSchemaObject) - .map(([key, value]) => ` ${key}: ${value},`) - .join('\n') - - // Use parseCollectionName to properly extract short name for both official and third-party modules - const shortName = parseCollectionName(collectionName).collectionName - - const zodSchemaString = `z.object({\n${schemaObjectString}\n})` - - // Remove IDs and convert relation collectionIds to names - const cleanedCollection = stripCollectionIds(collection, idToNameMap) - - schemaEntries.push(` ${shortName}: { - schema: ${zodSchemaString}, - raw: ${JSON.stringify(cleanedCollection, null, 2)} - },`) - - Logging.info( - `Generated schema for collection ${chalk.bold(collectionName)}` - ) - } - - return `import z from 'zod' - -const ${_.camelCase(moduleName)}Schemas = { -${schemaEntries.join('\n')} -} - -export default ${_.camelCase(moduleName)}Schemas -` -} - -/** - * Generates main schema file content - */ -export function generateMainSchemaContent(moduleDirs: string[]): string { - const imports = moduleDirs - .map(moduleDir => { - const [moduleDirPath, moduleDirName] = moduleDir.split('|') - - const targetPath = path.join( - '@lib/', - moduleDirName, - moduleDirPath.split(moduleDirName).pop() || '', - 'schema' - ) - - return ` ${_.snakeCase(moduleDirName)}: (await import('${targetPath}')).default,` - }) - .join('\n') - - return `import flattenSchemas from '@functions/utils/flattenSchema' - -export const SCHEMAS = { -${imports} -} - -const COLLECTION_SCHEMAS = flattenSchemas(SCHEMAS) - -export default COLLECTION_SCHEMAS -` -} diff --git a/tools/src/commands/db/functions/schema-generation/field-converter.ts b/tools/src/commands/db/functions/schema-generation/field-converter.ts deleted file mode 100644 index f62d6e7ca..000000000 --- a/tools/src/commands/db/functions/schema-generation/field-converter.ts +++ /dev/null @@ -1,96 +0,0 @@ -import Logging from '@/utils/logging' - -import { FIELD_TYPE_MAPPING } from '../../utils' -import type { PocketBaseField } from '../../utils/constants' - -/** - * Converts a PocketBase field to Zod schema string - */ -export function convertFieldToZodSchema(field: PocketBaseField): string | null { - if (field.name === 'id') { - return null // Skip auto-generated fields - } - - const converter = FIELD_TYPE_MAPPING[field.type] - - if (!converter) { - Logging.warn( - `Unknown field type '${field.type}' for field '${field.name}'. Skipping.` - ) - - return null - } - - return converter(field) -} - -/** - * Generates Zod schema for a collection - */ -export function generateCollectionSchema( - collection: Record -): Record { - const zodSchemaObject: Record = {} - - const fields = collection.fields as Array> - - for (const field of fields.filter(e => !e.hidden)) { - const zodSchema = convertFieldToZodSchema(field as PocketBaseField) - - if (zodSchema) { - zodSchemaObject[field.name as string] = zodSchema - } - } - - return zodSchemaObject -} - -/** - * Strips collection ID and field IDs from raw config - * This prevents migration conflicts when importing to different databases - * For relation fields, converts collectionId to collection name for portability - */ -export function stripCollectionIds( - collection: Record, - idToNameMap?: Map -): Record { - const cleaned = { ...collection } - - // Remove collection-level properties that cause conflicts - delete cleaned.id - delete cleaned.created - delete cleaned.updated - - if ('oauth2' in cleaned) { - delete cleaned.oauth2 - } - - // Remove field IDs and convert relation collectionIds to names - if (cleaned.fields && Array.isArray(cleaned.fields)) { - cleaned.fields = cleaned.fields.map((field: Record) => { - const cleanedField = { ...field } - - delete cleanedField.id - - // For relation fields, convert collectionId to collection name - if ( - cleanedField.type === 'relation' && - cleanedField.collectionId && - idToNameMap - ) { - const collectionName = idToNameMap.get( - cleanedField.collectionId as string - ) - - if (collectionName) { - // PocketBase can look up by name instead of ID - cleanedField.collectionId = collectionName - } - } - - return cleanedField - }) - } - - return cleaned -} diff --git a/tools/src/commands/db/functions/schema-generation/filterCollectionsMap.ts b/tools/src/commands/db/functions/schema-generation/filterCollectionsMap.ts new file mode 100644 index 000000000..a2c865a62 --- /dev/null +++ b/tools/src/commands/db/functions/schema-generation/filterCollectionsMap.ts @@ -0,0 +1,41 @@ +import Logging from '@/utils/logging' +import normalizePackage from '@/utils/normalizePackage' + +/** + * Filters a module collections map to include only a specific module's collections. + * + * Used when running schema generation for a single module instead of all modules. + * Matches by the module's short name (e.g., 'calendar' matches 'lifeforge--calendar'). + * + * @param moduleCollectionsMap - Full map of all modules to their collections + * @param targetModule - Optional module name to filter by. If omitted, returns the full map. + * @returns Filtered map containing only the target module, or the full map if no target specified + * + * @throws Exits process if targetModule is specified but not found in the map + * + * @example + * // Filter to only invoice-maker collections: + * filterCollectionsMap(fullMap, 'invoice-maker') + * // Returns: { '/path/to/melvinchia3636--invoice-maker': [...] } + */ +export default function filterCollectionsMap( + moduleCollectionsMap: Record[]>, + targetModule?: string +) { + const { shortName } = normalizePackage(targetModule || '', 'module') + + const filteredModuleCollectionsMap = targetModule + ? Object.fromEntries( + Object.entries(moduleCollectionsMap).filter(([key]) => + key.endsWith(shortName) + ) + ) + : moduleCollectionsMap + + if (targetModule && Object.keys(filteredModuleCollectionsMap).length === 0) { + Logging.error(`Module "${shortName}" not found or has no collections`) + process.exit(1) + } + + return filteredModuleCollectionsMap +} diff --git a/tools/src/commands/db/functions/schema-generation/generateSchemaContent.ts b/tools/src/commands/db/functions/schema-generation/generateSchemaContent.ts new file mode 100644 index 000000000..e27ac8fcb --- /dev/null +++ b/tools/src/commands/db/functions/schema-generation/generateSchemaContent.ts @@ -0,0 +1,79 @@ +import { parseCollectionName } from '@/utils/pocketbase' + +import generateZodSchemaString from './generateZodSchema' +import stripCollectionIds from './stripCollectionIds' + +/** + * Processes a single collection into schema.ts format with Zod schema and raw config. + * + * @param collection - PocketBase collection object + * @param idToNameMap - Map for converting relation IDs to collection names + * @returns Formatted schema entry string for the collection + */ +function processCollectionSchema( + collection: Record, + idToNameMap: Map +): string { + const collectionName = collection.name as string + + const shortName = parseCollectionName( + collectionName, + 'pb', + collectionName === 'users' ? 'users' : undefined + ).collectionName + + const zodSchemaString = generateZodSchemaString(collection) + + const cleanedRawCollection = stripCollectionIds(collection, idToNameMap) + + return ` ${shortName}: { + schema: ${zodSchemaString}, + raw: ${JSON.stringify(cleanedRawCollection, null, 2)} + },` +} + +/** + * Generates the complete TypeScript schema file content for a module. + * + * Creates a `schema.ts` file that exports: + * - Zod validation schemas for type-safe data access + * - Raw PocketBase collection configs for migration generation + * + * The generated file can be imported by both the migration generator (for raw configs) + * and application code (for Zod schemas). + * + * @param collections - Array of PocketBase collections belonging to this module + * @param idToNameMap - Map of collection IDs to names for resolving relation fields + * @returns Complete TypeScript file content as a string + * + * @example + * // Generated file structure: + * // import z from 'zod' + * // + * // const schemas = { + * // events: { + * // schema: z.object({ title: z.string(), ... }), + * // raw: { name: 'calendar__events', ... } + * // }, + * // } + * // + * // export default schemas + */ +export default function generateSchemaContent( + collections: Array>, + idToNameMap: Map +): string { + const schemaEntries: string[] = [] + + for (const collection of collections) { + schemaEntries.push(processCollectionSchema(collection, idToNameMap)) + } + + return `import z from 'zod' + +const schemas = { +${schemaEntries.join('\n')} +} + +export default schemas` +} diff --git a/tools/src/commands/db/functions/schema-generation/generateZodSchema.ts b/tools/src/commands/db/functions/schema-generation/generateZodSchema.ts new file mode 100644 index 000000000..7bb228301 --- /dev/null +++ b/tools/src/commands/db/functions/schema-generation/generateZodSchema.ts @@ -0,0 +1,113 @@ +import Logging from '@/utils/logging' + +/** + * PocketBase field definition with common properties. + */ +interface PocketBaseField { + name: string + type: string + required?: boolean + maxSelect?: number + values?: string[] + [key: string]: unknown +} + +/** + * Mapping of PocketBase field types to their Zod schema equivalents. + * + * Each entry is a function that takes a field definition and returns + * the appropriate Zod schema string. Some types (like select, file, relation) + * need to check field options to determine if they're single or multi-value. + */ +const FIELD_TYPE_MAPPING: Record string> = { + text: () => 'z.string()', + richtext: () => 'z.string()', + number: () => 'z.number()', + bool: () => 'z.boolean()', + email: () => 'z.email()', + url: () => 'z.url()', + date: () => 'z.string()', + autodate: () => 'z.string()', + password: () => 'z.string()', + json: () => 'z.any()', + geoPoint: () => 'z.object({ lat: z.number(), lon: z.number() })', + select: field => { + const values = [...(field.values ?? []), ...(field.required ? [] : [''])] + + const enumSchema = `z.enum(${JSON.stringify(values)})` + + return (field.maxSelect ?? 1) > 1 ? `z.array(${enumSchema})` : enumSchema + }, + file: field => { + const baseSchema = 'z.string()' + + return (field.maxSelect ?? 1) > 1 ? `z.array(${baseSchema})` : baseSchema + }, + relation: field => { + const baseSchema = 'z.string()' + + return (field.maxSelect ?? 1) > 1 ? `z.array(${baseSchema})` : baseSchema + } +} + +/** + * Converts a single PocketBase field to its Zod schema string representation. + * + * @param field - PocketBase field definition + * @returns Zod schema string, or null if field should be skipped + */ +function convertFieldToZodSchema(field: PocketBaseField): string | null { + if (field.name === 'id') { + return null // Skip auto-generated fields + } + + const converter = FIELD_TYPE_MAPPING[field.type] + + if (!converter) { + Logging.warn( + `Unknown field type '${field.type}' for field '${field.name}'. Skipping.` + ) + + return null + } + + return converter(field) +} + +/** + * Generates a Zod schema string for a PocketBase collection. + * + * Converts the collection's field definitions into a `z.object()` schema + * that can be used for runtime validation and TypeScript type inference. + * + * Hidden fields are automatically excluded from the generated schema. + * + * @param collection - PocketBase collection object with fields array + * @returns Zod schema string like `z.object({ name: z.string(), ... })` + * + * @example + * // Input collection with fields: name (text), age (number) + * // Output: z.object({ + * // name: z.string(), + * // age: z.number(), + * // }) + */ +export default function generateZodSchemaString( + collection: Record +): string { + const zodSchemaObject: Record = {} + + const fields = collection.fields as Array> + + for (const field of fields.filter(e => !e.hidden)) { + const zodSchema = convertFieldToZodSchema(field as PocketBaseField) + + if (zodSchema) { + zodSchemaObject[field.name as string] = zodSchema + } + } + + return `z.object({\n${Object.entries(zodSchemaObject) + .map(([key, value]) => ` ${key}: ${value},`) + .join('\n')}\n})` +} diff --git a/tools/src/commands/db/functions/schema-generation/getCollectionsFromPB.ts b/tools/src/commands/db/functions/schema-generation/getCollectionsFromPB.ts new file mode 100644 index 000000000..3c4f7a6af --- /dev/null +++ b/tools/src/commands/db/functions/schema-generation/getCollectionsFromPB.ts @@ -0,0 +1,41 @@ +import { isDockerMode } from '@/utils/helpers' +import Logging from '@/utils/logging' +import getPBInstance from '@/utils/pocketbase' + +/** + * Fetches all non-system collections from a running PocketBase instance. + * + * This function handles PocketBase lifecycle automatically: + * - In Docker mode: Connects to the existing PocketBase instance + * - In local mode: Starts a temporary PocketBase instance if needed, then stops it after fetching + * + * System collections (like `_superusers`, `_auths`, etc.) are automatically filtered out + * since they're managed by PocketBase and shouldn't be included in module schemas. + * + * @returns Array of non-system PocketBase collection objects + * + * @example + * const collections = await getCollectionsFromPB() + * // Returns: [{ name: 'calendar__events', type: 'base', ... }, ...] + */ +export default async function getCollectionsFromPB() { + Logging.debug('Connecting to PocketBase...') + + const { pb, killPB } = await getPBInstance(!isDockerMode()) + + Logging.debug('Fetching all collections...') + + const allCollections = await pb.collections.getFullList() + + killPB?.() + + const nonSystemCollections = allCollections.filter( + collection => !collection.system + ) + + Logging.debug( + `Found ${Logging.highlight(String(allCollections.length))} collections, ${Logging.highlight(String(nonSystemCollections.length))} non-system` + ) + + return nonSystemCollections +} diff --git a/tools/src/commands/db/functions/schema-generation/listSchemaPaths.ts b/tools/src/commands/db/functions/schema-generation/listSchemaPaths.ts new file mode 100644 index 000000000..4c477181f --- /dev/null +++ b/tools/src/commands/db/functions/schema-generation/listSchemaPaths.ts @@ -0,0 +1,46 @@ +import fs from 'fs' +import path from 'path' + +import { PROJECT_ROOT } from '@/constants/constants' +import Logging from '@/utils/logging' + +/** + * Discovers all module directories that contain schema files. + * + * Scans two locations for `schema.ts` files: + * 1. Core modules: `server/src/lib/**\/schema.ts` (built-in features like users, apiKeys) + * 2. App modules: `apps\**\/server/schema.ts` (installed/created modules) + * + * Returns the parent directory of each schema file, which represents the module root. + * For app modules, strips the trailing `/server` to get the actual module directory. + * + * @returns Array of absolute paths to module directories containing schema files + * + * @example + * // Returns paths like: + * // [ + * // '/project/server/src/lib/user', + * // '/project/apps/lifeforge--calendar', + * // '/project/apps/melvinchia3636--invoice-maker' + * // ] + */ +export function listSchemaPaths(): string[] { + const modulesDirs = [ + './server/src/lib/**/schema.ts', + './apps/**/server/schema.ts' + ] + + let allModules: string[] = [] + + try { + allModules = modulesDirs + .map(dir => fs.globSync(path.resolve(PROJECT_ROOT, dir))) + .flat() + .map(entry => path.dirname(entry).replace(/\/server$/, '')) + } catch (error) { + Logging.error(`Failed to read modules directory: ${error}`) + process.exit(1) + } + + return allModules +} diff --git a/tools/src/commands/db/functions/schema-generation/matchCollectionToModule.ts b/tools/src/commands/db/functions/schema-generation/matchCollectionToModule.ts new file mode 100644 index 000000000..488ee7f7f --- /dev/null +++ b/tools/src/commands/db/functions/schema-generation/matchCollectionToModule.ts @@ -0,0 +1,54 @@ +import path from 'path' + +import { parsePackageName } from '@/commands/modules/functions/parsePackageName' +import { parseCollectionName } from '@/utils/pocketbase' + +/** + * Finds the module that owns a given PocketBase collection. + * + * PocketBase collection names follow a naming convention: + * - First-party: `moduleName__collectionName` (e.g., `calendar__events`) + * - Third-party: `username___moduleName__collectionName` (e.g., `melvinchia3636___invoice_maker__clients`) + * + * This function parses the collection name and matches it against registered modules + * by comparing the username and module name prefixes. + * + * Special case: The `users` collection maps to the `user` module. + * + * @param allModules - Array of module directory paths from `listSchemaPaths()` + * @param collection - PocketBase collection object with `name` property + * @returns Matching module directory path, or undefined if no match found + * + * @example + * // Collection 'calendar__events' matches module at '/apps/lifeforge--calendar' + * // Collection 'melvinchia3636___invoice_maker__clients' matches '/apps/melvinchia3636--invoice-maker' + */ +export function matchCollectionToModule( + allModules: string[], + collection: Record +) { + const collectionName = collection.name as string + + const parsed = parseCollectionName( + collectionName, + 'pb', + collectionName === 'users' ? 'user' : undefined + ) + + const modulePrefix = parsed.username + ? `${parsed.username}___${parsed.moduleName}` + : parsed.moduleName + + const matchingModule = allModules.find(module => { + const { username, moduleName } = parsePackageName( + path.basename(module), + path.dirname(module).endsWith('/server/src/lib') + ) + + const expectedPrefix = username ? `${username}___${moduleName}` : moduleName + + return modulePrefix === expectedPrefix + }) + + return matchingModule +} diff --git a/tools/src/commands/db/functions/schema-generation/module-mapper.ts b/tools/src/commands/db/functions/schema-generation/module-mapper.ts deleted file mode 100644 index 24b363793..000000000 --- a/tools/src/commands/db/functions/schema-generation/module-mapper.ts +++ /dev/null @@ -1,117 +0,0 @@ -import fs from 'fs' -import _ from 'lodash' - -import { - parseCollectionName, - parsePackageName -} from '@/commands/modules/functions/registry/namespaceUtils' -import Logging from '@/utils/logging' - -/** - * Builds mapping of modules to their collections - */ -export async function buildModuleCollectionsMap( - collections: Array> -): Promise[]>> { - const moduleCollectionsMap: Record[]> = {} - - const modulesDirs = [ - './server/src/lib/**/schema.ts', - './apps/**/server/schema.ts' - ] - - let allModules: string[] = [] - - try { - allModules = modulesDirs - .map(dir => fs.globSync(dir)) - .flat() - .map(entry => entry.split('/').slice(0, -1).join('/')) - } catch (error) { - Logging.error(`Failed to read modules directory: ${error}`) - process.exit(1) - } - - for (const collection of collections) { - const collectionName = collection.name as string - - // Parse the collection name to get the module identifier - const parsed = parseCollectionName(collectionName) - - // Build the module prefix (including username for third-party) - const modulePrefix = parsed.username - ? `${parsed.username}___${parsed.moduleName}` - : parsed.moduleName - - const matchingModule = allModules.find(module => { - const moduleDirName = - module - .replace(/\/server$/, '') - .split('/') - .pop() || '' - - const { username, moduleName } = parsePackageName(moduleDirName) - - const expectedPrefix = username - ? `${username}___${moduleName}` - : moduleName - - return modulePrefix === expectedPrefix - }) - - if (!matchingModule) { - // Fallback: use camelCase for path lookup - const moduleName = _.camelCase(parsed.moduleName) - - const possibleModulePath = [ - `./server/src/lib/${moduleName}/server`, - `./apps/${moduleName}/server` - ] - - const foundModulePath = possibleModulePath.filter(modulePath => - fs.existsSync(modulePath) - ) - - if (foundModulePath.length > 0) { - Logging.debug( - `Inferred module path for collection '${collectionName}': ${foundModulePath[0]}` - ) - - const key = `${foundModulePath[0]}|${moduleName}` - - if (!moduleCollectionsMap[key]) { - moduleCollectionsMap[key] = [] - } - - moduleCollectionsMap[key].push(collection) - continue - } - - Logging.warn(`Collection '${collectionName}' has no corresponding module`) - continue - } - - const moduleName = matchingModule - .replace(/\/server$/, '') - .split('/') - .pop() - - const key = `${matchingModule}|${moduleName}` - - if (!moduleCollectionsMap[key]) { - moduleCollectionsMap[key] = [] - } - - moduleCollectionsMap[key].push(collection) - } - - const totalCollections = Object.values(moduleCollectionsMap).flat().length - - const moduleCount = Object.keys(moduleCollectionsMap).length - - Logging.info( - `Found ${totalCollections} collections across ${moduleCount} modules` - ) - - return moduleCollectionsMap -} diff --git a/tools/src/commands/db/functions/schema-generation/schema-processor.ts b/tools/src/commands/db/functions/schema-generation/schema-processor.ts deleted file mode 100644 index 3127f35a7..000000000 --- a/tools/src/commands/db/functions/schema-generation/schema-processor.ts +++ /dev/null @@ -1,80 +0,0 @@ -import chalk from 'chalk' -import path from 'path' - -import { parseCollectionName } from '@/commands/modules/functions/registry/namespaceUtils' -import Logging from '@/utils/logging' - -import { writeFormattedFile } from '../../utils' -import { generateModuleSchemaContent } from './content-generator' - -/** - * Processes schema generation for modules - * @param moduleCollectionsMap - Map of module paths to their collections - * @param idToNameMap - Map of collection IDs to names for relation resolution - * @param targetModule - Optional specific module to process - */ -export async function processSchemaGeneration( - moduleCollectionsMap: Record[]>, - idToNameMap: Map, - targetModule?: string -): Promise<{ moduleSchemas: Record; moduleDirs: string[] }> { - const filteredModuleCollectionsMap = targetModule - ? Object.fromEntries( - Object.entries(moduleCollectionsMap).filter(([key]) => - key.includes(targetModule) - ) - ) - : moduleCollectionsMap - - if (targetModule && Object.keys(filteredModuleCollectionsMap).length === 0) { - Logging.error(`Module "${targetModule}" not found or has no collections`) - process.exit(1) - } - - const moduleSchemas: Record = {} - - const moduleDirs: string[] = [] - - for (const [moduleDir, collections] of Object.entries( - filteredModuleCollectionsMap - )) { - const [moduleDirPath, moduleDirName] = moduleDir.split('|') - - if (!collections.length) { - Logging.warn( - `No collections found for module ${chalk.bold(moduleDirName)}` - ) - continue - } - - const firstCollection = collections[0] as Record - - // Use parseCollectionName to extract module name with username support for third-party modules - const parsed = parseCollectionName(firstCollection.name as string) - - const moduleName = parsed.username - ? `${parsed.username}___${parsed.moduleName}` - : parsed.moduleName - - moduleDirs.push(moduleDir) - - const moduleSchemaContent = generateModuleSchemaContent( - moduleName, - collections as Array>, - idToNameMap - ) - - moduleSchemas[moduleDirName] = moduleSchemaContent - - // Write individual module schema file - const moduleSchemaPath = path.join(moduleDirPath, 'schema.ts') - - await writeFormattedFile(moduleSchemaPath, moduleSchemaContent) - - Logging.debug( - `Created schema file for module ${chalk.bold(moduleDirName)} at ${chalk.bold(`lib/${moduleDirName}/schema.ts`)}` - ) - } - - return { moduleSchemas, moduleDirs } -} diff --git a/tools/src/commands/db/functions/schema-generation/stripCollectionIds.ts b/tools/src/commands/db/functions/schema-generation/stripCollectionIds.ts new file mode 100644 index 000000000..747360e61 --- /dev/null +++ b/tools/src/commands/db/functions/schema-generation/stripCollectionIds.ts @@ -0,0 +1,68 @@ +/** + * Strips auto-generated IDs and timestamps from a collection configuration. + * + * PocketBase assigns unique IDs to collections and fields, but these IDs are + * database-specific and not portable. By stripping them, the generated schema + * can be used to recreate collections in any PocketBase instance. + * + * For relation fields, this function also converts the `collectionId` (which is + * a database-specific ID) to the collection's `name` using the provided map. + * This allows migrations to look up collections by name instead of ID. + * + * Removed properties: + * - `id` - Collection and field IDs + * - `created` / `updated` - Timestamps + * - `oauth2` - OAuth config (if present) + * + * @param collection - Raw PocketBase collection object + * @param idToNameMap - Optional map for converting relation collectionIds to names + * @returns Cleaned collection config without database-specific IDs + * + * @example + * // Before: { id: 'abc123', name: 'events', fields: [{ id: 'def456', name: 'title', ... }] } + * // After: { name: 'events', fields: [{ name: 'title', ... }] } + */ +export default function stripCollectionIds( + collection: Record, + idToNameMap?: Map +): Record { + const cleaned = { ...collection } + + // Remove collection-level properties that cause conflicts + delete cleaned.id + delete cleaned.created + delete cleaned.updated + + if ('oauth2' in cleaned) { + delete cleaned.oauth2 + } + + // Remove field IDs and convert relation collectionIds to names + if (cleaned.fields && Array.isArray(cleaned.fields)) { + cleaned.fields = cleaned.fields.map((field: Record) => { + const cleanedField = { ...field } + + delete cleanedField.id + + // For relation fields, convert collectionId to collection name + if ( + cleanedField.type === 'relation' && + cleanedField.collectionId && + idToNameMap + ) { + const collectionName = idToNameMap.get( + cleanedField.collectionId as string + ) + + if (collectionName) { + // PocketBase can look up by name instead of ID + cleanedField.collectionId = collectionName + } + } + + return cleanedField + }) + } + + return cleaned +} diff --git a/tools/src/commands/db/handlers/generateMigrationsHandler.ts b/tools/src/commands/db/handlers/generateMigrationsHandler.ts index 50de4edab..867c6582c 100644 --- a/tools/src/commands/db/handlers/generateMigrationsHandler.ts +++ b/tools/src/commands/db/handlers/generateMigrationsHandler.ts @@ -1,163 +1,32 @@ -import chalk from 'chalk' - -import { isDockerMode } from '@/utils/helpers' import Logging from '@/utils/logging' -import { checkRunningPBInstances } from '@/utils/pocketbase' -import { - createSkeletonMigration, - createStructureMigration, - createViewQueryMigration, - runMigrateUp -} from '../functions/migration-generation/migration-file' -import { getSchemaFiles, importSchemaModules } from '../utils/file-utils' +import buildIdToNameMap from '../functions/migration-generation/buildIdToNameMap' +import stageMigration from '../functions/migration-generation/stageMigrations' +import { importSchemaModules } from '../utils' import { cleanupOldMigrations } from '../utils/pocketbase-utils' -/** - * Builds an ID-to-name map from all schemas - */ -function buildIdToNameMap( - importedSchemas: Array<{ - moduleName: string - schema: Record }> - }> -): Map { - const idToNameMap = new Map() - - for (const { schema } of importedSchemas) { - for (const entry of Object.values(schema)) { - const raw = entry?.raw - - if (raw?.id && raw?.name) { - idToNameMap.set(raw.id as string, raw.name as string) - } - } - } - - return idToNameMap -} - /** * Command handler for generating database migrations - * - * Five-phase approach: - * 1. Generate skeleton migrations for all modules (stub collections) - * 2. Run migrate up to apply skeletons (all collections now exist) - * 3. Generate structure migrations for all modules (full schema with relations) - * 4. Run migrate up to apply structures (all structures now in place) - * 5. Generate view query migrations for modules with view collections */ export async function generateMigrationsHandler( targetModule?: string ): Promise { try { - Logging.step('Starting database migration generation') - - // Skip running instances check in Docker mode (we control the container) - if (!isDockerMode()) { - checkRunningPBInstances() - } - await cleanupOldMigrations(targetModule) - const schemaFiles = getSchemaFiles(targetModule) + const importedSchemas = await importSchemaModules(targetModule) - Logging.debug( - targetModule - ? `Processing module: ${chalk.bold.blue(targetModule)}` - : `Found ${chalk.bold.blue(schemaFiles.length)} schema files.` - ) + const idToNameMap = await buildIdToNameMap(targetModule) - const importedSchemas = await importSchemaModules(schemaFiles) - - // Build ID-to-name map from all schemas for relation resolution - const idToNameMap = buildIdToNameMap(importedSchemas) - - Logging.debug(`Built ID-to-name map with ${idToNameMap.size} collections`) - - // Phase 1: Generate all skeleton migrations - Logging.debug('Phase 1: Creating skeleton migrations...') - - for (const { moduleName, schema } of importedSchemas) { - const result = await createSkeletonMigration(moduleName, schema) - - if (!result.success) { - Logging.actionableError( - `Skeleton migration failed for module ${moduleName}`, - 'Check the module schema definition for syntax errors' - ) - process.exit(1) - } + for (const [index, phase] of Object.entries([ + 'skeleton', + 'structure', + 'views' + ] as const)) { + await stageMigration(phase, Number(index), importedSchemas, idToNameMap) } - Logging.debug(`Created ${importedSchemas.length} skeleton migrations`) - - // Phase 2: Run migrate up to apply skeleton migrations - Logging.debug('Phase 2: Applying skeleton migrations...') - runMigrateUp() - - // Phase 3: Generate all structure migrations - Logging.debug('Phase 3: Creating structure migrations...') - - for (const { moduleName, schema } of importedSchemas) { - const result = await createStructureMigration( - moduleName, - schema, - idToNameMap - ) - - if (!result.success) { - Logging.actionableError( - `Structure migration failed for module ${moduleName}`, - 'Check the module schema definition for syntax errors' - ) - process.exit(1) - } - } - - Logging.debug(`Created ${importedSchemas.length} structure migrations`) - - // Phase 4: Run migrate up to apply structure migrations - Logging.debug('Phase 4: Applying structure migrations...') - runMigrateUp() - - // Phase 5: Generate view query migrations (for modules with view collections) - Logging.debug('Phase 5: Creating view query migrations...') - - let viewMigrationCount = 0 - - for (const { moduleName, schema } of importedSchemas) { - const result = await createViewQueryMigration(moduleName, schema) - - if (!result.success) { - Logging.actionableError( - `View query migration failed for module ${moduleName}`, - 'Check the view query definition for syntax errors' - ) - process.exit(1) - } - - if (result.hasViews) { - viewMigrationCount++ - } - } - - if (viewMigrationCount > 0) { - Logging.debug(`Created ${viewMigrationCount} view query migrations`) - - // Phase 6: Apply view query migrations - Logging.debug('Phase 6: Applying view query migrations...') - runMigrateUp() - } else { - Logging.debug('No view collections found, skipping view migrations') - } - - // Summary - const message = targetModule - ? `Database migrations applied for ${chalk.bold.blue(targetModule)}` - : 'Database migrations applied for all modules' - - Logging.success(message) + Logging.success('Migrations generated successfully') } catch (error) { Logging.actionableError( 'Migration script failed', diff --git a/tools/src/commands/db/handlers/generateSchemasHandler.ts b/tools/src/commands/db/handlers/generateSchemasHandler.ts index 7b353aca7..b20bf03bb 100644 --- a/tools/src/commands/db/handlers/generateSchemasHandler.ts +++ b/tools/src/commands/db/handlers/generateSchemasHandler.ts @@ -1,11 +1,12 @@ -import chalk from 'chalk' +import path from 'path' -import { isDockerMode } from '@/utils/helpers' import Logging from '@/utils/logging' -import getPBInstance from '@/utils/pocketbase' -import { buildModuleCollectionsMap } from '../functions/schema-generation/module-mapper' -import { processSchemaGeneration } from '../functions/schema-generation/schema-processor' +import buildModuleCollectionsMap from '../functions/schema-generation/buildModuleCollectionsMap' +import filterCollectionsMap from '../functions/schema-generation/filterCollectionsMap' +import generateSchemaContent from '../functions/schema-generation/generateSchemaContent' +import getCollectionsFromPB from '../functions/schema-generation/getCollectionsFromPB' +import { writeFormattedFile } from '../utils' /** * Command handler for generating database schemas @@ -13,52 +14,51 @@ import { processSchemaGeneration } from '../functions/schema-generation/schema-p export async function generateSchemaHandler( targetModule?: string ): Promise { - try { - Logging.info('Starting schema generation process...') - - const { pb, killPB } = await getPBInstance(!isDockerMode()) - - Logging.debug('Fetching collections from PocketBase...') - - const allCollections = await pb.collections.getFullList() - - const userCollections = allCollections.filter( - collection => !collection.system + if (targetModule) { + Logging.debug( + `Generating schema for module: ${Logging.highlight(targetModule)}` ) - - Logging.info(`Found ${userCollections.length} user-defined collections`) - - // Build ID-to-name map for all collections (including system) - // This allows relation fields to reference collections by name instead of ID - const idToNameMap = new Map() - - for (const collection of allCollections) { - idToNameMap.set(collection.id, collection.name) - } - - const moduleCollectionsMap = - await buildModuleCollectionsMap(userCollections) - - const { moduleSchemas } = await processSchemaGeneration( - moduleCollectionsMap, - idToNameMap, - targetModule - ) - - // Note: core/schema.ts is generated by the module registry generator (bun forge modules install) - // db pull only updates individual module schema files - - const moduleCount = Object.keys(moduleSchemas).length - - Logging.info( - targetModule - ? `Schema generation completed for module ${chalk.bold.blue(targetModule)}!` - : `Schema generation completed! Created ${moduleCount} module schema files.` - ) - - killPB?.() - } catch (error) { - Logging.error(`Schema generation failed: ${error}`) - process.exit(1) } + + const collections = await getCollectionsFromPB() + + const moduleCollectionsMap = await buildModuleCollectionsMap(collections) + + const filteredMap = filterCollectionsMap(moduleCollectionsMap, targetModule) + + const entries = Object.entries(filteredMap) + + if (entries.length === 0) { + Logging.warn('No modules found to generate schemas for') + + return + } + + await Promise.all( + entries.map(async ([modulePath, moduleCollections]) => { + const schemaPath = path.join(modulePath, 'schema.ts') + + const collectionCount = moduleCollections.length + + Logging.debug( + `Writing ${Logging.highlight(String(collectionCount))} collections to ${Logging.dim(schemaPath)}` + ) + + await writeFormattedFile( + schemaPath, + generateSchemaContent( + moduleCollections as Array>, + new Map( + moduleCollections.map( + collection => [collection.id, collection.name] as [string, string] + ) + ) + ) + ) + }) + ) + + Logging.success( + `Generated schemas for ${Logging.highlight(String(entries.length))} modules` + ) } diff --git a/tools/src/commands/db/handlers/initializeDatabaseHandler.ts b/tools/src/commands/db/handlers/initializeDatabaseHandler.ts index 0769830a7..ed2fc1511 100644 --- a/tools/src/commands/db/handlers/initializeDatabaseHandler.ts +++ b/tools/src/commands/db/handlers/initializeDatabaseHandler.ts @@ -3,11 +3,11 @@ import Logging from '@/utils/logging' import { checkRunningPBInstances } from '@/utils/pocketbase' import { downloadPocketBaseBinary } from '../functions/database-initialization/download-pocketbase' -import { runDatabaseMigrations } from '../functions/database-initialization/migrations' import { createPocketBaseSuperuser, validatePocketBaseNotInitialized } from '../functions/database-initialization/superuser' +import { generateMigrationsHandler } from './generateMigrationsHandler' export async function initializeDatabaseHandler() { const { PB_EMAIL, PB_PASSWORD } = getEnvVars([ @@ -20,10 +20,10 @@ export async function initializeDatabaseHandler() { checkRunningPBInstances() validatePocketBaseNotInitialized() - createPocketBaseSuperuser(PB_EMAIL, PB_PASSWORD) - runDatabaseMigrations() - Logging.success( - 'Setup process complete. You can now start the PocketBase server with `bun forge dev db`' - ) + createPocketBaseSuperuser(PB_EMAIL, PB_PASSWORD) + + generateMigrationsHandler() + + Logging.success('Database initialized successfully') } diff --git a/tools/src/commands/db/utils/constants.ts b/tools/src/commands/db/utils/constants.ts index 21725c382..393644cdb 100644 --- a/tools/src/commands/db/utils/constants.ts +++ b/tools/src/commands/db/utils/constants.ts @@ -2,48 +2,8 @@ * Constants for database command operations */ -export interface PocketBaseField { - name: string - type: string - required?: boolean - maxSelect?: number - values?: string[] - [key: string]: unknown -} -export const FIELD_TYPE_MAPPING: Record< - string, - (field: PocketBaseField) => string -> = { - text: () => 'z.string()', - richtext: () => 'z.string()', - number: () => 'z.number()', - bool: () => 'z.boolean()', - email: () => 'z.email()', - url: () => 'z.url()', - date: () => 'z.string()', - autodate: () => 'z.string()', - password: () => 'z.string()', - json: () => 'z.any()', - geoPoint: () => 'z.object({ lat: z.number(), lon: z.number() })', - select: field => { - const values = [...(field.values ?? []), ...(field.required ? [] : [''])] - const enumSchema = `z.enum(${JSON.stringify(values)})` - - return (field.maxSelect ?? 1) > 1 ? `z.array(${enumSchema})` : enumSchema - }, - file: field => { - const baseSchema = 'z.string()' - - return (field.maxSelect ?? 1) > 1 ? `z.array(${baseSchema})` : baseSchema - }, - relation: field => { - const baseSchema = 'z.string()' - - return (field.maxSelect ?? 1) > 1 ? `z.array(${baseSchema})` : baseSchema - } -} /** * Prettier formatting options for generated files diff --git a/tools/src/commands/db/utils/file-utils.ts b/tools/src/commands/db/utils/file-utils.ts index 04a451e0b..631433986 100644 --- a/tools/src/commands/db/utils/file-utils.ts +++ b/tools/src/commands/db/utils/file-utils.ts @@ -67,12 +67,14 @@ export function getSchemaFiles(targetModule?: string): string[] { /** * Imports schema modules from file paths */ -export async function importSchemaModules(schemaFiles: string[]): Promise< +export async function importSchemaModules(targetModule?: string): Promise< Array<{ moduleName: string schema: Record }> }> > { + const schemaFiles = getSchemaFiles(targetModule) + return Promise.all( schemaFiles.map(async schemaPath => { const module = await import(path.resolve(schemaPath)) diff --git a/tools/src/commands/db/utils/index.ts b/tools/src/commands/db/utils/index.ts index d20211ae8..a6ba0b345 100644 --- a/tools/src/commands/db/utils/index.ts +++ b/tools/src/commands/db/utils/index.ts @@ -5,8 +5,4 @@ export { importSchemaModules } from './file-utils' -export { - FIELD_TYPE_MAPPING, - PRETTIER_OPTIONS, - SCHEMA_PATTERNS -} from './constants' +export { PRETTIER_OPTIONS, SCHEMA_PATTERNS } from './constants' diff --git a/tools/src/commands/db/utils/pocketbase-utils.ts b/tools/src/commands/db/utils/pocketbase-utils.ts index 2a6bbf778..17a357ce4 100644 --- a/tools/src/commands/db/utils/pocketbase-utils.ts +++ b/tools/src/commands/db/utils/pocketbase-utils.ts @@ -1,4 +1,3 @@ -import chalk from 'chalk' import { execSync } from 'child_process' import fs from 'fs' import path from 'path' @@ -39,11 +38,12 @@ export async function cleanupOldMigrations( } ) + const removedCount = migrationFiles.filter(file => + file.endsWith(`_${targetModule}.js`) + ).length + Logging.debug( - `Removed ${chalk.bold.blue( - migrationFiles.filter(file => file.endsWith(`_${targetModule}.js`)) - .length - )} old migrations for module ${chalk.bold.blue(targetModule)}.` + `Removed ${Logging.highlight(String(removedCount))} old migrations for module ${Logging.highlight(targetModule)}.` ) } } catch { diff --git a/tools/src/commands/dev/config/commands.ts b/tools/src/commands/dev/config/commands.ts index 5d3938610..28ef350e8 100644 --- a/tools/src/commands/dev/config/commands.ts +++ b/tools/src/commands/dev/config/commands.ts @@ -1,13 +1,8 @@ -import chalk from 'chalk' import fs from 'fs' import { PB_BINARY_PATH, PB_KWARGS } from '@/constants/db' -import { - checkPortInUse, - delay, - executeCommand, - killExistingProcess -} from '@/utils/helpers' +import executeCommand from '@/utils/commands' +import { checkPortInUse, delay, killExistingProcess } from '@/utils/helpers' import Logging from '@/utils/logging' /** @@ -39,7 +34,7 @@ export const SERVICE_COMMANDS: Record = { if (!fs.existsSync(PB_BINARY_PATH)) { Logging.actionableError( `PocketBase binary does not exist: ${PB_BINARY_PATH}`, - `Please run "${chalk.bold.blue('bun forge db init')}" to initialize the database` + `Please run "${Logging.highlight('bun forge db init')}" to initialize the database` ) process.exit(1) } diff --git a/tools/src/commands/dev/functions/startServices.ts b/tools/src/commands/dev/functions/startServices.ts index b9c5763e6..5f49106dd 100644 --- a/tools/src/commands/dev/functions/startServices.ts +++ b/tools/src/commands/dev/functions/startServices.ts @@ -2,7 +2,8 @@ import concurrently from 'concurrently' import { PROJECTS } from '@/commands/project/constants/projects' import { TOOLS_ALLOWED } from '@/constants/constants' -import { executeCommand, getEnvVars } from '@/utils/helpers' +import executeCommand from '@/utils/commands' +import { getEnvVars } from '@/utils/helpers' import Logging from '@/utils/logging' import { SERVICE_COMMANDS } from '../config/commands' @@ -51,8 +52,6 @@ export async function startSingleService( * Starts all development services concurrently */ export async function startAllServices(): Promise { - Logging.progress('Starting all services: database, server, and client') - try { const concurrentServices = await getConcurrentServices() diff --git a/tools/src/commands/dev/functions/validateServices.ts b/tools/src/commands/dev/functions/validateServices.ts index aebbba80d..1d7bc67a5 100644 --- a/tools/src/commands/dev/functions/validateServices.ts +++ b/tools/src/commands/dev/functions/validateServices.ts @@ -1,5 +1,3 @@ -import Logging from '@/utils/logging' - import SERVICES from '../constants/services' /** @@ -7,7 +5,6 @@ import SERVICES from '../constants/services' */ export default function validateService(service: string): void { if (service && !SERVICES.includes(service)) { - Logging.options(`Invalid service: "${service}"`, [...SERVICES]) process.exit(1) } } diff --git a/tools/src/commands/dev/handlers/devHandler.ts b/tools/src/commands/dev/handlers/devHandler.ts index caae81932..f601fda01 100644 --- a/tools/src/commands/dev/handlers/devHandler.ts +++ b/tools/src/commands/dev/handlers/devHandler.ts @@ -10,12 +10,13 @@ export function devHandler(service: string, extraArgs: string[] = []): void { validateService(service) if (!service) { + Logging.info('Starting all services...') startAllServices() return } - Logging.progress(`Starting ${service} service`) + Logging.info(`Starting ${Logging.highlight(service)} service...`) if (extraArgs.length > 0) { Logging.debug(`Extra arguments: ${extraArgs.join(' ')}`) @@ -25,7 +26,7 @@ export function devHandler(service: string, extraArgs: string[] = []): void { startSingleService(service, extraArgs) } catch (error) { Logging.actionableError( - `Failed to start ${service} service`, + `Failed to start ${Logging.highlight(service)} service`, 'Check if all required dependencies are installed and environment variables are set' ) Logging.debug(`Error details: ${error}`) diff --git a/tools/src/commands/locales/functions/ensureLocaleNotInUse.ts b/tools/src/commands/locales/functions/ensureLocaleNotInUse.ts index 667fab538..94c849579 100644 --- a/tools/src/commands/locales/functions/ensureLocaleNotInUse.ts +++ b/tools/src/commands/locales/functions/ensureLocaleNotInUse.ts @@ -6,21 +6,17 @@ async function ensureLocaleNotInUse(shortName: string) { const { pb, killPB } = await getPBInstance() - try { - const user = await pb.collection('users').getFirstListItem("id != ''") + const user = await pb.collection('users').getFirstListItem("id != ''") - if (user.language === shortName) { - Logging.actionableError( - `Cannot uninstall locale "${shortName}"`, - 'This language is currently selected. Change your language first.' - ) + killPB?.() - killPB?.() + if (user.language === shortName) { + Logging.actionableError( + `Cannot uninstall locale "${shortName}"`, + 'This language is currently selected. Change your language first.' + ) - process.exit(1) - } - } finally { - killPB?.() + process.exit(1) } } diff --git a/tools/src/commands/locales/functions/getLocalesMeta.ts b/tools/src/commands/locales/functions/getLocalesMeta.ts deleted file mode 100644 index 5b24d42b0..000000000 --- a/tools/src/commands/locales/functions/getLocalesMeta.ts +++ /dev/null @@ -1,35 +0,0 @@ -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/src/commands/locales/functions/getPackagesToCheck.ts b/tools/src/commands/locales/functions/getPackagesToCheck.ts index 3b7b37195..9dc2a04ad 100644 --- a/tools/src/commands/locales/functions/getPackagesToCheck.ts +++ b/tools/src/commands/locales/functions/getPackagesToCheck.ts @@ -1,7 +1,7 @@ import Logging from '@/utils/logging' +import normalizePackage from '@/utils/normalizePackage' import { getInstalledLocalesWithMeta } from './getInstalledLocales' -import { normalizeLocalePackageName } from './getLocalesMeta' function getPackagesToCheck(langCode?: string) { const localePackages = getInstalledLocalesWithMeta() @@ -14,7 +14,7 @@ function getPackagesToCheck(langCode?: string) { const packagesToCheck = langCode ? localePackages.filter( - p => p.name === normalizeLocalePackageName(langCode) + p => p.name === normalizePackage(langCode, 'locale').fullName ) : localePackages diff --git a/tools/src/commands/locales/functions/getUpgrades.ts b/tools/src/commands/locales/functions/getUpgrades.ts index e3f8afc66..ec0e82f26 100644 --- a/tools/src/commands/locales/functions/getUpgrades.ts +++ b/tools/src/commands/locales/functions/getUpgrades.ts @@ -47,13 +47,16 @@ async function getUpgrades( } if (!upgrades.length) { - Logging.success('All locales are up to date!') - + Logging.info('All locales are up to date') process.exit(0) } Logging.info('Available upgrades:') - upgrades.forEach(u => Logging.info(` ${u.name}: ${u.current} → ${u.latest}`)) + upgrades.forEach(u => + Logging.print( + ` ${Logging.highlight(u.name)}: ${u.current} → ${Logging.green(u.latest)}` + ) + ) return upgrades } diff --git a/tools/src/commands/locales/functions/installAndMoveLocales.ts b/tools/src/commands/locales/functions/installAndMoveLocales.ts index ddeb95487..13b5f6f5b 100644 --- a/tools/src/commands/locales/functions/installAndMoveLocales.ts +++ b/tools/src/commands/locales/functions/installAndMoveLocales.ts @@ -1,6 +1,7 @@ import fs from 'fs' -import { executeCommand } from '@/utils/helpers' +import executeCommand from '@/utils/commands' +import Logging, { LEVEL_ORDER } from '@/utils/logging' import { addDependency } from '@/utils/packageJson' function installAndMoveLocales(fullPackageName: string, targetDir: string) { @@ -8,9 +9,13 @@ function installAndMoveLocales(fullPackageName: string, targetDir: string) { fs.rmSync(targetDir, { recursive: true, force: true }) } + Logging.debug( + `Installing ${Logging.highlight(fullPackageName)} from registry...` + ) + executeCommand(`bun add ${fullPackageName}@latest`, { cwd: process.cwd(), - stdio: 'inherit' + stdio: Logging.level > LEVEL_ORDER['info'] ? 'pipe' : 'inherit' }) const installedPath = `${process.cwd()}/node_modules/${fullPackageName}` @@ -19,12 +24,16 @@ function installAndMoveLocales(fullPackageName: string, targetDir: string) { throw new Error(`Failed to install ${fullPackageName}`) } + Logging.debug( + `Copying ${Logging.highlight(fullPackageName)} to ${targetDir}...` + ) + fs.cpSync(installedPath, targetDir, { recursive: true }) addDependency(fullPackageName) fs.rmSync(installedPath, { recursive: true, force: true }) - executeCommand('bun install', { cwd: process.cwd(), stdio: 'inherit' }) + } export default installAndMoveLocales diff --git a/tools/src/commands/locales/functions/setFirstLangInDB.ts b/tools/src/commands/locales/functions/setFirstLangInDB.ts index 07ca751cc..7cf1f0819 100644 --- a/tools/src/commands/locales/functions/setFirstLangInDB.ts +++ b/tools/src/commands/locales/functions/setFirstLangInDB.ts @@ -7,7 +7,7 @@ async function setFirstLangInDB(shortName: string) { const installedLocales = getInstalledLocales() if (installedLocales.length === 1) { - Logging.step('First language pack - setting as default for user') + Logging.debug('This is the first locale, setting as default...') const { pb, killPB } = await getPBInstance() @@ -15,7 +15,7 @@ async function setFirstLangInDB(shortName: string) { await pb.collection('users').update(user.id, { language: shortName }) - Logging.info(`Set ${shortName} as default language`) + Logging.info(`Set ${Logging.highlight(shortName)} as default language`) killPB?.() } } diff --git a/tools/src/commands/locales/handlers/install-locale.ts b/tools/src/commands/locales/handlers/installLocaleHandler.ts similarity index 63% rename from tools/src/commands/locales/handlers/install-locale.ts rename to tools/src/commands/locales/handlers/installLocaleHandler.ts index 96c1d13f7..21a49bc48 100644 --- a/tools/src/commands/locales/handlers/install-locale.ts +++ b/tools/src/commands/locales/handlers/installLocaleHandler.ts @@ -1,13 +1,16 @@ import fs from 'fs' import Logging from '@/utils/logging' +import normalizePackage from '@/utils/normalizePackage' -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) + const { fullName, shortName, targetDir } = normalizePackage( + langCode, + 'locale' + ) if (fs.existsSync(targetDir)) { Logging.actionableError( @@ -18,17 +21,17 @@ export async function installLocaleHandler(langCode: string): Promise { process.exit(1) } - Logging.progress('Fetching locale from registry...') + Logging.info(`Installing ${Logging.highlight(fullName)}...`) try { - installAndMoveLocales(fullPackageName, targetDir) + installAndMoveLocales(fullName, targetDir) await setFirstLangInDB(shortName) - Logging.success(`Locale ${fullPackageName} installed successfully!`) + Logging.success(`Installed ${Logging.highlight(fullName)}`) } catch (error) { Logging.actionableError( - `Failed to install ${fullPackageName}`, + `Failed to install ${Logging.highlight(fullName)}`, 'Make sure the locale exists in the registry' ) throw error diff --git a/tools/src/commands/locales/handlers/list-locales.ts b/tools/src/commands/locales/handlers/listLocalesHandler.ts similarity index 80% rename from tools/src/commands/locales/handlers/list-locales.ts rename to tools/src/commands/locales/handlers/listLocalesHandler.ts index 3f602d826..4853f12c4 100644 --- a/tools/src/commands/locales/handlers/list-locales.ts +++ b/tools/src/commands/locales/handlers/listLocalesHandler.ts @@ -1,5 +1,3 @@ -import chalk from 'chalk' - import Logging from '@/utils/logging' import { getInstalledLocalesWithMeta } from '../functions/getInstalledLocales' @@ -19,8 +17,8 @@ export function listLocalesHandler(): void { Logging.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})` + Logging.print( + ` ${Logging.highlight(locale.name)} - ${locale.displayName} (v.${locale.version})` ) } } diff --git a/tools/src/commands/locales/handlers/publish-locale.ts b/tools/src/commands/locales/handlers/publishLocaleHandler.ts similarity index 68% rename from tools/src/commands/locales/handlers/publish-locale.ts rename to tools/src/commands/locales/handlers/publishLocaleHandler.ts index ba253ec64..2de3d3be7 100644 --- a/tools/src/commands/locales/handlers/publish-locale.ts +++ b/tools/src/commands/locales/handlers/publishLocaleHandler.ts @@ -1,18 +1,18 @@ import fs from 'fs' -import { executeCommand } from '@/utils/helpers' +import executeCommand from '@/utils/commands' import Logging from '@/utils/logging' +import normalizePackage from '@/utils/normalizePackage' 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) + const { fullName, targetDir } = normalizePackage(langCode, 'locale') if (!fs.existsSync(targetDir)) { Logging.actionableError( @@ -23,24 +23,28 @@ export async function publishLocaleHandler( return } + Logging.info('Validating locale structure...') validateLocaleStructure(targetDir) const auth = await checkAuth() if (options?.official) { + Logging.info('Validating maintainer access...') validateMaintainerAccess(auth.username ?? '') } + Logging.info(`Publishing ${Logging.highlight(fullName)}...`) + try { executeCommand(`npm publish --registry ${getRegistryUrl()}`, { cwd: targetDir, stdio: 'inherit' }) - Logging.success(`Locale "${shortName}" published successfully!`) + Logging.success(`Published ${Logging.highlight(fullName)}`) } catch (error) { Logging.actionableError( - 'Failed to publish locale', + `Failed to publish ${Logging.highlight(fullName)}`, 'Check if you are properly authenticated with the registry' ) throw error diff --git a/tools/src/commands/locales/handlers/uninstall-locale.ts b/tools/src/commands/locales/handlers/uninstallLocaleHandler.ts similarity index 55% rename from tools/src/commands/locales/handlers/uninstall-locale.ts rename to tools/src/commands/locales/handlers/uninstallLocaleHandler.ts index 00f5565e9..a72f35cff 100644 --- a/tools/src/commands/locales/handlers/uninstall-locale.ts +++ b/tools/src/commands/locales/handlers/uninstallLocaleHandler.ts @@ -1,16 +1,19 @@ import fs from 'fs' -import { executeCommand } from '@/utils/helpers' +import { installDependencies } from '@/utils/commands' import Logging from '@/utils/logging' +import normalizePackage from '@/utils/normalizePackage' import { findPackageName, removeDependency } from '@/utils/packageJson' import ensureLocaleNotInUse from '../functions/ensureLocaleNotInUse' -import getLocalesMeta from '../functions/getLocalesMeta' export async function uninstallLocaleHandler(langCode: string): Promise { - const { fullPackageName, shortName, targetDir } = getLocalesMeta(langCode) + const { fullName, shortName, targetDir } = normalizePackage( + langCode, + 'locale' + ) - const found = findPackageName(fullPackageName) + const found = findPackageName(fullName) if (!found) { Logging.actionableError( @@ -23,13 +26,13 @@ export async function uninstallLocaleHandler(langCode: string): Promise { await ensureLocaleNotInUse(shortName) - Logging.info(`Uninstalling locale ${fullPackageName}...`) + Logging.info(`Uninstalling ${Logging.highlight(fullName)}...`) fs.rmSync(targetDir, { recursive: true, force: true }) - removeDependency(fullPackageName) + removeDependency(fullName) - executeCommand('bun install', { cwd: process.cwd(), stdio: 'inherit' }) + installDependencies() - Logging.info(`Uninstalled locale ${fullPackageName}`) + Logging.success(`Uninstalled ${Logging.highlight(fullName)}`) } diff --git a/tools/src/commands/locales/handlers/upgrade-locale.ts b/tools/src/commands/locales/handlers/upgradeLocalesHandler.ts similarity index 57% rename from tools/src/commands/locales/handlers/upgrade-locale.ts rename to tools/src/commands/locales/handlers/upgradeLocalesHandler.ts index 7c04d3199..e2e1ace31 100644 --- a/tools/src/commands/locales/handlers/upgrade-locale.ts +++ b/tools/src/commands/locales/handlers/upgradeLocalesHandler.ts @@ -1,8 +1,9 @@ -import { confirmAction, executeCommand } from '@/utils/helpers' +import { installDependencies } from '@/utils/commands' +import { confirmAction } from '@/utils/helpers' import Logging from '@/utils/logging' +import normalizePackage from '@/utils/normalizePackage' 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' @@ -19,21 +20,27 @@ export async function upgradeLocaleHandler(langCode?: string): Promise { let upgradedCount = 0 for (const upgrade of upgrades) { + Logging.info(`Upgrading ${Logging.highlight(upgrade.name)}...`) + try { installAndMoveLocales( upgrade.name, - getLocalesMeta(upgrade.name).targetDir + normalizePackage(upgrade.name, 'locale').targetDir ) - Logging.success(`Upgraded ${upgrade.name} to ${upgrade.latest}`) + Logging.success(`Upgraded ${Logging.highlight(upgrade.name)}`) upgradedCount++ } catch (error) { - Logging.error(`Failed to upgrade ${upgrade.name}: ${error}`) + Logging.error( + `Failed to upgrade ${Logging.highlight(upgrade.name)}: ${error}` + ) } } if (upgradedCount > 0) { - executeCommand('bun install', { cwd: process.cwd(), stdio: 'inherit' }) - Logging.success(`Upgraded ${upgradedCount} locale(s)`) + installDependencies() + Logging.success( + `Upgraded ${upgradedCount} locale${upgradedCount > 1 ? 's' : ''}` + ) } } diff --git a/tools/src/commands/locales/index.ts b/tools/src/commands/locales/index.ts index daeb55f67..0d38090be 100644 --- a/tools/src/commands/locales/index.ts +++ b/tools/src/commands/locales/index.ts @@ -1,10 +1,10 @@ import type { Command } from 'commander' -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' +import { installLocaleHandler } from './handlers/installLocaleHandler' +import { listLocalesHandler } from './handlers/listLocalesHandler' +import { publishLocaleHandler } from './handlers/publishLocaleHandler' +import { uninstallLocaleHandler } from './handlers/uninstallLocaleHandler' +import { upgradeLocaleHandler } from './handlers/upgradeLocalesHandler' export default function setup(program: Command): void { const command = program diff --git a/tools/src/commands/modules/functions/getFsMetadata.ts b/tools/src/commands/modules/functions/getFsMetadata.ts deleted file mode 100644 index f1cef4081..000000000 --- a/tools/src/commands/modules/functions/getFsMetadata.ts +++ /dev/null @@ -1,24 +0,0 @@ -import fs from 'fs' -import path from 'path' - -export default function getFsMetadata(moduleName: string) { - const fullName = moduleName.startsWith('@lifeforge/') - ? moduleName - : `@lifeforge/${moduleName}` - - const shortName = fullName.replace('@lifeforge/', '') - - const appsDir = path.join(process.cwd(), 'apps') - - if (!fs.existsSync(appsDir)) { - fs.mkdirSync(appsDir, { recursive: true }) - } - - const targetDir = path.join(appsDir, shortName) - - return { - fullName, - shortName, - targetDir - } -} diff --git a/tools/src/commands/modules/functions/installModulePackage.ts b/tools/src/commands/modules/functions/installModulePackage.ts index 2b426fb1a..3aaba69f7 100644 --- a/tools/src/commands/modules/functions/installModulePackage.ts +++ b/tools/src/commands/modules/functions/installModulePackage.ts @@ -1,8 +1,8 @@ import fs from 'fs' import path from 'path' -import { executeCommand } from '@/utils/helpers' -import Logging from '@/utils/logging' +import executeCommand from '@/utils/commands' +import Logging, { LEVEL_ORDER } from '@/utils/logging' /** * Installs a module package from the registry and copies it to the target directory. @@ -14,18 +14,24 @@ export default function installModulePackage( fullName: string, targetDir: string ) { + Logging.debug(`Installing ${Logging.highlight(fullName)} from registry...`) + executeCommand(`bun add ${fullName}@latest`, { cwd: process.cwd(), - stdio: 'inherit' + stdio: Logging.level > LEVEL_ORDER['info'] ? 'pipe' : 'inherit' }) const installedPath = path.join(process.cwd(), 'node_modules', fullName) if (!fs.existsSync(installedPath)) { - Logging.error(`Failed to install ${fullName}`) + Logging.actionableError( + `Failed to install ${Logging.highlight(fullName)}`, + 'Check if the package exists in the registry' + ) process.exit(1) } + Logging.debug(`Copying ${Logging.highlight(fullName)} to ${targetDir}...`) fs.cpSync(installedPath, targetDir, { recursive: true }) } diff --git a/tools/src/commands/modules/functions/linkModuleToWorkspace.ts b/tools/src/commands/modules/functions/linkModuleToWorkspace.ts index 99d47fb2d..794d51e4a 100644 --- a/tools/src/commands/modules/functions/linkModuleToWorkspace.ts +++ b/tools/src/commands/modules/functions/linkModuleToWorkspace.ts @@ -1,7 +1,8 @@ import fs from 'fs' import path from 'path' -import { executeCommand } from '@/utils/helpers' +import { installDependencies } from '@/utils/commands' +import Logging from '@/utils/logging' import { addDependency } from '@/utils/packageJson' /** @@ -12,6 +13,8 @@ import { addDependency } from '@/utils/packageJson' * @param fullName The full name of the module */ export default function linkModuleToWorkspace(fullName: string) { + Logging.debug(`Linking ${Logging.highlight(fullName)} to workspace...`) + addDependency(fullName) const nodeModulesPath = path.join(process.cwd(), 'node_modules', fullName) @@ -20,8 +23,7 @@ export default function linkModuleToWorkspace(fullName: string) { fs.rmSync(nodeModulesPath, { recursive: true, force: true }) } - executeCommand('bun install', { - cwd: process.cwd(), - stdio: 'inherit' - }) + installDependencies() + + Logging.debug(`Linked ${Logging.highlight(fullName)} to workspace`) } diff --git a/tools/src/commands/modules/functions/listModules.ts b/tools/src/commands/modules/functions/listModules.ts index 64d5590e4..7a3b97995 100644 --- a/tools/src/commands/modules/functions/listModules.ts +++ b/tools/src/commands/modules/functions/listModules.ts @@ -4,7 +4,7 @@ import path from 'path' import Logging from '@/utils/logging' import { readRootPackageJson } from '@/utils/packageJson' -import getFsMetadata from './getFsMetadata' +import normalizePackage from '../../../utils/normalizePackage' interface ModuleBasicInfo { name: string @@ -33,7 +33,7 @@ export default function listModules( const modules: Record = {} for (const module of allModules) { - const { targetDir } = getFsMetadata(module) + const { targetDir } = normalizePackage(module) const packageJson = JSON.parse( fs.readFileSync(path.join(targetDir, 'package.json'), 'utf-8') diff --git a/tools/src/commands/modules/functions/registry/namespaceUtils.ts b/tools/src/commands/modules/functions/parsePackageName.ts similarity index 62% rename from tools/src/commands/modules/functions/registry/namespaceUtils.ts rename to tools/src/commands/modules/functions/parsePackageName.ts index f3b748713..0bc85fab2 100644 --- a/tools/src/commands/modules/functions/registry/namespaceUtils.ts +++ b/tools/src/commands/modules/functions/parsePackageName.ts @@ -7,15 +7,27 @@ import Logging from '@/utils/logging' * - "melvinchia3636--invoice-maker" → { username: "melvinchia3636", moduleName: "invoice_maker" } * - "lifeforge--achievements" → { moduleName: "achievements" } (official, no username) */ -export function parsePackageName(packageName: string): { +export function parsePackageName( + packageName: string, + isLibModule?: boolean +): { username?: string moduleName: string } { const withoutScope = packageName.replace(/^@lifeforge\//, '') if (!withoutScope.includes('--')) { - Logging.error(`Invalid package name: ${packageName}`) - process.exit(1) + if (!isLibModule) { + Logging.actionableError( + `Invalid package name: ${Logging.highlight(packageName)}`, + 'Package name must include "--" separator (e.g., username--module-name)' + ) + process.exit(1) + } + + return { + moduleName: withoutScope.replace(/-/g, '_') + } } const [username, moduleName] = withoutScope.split('--', 2) diff --git a/tools/src/commands/modules/functions/prompts/module-type.ts b/tools/src/commands/modules/functions/prompts/module-type.ts index 9efcff6e4..fbe5add82 100644 --- a/tools/src/commands/modules/functions/prompts/module-type.ts +++ b/tools/src/commands/modules/functions/prompts/module-type.ts @@ -1,4 +1,3 @@ -import chalk from 'chalk' import fs from 'fs' import prompts from 'prompts' @@ -16,7 +15,7 @@ export async function promptModuleType(): Promise< message: 'Select the type of module to create:', choices: Object.entries(AVAILABLE_TEMPLATE_MODULE_TYPES).map( ([value, title]) => ({ - title: `${title} ${chalk.dim(`(${value})`)}`, + title: `${title} ${Logging.dim(`(${value})`)}`, value }) ) diff --git a/tools/src/commands/modules/functions/registry/generateSchemaRegistry.ts b/tools/src/commands/modules/functions/registry/generateSchemaRegistry.ts index fe5f0eb1c..f3d29f462 100644 --- a/tools/src/commands/modules/functions/registry/generateSchemaRegistry.ts +++ b/tools/src/commands/modules/functions/registry/generateSchemaRegistry.ts @@ -1,15 +1,19 @@ import fs from 'fs' import path from 'path' -import getFsMetadata from '../getFsMetadata' +import { SERVER_SCHEMA_PATH } from '@/constants/constants' + +import normalizePackage from '../../../../utils/normalizePackage' import listModules from '../listModules' -import { parsePackageName } from './namespaceUtils' +import { parsePackageName } from '../parsePackageName' export default function generateSchemaRegistry(): string { const modules = Object.keys(listModules()) const modulesWithSchema = modules.filter(mod => - fs.existsSync(path.join(getFsMetadata(mod).targetDir, 'server/schema.ts')) + fs.existsSync( + path.join(normalizePackage(mod).targetDir, 'server/schema.ts') + ) ) const moduleSchemas = modulesWithSchema @@ -22,17 +26,15 @@ export default function generateSchemaRegistry(): string { }) .join('\n') - return `// AUTO-GENERATED - DO NOT EDIT + const registry = `// 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, +const SCHEMAS = { ${moduleSchemas} } -const COLLECTION_SCHEMAS = flattenSchemas(SCHEMAS) - -export default COLLECTION_SCHEMAS +export default SCHEMAS ` + + fs.writeFileSync(SERVER_SCHEMA_PATH, registry) } diff --git a/tools/src/commands/modules/functions/registry/generateServerRegistry.ts b/tools/src/commands/modules/functions/registry/generateServerRegistry.ts index 199332b51..c712ef5aa 100644 --- a/tools/src/commands/modules/functions/registry/generateServerRegistry.ts +++ b/tools/src/commands/modules/functions/registry/generateServerRegistry.ts @@ -1,15 +1,17 @@ import fs from 'fs' import path from 'path' -import getFsMetadata from '../getFsMetadata' +import { SERVER_ROUTES_PATH } from '@/constants/constants' + +import normalizePackage from '../../../../utils/normalizePackage' import listModules from '../listModules' -import { parsePackageName } from './namespaceUtils' +import { parsePackageName } from '../parsePackageName' export default function generateServerRegistry(): string { const modules = Object.keys(listModules(true)) const modulesWithServer = modules.filter(mod => - fs.existsSync(path.join(getFsMetadata(mod).targetDir, 'server/index.ts')) + fs.existsSync(path.join(normalizePackage(mod).targetDir, 'server/index.ts')) ) if (modulesWithServer.length === 0) { @@ -32,7 +34,7 @@ export default appRoutes }) .join('\n') - return `// AUTO-GENERATED - DO NOT EDIT + const registry = `// AUTO-GENERATED - DO NOT EDIT import { forgeRouter } from '@functions/routes' const appRoutes = forgeRouter({ @@ -41,4 +43,6 @@ ${imports} export default appRoutes ` + + fs.writeFileSync(SERVER_ROUTES_PATH, registry) } diff --git a/tools/src/commands/modules/functions/templates/copy-template.ts b/tools/src/commands/modules/functions/templates/copy-template.ts index 7a9bc1f8b..44a09cf3b 100644 --- a/tools/src/commands/modules/functions/templates/copy-template.ts +++ b/tools/src/commands/modules/functions/templates/copy-template.ts @@ -2,8 +2,6 @@ import fs from 'fs' import Handlebars from 'handlebars' import _ from 'lodash' -import Logging from '@/utils/logging' - import { AVAILABLE_TEMPLATE_MODULE_TYPES } from '../../../../constants/constants' export type ModuleMetadata = { @@ -74,8 +72,6 @@ export function renameTsConfigFile(moduleDir: string): void { } export function copyTemplateFiles(moduleMetadata: ModuleMetadata): void { - Logging.step(`Creating module "${moduleMetadata.moduleName.en}"...`) - const templateDir = `${process.cwd()}/tools/src/templates/${moduleMetadata.moduleType}` const destinationDir = `${process.cwd()}/apps/${_.camelCase(moduleMetadata.moduleName.en)}` @@ -89,8 +85,4 @@ export function copyTemplateFiles(moduleMetadata: ModuleMetadata): void { ) renameTsConfigFile(destinationDir) - - Logging.success( - `Module "${moduleMetadata.moduleName.en}" created successfully at ${destinationDir}` - ) } diff --git a/tools/src/commands/modules/functions/templates/init-git-repo.ts b/tools/src/commands/modules/functions/templates/init-git-repo.ts index df602a46c..6a3087f56 100644 --- a/tools/src/commands/modules/functions/templates/init-git-repo.ts +++ b/tools/src/commands/modules/functions/templates/init-git-repo.ts @@ -1,9 +1,6 @@ -import { executeCommand } from '@/utils/helpers' -import Logging from '@/utils/logging' +import executeCommand from '@/utils/commands' export function initializeGitRepository(modulePath: string): void { - Logging.step('Initializing git repository for the new module...') - executeCommand('git init', { cwd: modulePath, stdio: 'ignore' }) executeCommand('git add .', { cwd: modulePath, stdio: 'ignore' }) executeCommand('git commit -m "feat: initial commit"', { diff --git a/tools/src/commands/modules/handlers/installModuleHandler.ts b/tools/src/commands/modules/handlers/installModuleHandler.ts index a06ef13c4..5af1ae015 100644 --- a/tools/src/commands/modules/handlers/installModuleHandler.ts +++ b/tools/src/commands/modules/handlers/installModuleHandler.ts @@ -5,14 +5,14 @@ import { generateMigrationsHandler } from '@/commands/db/handlers/generateMigrat import Logging from '@/utils/logging' import { checkPackageExists } from '@/utils/registry' -import getFsMetadata from '../functions/getFsMetadata' +import normalizePackage from '../../../utils/normalizePackage' import installModulePackage from '../functions/installModulePackage' import linkModuleToWorkspace from '../functions/linkModuleToWorkspace' import generateSchemaRegistry from '../functions/registry/generateSchemaRegistry' import generateServerRegistry from '../functions/registry/generateServerRegistry' export async function installModuleHandler(moduleName: string): Promise { - const { fullName, shortName, targetDir } = getFsMetadata(moduleName) + const { fullName, shortName, targetDir } = normalizePackage(moduleName) if (fs.existsSync(targetDir)) { Logging.actionableError( @@ -25,22 +25,29 @@ export async function installModuleHandler(moduleName: string): Promise { if (!(await checkPackageExists(fullName))) { Logging.actionableError( - `Module ${fullName} does not exist`, + `Module ${Logging.highlight(fullName)} does not exist`, `Check the module name and try again` ) return } + Logging.info(`Installing ${Logging.highlight(fullName)}...`) + installModulePackage(fullName, targetDir) linkModuleToWorkspace(fullName) + Logging.info('Regenerating registries...') + generateServerRegistry() generateSchemaRegistry() if (fs.existsSync(path.join(targetDir, 'server', 'schema.ts'))) { + Logging.info('Generating database migrations...') generateMigrationsHandler(moduleName) } + + Logging.success(`Installed ${Logging.highlight(fullName)}`) } diff --git a/tools/src/commands/modules/handlers/listModuleHandler.ts b/tools/src/commands/modules/handlers/listModuleHandler.ts index cb73f3579..63b4be28f 100644 --- a/tools/src/commands/modules/handlers/listModuleHandler.ts +++ b/tools/src/commands/modules/handlers/listModuleHandler.ts @@ -1,5 +1,3 @@ -import chalk from 'chalk' - import Logging from '@/utils/logging' import listModules from '../functions/listModules' @@ -17,8 +15,8 @@ export async function listModulesHandler(): Promise { ) Object.entries(modules).forEach(([name, info]) => { - console.log( - ` ${chalk.green(name)} - ${info.displayName} (${info.version})` + Logging.print( + ` ${Logging.highlight(name)} - ${info.displayName} (${info.version})` ) }) } diff --git a/tools/src/commands/modules/handlers/publishModuleHandler.ts b/tools/src/commands/modules/handlers/publishModuleHandler.ts index 58c013e64..cb921deea 100644 --- a/tools/src/commands/modules/handlers/publishModuleHandler.ts +++ b/tools/src/commands/modules/handlers/publishModuleHandler.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' -import { executeCommand } from '@/utils/helpers' +import executeCommand from '@/utils/commands' import Logging from '@/utils/logging' import { getRegistryUrl } from '../../../utils/registry' @@ -19,16 +19,27 @@ export async function publishModuleHandler(moduleName: string): Promise { process.exit(1) } + Logging.info(`Validating module structure...`) await validateModuleStructure(modulePath) + + Logging.info(`Validating module author...`) await validateModuleAuthor(modulePath) + Logging.info(`Publishing ${Logging.highlight(moduleName)}...`) + try { executeCommand(`npm publish --registry ${getRegistryUrl()}`, { cwd: modulePath, stdio: 'inherit' }) + + Logging.success(`Published ${Logging.highlight(moduleName)}`) } catch (error) { - Logging.error(`Publish failed: ${error}`) + Logging.actionableError( + `Publish failed for ${Logging.highlight(moduleName)}`, + 'Check npm authentication and try again' + ) + Logging.debug(`Error: ${error}`) process.exit(1) } } diff --git a/tools/src/commands/modules/handlers/uninstallModuleHandler.ts b/tools/src/commands/modules/handlers/uninstallModuleHandler.ts index 1bfaac42f..9b16f98e9 100644 --- a/tools/src/commands/modules/handlers/uninstallModuleHandler.ts +++ b/tools/src/commands/modules/handlers/uninstallModuleHandler.ts @@ -1,34 +1,40 @@ import fs from 'fs' -import { executeCommand } from '@/utils/helpers' +import { installDependencies } from '@/utils/commands' import Logging from '@/utils/logging' import { findPackageName, removeDependency } from '@/utils/packageJson' -import getFsMetadata from '../functions/getFsMetadata' +import normalizePackage from '../../../utils/normalizePackage' import generateSchemaRegistry from '../functions/registry/generateSchemaRegistry' import generateServerRegistry from '../functions/registry/generateServerRegistry' export async function uninstallModuleHandler( moduleName: string ): Promise { - const { targetDir, fullName } = getFsMetadata(moduleName) + const { targetDir, fullName } = normalizePackage(moduleName) if (!findPackageName(fullName)) { - Logging.error(`Module ${fullName} not found`) + Logging.actionableError( + `Module ${Logging.highlight(fullName)} not found`, + 'Run "bun forge modules list" to see installed modules' + ) return } + Logging.info(`Uninstalling ${Logging.highlight(fullName)}...`) + removeDependency(fullName) fs.rmSync(targetDir, { recursive: true, force: true }) - executeCommand('bun install', { - cwd: process.cwd(), - stdio: 'inherit' - }) + installDependencies() + + Logging.info('Regenerating registries...') generateServerRegistry() generateSchemaRegistry() + + Logging.success(`Uninstalled ${Logging.highlight(fullName)}`) } diff --git a/tools/src/commands/modules/handlers/upgradeModuleHandler.ts b/tools/src/commands/modules/handlers/upgradeModuleHandler.ts index 499a166c6..aa66b2ffc 100644 --- a/tools/src/commands/modules/handlers/upgradeModuleHandler.ts +++ b/tools/src/commands/modules/handlers/upgradeModuleHandler.ts @@ -6,7 +6,7 @@ import { generateMigrationsHandler } from '@/commands/db/handlers/generateMigrat import Logging from '@/utils/logging' import { getPackageLatestVersion } from '@/utils/registry' -import getFsMetadata from '../functions/getFsMetadata' +import normalizePackage from '../../../utils/normalizePackage' import installModulePackage from '../functions/installModulePackage' import linkModuleToWorkspace from '../functions/linkModuleToWorkspace' import listModules from '../functions/listModules' @@ -17,22 +17,28 @@ async function upgradeModule( packageName: string, currentVersion: string ): Promise { - const { fullName, targetDir } = getFsMetadata(packageName) + const { fullName, targetDir } = normalizePackage(packageName) const latestVersion = await getPackageLatestVersion(fullName) if (!latestVersion) { - Logging.warn(`Could not check registry for ${fullName}`) + Logging.warn(`Could not check registry for ${Logging.highlight(fullName)}`) return false } if (semver.eq(currentVersion, latestVersion)) { - Logging.info(`${packageName}@${currentVersion} is up to date`) + Logging.info( + `${Logging.highlight(packageName)}@${currentVersion} is up to date` + ) return false } + Logging.info( + `Upgrading ${Logging.highlight(packageName)} from ${currentVersion} to ${latestVersion}...` + ) + const backupPath = path.join(path.dirname(targetDir), `${packageName}.backup`) try { @@ -50,9 +56,13 @@ async function upgradeModule( fs.rmSync(backupPath, { recursive: true, force: true }) + Logging.success( + `Upgraded ${Logging.highlight(packageName)} to ${latestVersion}` + ) + return true } catch (error) { - Logging.error(`Failed to upgrade ${fullName}: ${error}`) + Logging.error(`Failed to upgrade ${Logging.highlight(fullName)}: ${error}`) if (fs.existsSync(backupPath)) { fs.renameSync(backupPath, targetDir) @@ -68,7 +78,7 @@ export async function upgradeModuleHandler(moduleName?: string): Promise { let upgradedCount = 0 if (moduleName) { - const { fullName } = getFsMetadata(moduleName) + const { fullName } = normalizePackage(moduleName) const mod = modules[fullName] @@ -97,10 +107,16 @@ export async function upgradeModuleHandler(moduleName?: string): Promise { } if (upgradedCount > 0) { + Logging.info('Regenerating registries...') + generateServerRegistry() generateSchemaRegistry() generateMigrationsHandler() + + Logging.success( + `Upgraded ${upgradedCount} module${upgradedCount > 1 ? 's' : ''}` + ) } } diff --git a/tools/src/commands/project/functions/executeProjectCommand.ts b/tools/src/commands/project/functions/executeProjectCommand.ts index c1ccf55e8..94cf0a975 100644 --- a/tools/src/commands/project/functions/executeProjectCommand.ts +++ b/tools/src/commands/project/functions/executeProjectCommand.ts @@ -1,8 +1,4 @@ -import { - executeCommand, - logProcessComplete, - logProcessStart -} from '@/utils/helpers' +import executeCommand from '@/utils/commands' import type { CommandType } from '../constants/commands' import { PROJECTS, type ProjectType } from '../constants/projects' @@ -18,15 +14,12 @@ export function executeProjectCommand( const finalProjects = projects?.length ? projects : allProjectKeys - logProcessStart(commandType, finalProjects) - for (const projectType of finalProjects) { const projectPath = PROJECTS[projectType as ProjectType] - const command = `cd ${projectPath} && bun run ${commandType}` - - executeCommand(command) + executeCommand(`bun run ${commandType}`, { + cwd: projectPath, + stdio: 'pipe' + }) } - - logProcessComplete(commandType) } diff --git a/tools/src/commands/project/functions/validateProjectArguments.ts b/tools/src/commands/project/functions/validateProjectArguments.ts index ffff9fd93..fa3b5fa91 100644 --- a/tools/src/commands/project/functions/validateProjectArguments.ts +++ b/tools/src/commands/project/functions/validateProjectArguments.ts @@ -26,9 +26,9 @@ export function validateProjectArguments(projects: string[] | undefined): void { const validation = validateProjects(projects, validProjects) if (!validation.isValid) { - Logging.options( + Logging.actionableError( `Invalid project(s): ${validation.invalidProjects.join(', ')}`, - validProjects + 'Available projects: ' + validProjects.join(', ') ) process.exit(1) } diff --git a/tools/src/constants/constants.ts b/tools/src/constants/constants.ts index b34301a99..7095d7919 100644 --- a/tools/src/constants/constants.ts +++ b/tools/src/constants/constants.ts @@ -1,10 +1,9 @@ import fs from 'fs' import path from 'path' -/** - * Directory containing all tools - */ -export const TOOLS_DIR = path.join(__dirname, '../../../tools') +export const PROJECT_ROOT = import.meta.dirname.split('/tools')[0] + +const TOOLS_DIR = path.join(PROJECT_ROOT, 'tools') /** * Dynamically discovered tools from the tools directory diff --git a/tools/src/index.ts b/tools/src/index.ts index e5e788ec7..40af40812 100644 --- a/tools/src/index.ts +++ b/tools/src/index.ts @@ -31,5 +31,5 @@ try { await setupCLI() runCLI() } catch (error) { - Logging.fatal(`Unexpected error occurred: ${error}`) + Logging.error(`Unexpected error occurred: ${error}`) } diff --git a/tools/src/types/index.ts b/tools/src/types/index.ts index 711d216c0..88deba052 100644 --- a/tools/src/types/index.ts +++ b/tools/src/types/index.ts @@ -1,4 +1,3 @@ -import type { IOType } from 'child_process' export interface ProjectConfig { path: string @@ -9,10 +8,3 @@ export interface ServiceConfig extends ProjectConfig { command?: string cwd?: string } - -export interface CommandExecutionOptions { - stdio?: IOType | [IOType, IOType, IOType] - cwd?: string - env?: Record - exitOnError?: boolean -} diff --git a/tools/src/utils/commands.ts b/tools/src/utils/commands.ts new file mode 100644 index 000000000..5cc57ab42 --- /dev/null +++ b/tools/src/utils/commands.ts @@ -0,0 +1,76 @@ +import { type IOType, spawnSync } from 'child_process' + +import Logging, { LEVEL_ORDER } from './logging' + +interface CommandExecutionOptions { + stdio?: IOType | [IOType, IOType, IOType] + cwd?: string + env?: Record + exitOnError?: boolean +} + +/** + * Executes a shell command with proper error handling + */ +export default function executeCommand( + command: string | (() => string), + options: CommandExecutionOptions = {}, + _arguments: string[] = [] +): string { + let cmd: string + + try { + cmd = typeof command === 'function' ? command() : command + } catch (error) { + Logging.actionableError( + `Failed to generate command: ${error}`, + 'Check the command generation logic for errors' + ) + process.exit(1) + } + + try { + Logging.debug(`Executing: ${cmd}`) + + const [toBeExecuted, ...args] = cmd.split(' ') + + const result = spawnSync(toBeExecuted, [...args, ..._arguments], { + stdio: 'inherit', + encoding: 'utf8', + shell: true, + ...options + }) + + if (result.error) { + throw result.error + } + + if (result.status !== 0) { + throw result.status + } + + if (!options.stdio || options.stdio === 'inherit') { + Logging.debug(`Completed: ${cmd}`) + } + + return result.stdout?.toString().trim() || '' + } catch (error) { + if (!options.exitOnError) { + throw error + } + + Logging.actionableError( + `Command execution failed: ${cmd}`, + 'Check if the command exists and you have the necessary permissions' + ) + Logging.debug(`Error details: ${error}`) + process.exit(1) + } +} + +export function installDependencies() { + executeCommand('bun install', { + cwd: process.cwd(), + stdio: Logging.level > LEVEL_ORDER['debug'] ? 'pipe' : 'inherit' + }) +} diff --git a/tools/src/utils/github-cli.ts b/tools/src/utils/github-cli.ts index 07f89846e..ccf82cb9e 100644 --- a/tools/src/utils/github-cli.ts +++ b/tools/src/utils/github-cli.ts @@ -1,10 +1,9 @@ -import { executeCommand } from '@/utils/helpers' import Logging from '@/utils/logging' +import executeCommand from './commands' + export function validateMaintainerAccess(username: string): void { try { - Logging.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`, @@ -20,8 +19,6 @@ export function validateMaintainerAccess(username: string): void { const allowedPermissions = ['admin', 'maintain', 'write'] if (allowedPermissions.includes(response.permission)) { - Logging.success(`Verified maintainer access for ${username}`) - return } diff --git a/tools/src/utils/helpers.ts b/tools/src/utils/helpers.ts index 5092845ce..91c652040 100644 --- a/tools/src/utils/helpers.ts +++ b/tools/src/utils/helpers.ts @@ -1,71 +1,9 @@ -import chalk from 'chalk' import { spawnSync } from 'child_process' -import fs from 'fs' -import path from 'path' import prompts from 'prompts' -import type { CommandExecutionOptions } from '../types' +import executeCommand from './commands' import Logging from './logging' -/** - * Executes a shell command with proper error handling - */ -export function executeCommand( - command: string | (() => string), - options: CommandExecutionOptions = {}, - _arguments: string[] = [] -): string { - let cmd: string - - try { - cmd = typeof command === 'function' ? command() : command - } catch (error) { - Logging.actionableError( - `Failed to generate command: ${error}`, - 'Check the command generation logic for errors' - ) - process.exit(1) - } - - try { - Logging.debug(`Executing: ${cmd}`) - - const [toBeExecuted, ...args] = cmd.split(' ') - - const result = spawnSync(toBeExecuted, [...args, ..._arguments], { - stdio: 'inherit', - encoding: 'utf8', - shell: true, - ...options - }) - - if (result.error) { - throw result.error - } - - if (result.status !== 0) { - throw result.status - } - - if (!options.stdio || options.stdio === 'inherit') { - Logging.debug(`Completed: ${cmd}`) - } - - return result.stdout?.toString().trim() || '' - } catch (error) { - if (!options.exitOnError) { - throw error - } - - Logging.actionableError( - `Command execution failed: ${cmd}`, - 'Check if the command exists and you have the necessary permissions' - ) - Logging.debug(`Error details: ${error}`) - process.exit(1) - } -} - /** * Validates environment variables */ @@ -115,29 +53,6 @@ export function getEnvVar(varName: string, fallback?: string): string { process.exit(1) } -/** - * Formats project list for display - */ -export function formatProjectList(projects: string[]): string { - return projects.join(', ') -} - -/** - * Logs a process start message - */ -export function logProcessStart(processType: string, projects: string[]): void { - Logging.step( - `Running ${processType} for ${projects.length} project(s): ${formatProjectList(projects)}` - ) -} - -/** - * Logs a process completion message - */ -export function logProcessComplete(processType: string): void { - Logging.success(`All projects ${processType} completed successfully`) -} - /** * Kills existing processes matching the given keyword */ @@ -149,7 +64,7 @@ export function killExistingProcess( process.kill(processKeywordOrPID) Logging.debug( - `Killed process with PID: ${chalk.bold.blue(processKeywordOrPID)}` + `Killed process with PID: ${Logging.highlight(String(processKeywordOrPID))}` ) return @@ -164,9 +79,7 @@ export function killExistingProcess( executeCommand(`pkill -f "${processKeywordOrPID}"`) Logging.debug( - `Killed process matching keyword: ${chalk.bold.blue( - processKeywordOrPID - )} (PID: ${chalk.bold.blue(serverInstance)})` + `Killed process matching keyword: ${Logging.highlight(processKeywordOrPID)} (PID: ${Logging.highlight(serverInstance)})` ) return parseInt(serverInstance, 10) @@ -176,48 +89,6 @@ export function killExistingProcess( } } -export type PathConfig = { - path: string - type: 'file' | 'directory' - children?: PathConfig[] -} - -export function validateFilePaths( - paths: PathConfig[], - basedir: string -): boolean { - for (const p of paths) { - const { path: pth, type, children } = p - - const fullPath = path.resolve(basedir, pth) - - if (!fs.existsSync(fullPath)) { - Logging.error(`Invalid module structure detected: ${pth} does not exist`) - process.exit(1) - } - - const stats = fs.lstatSync(fullPath) - - if (type === 'file' && !stats.isFile()) { - Logging.error(`Invalid module structure detected: ${pth} is not a file`) - process.exit(1) - } - - if (type === 'directory' && !stats.isDirectory()) { - Logging.error( - `Invalid module structure detected: ${pth} is not a directory` - ) - process.exit(1) - } - - if (children) { - validateFilePaths(children, fullPath) - } - } - - return true -} - export function checkPortInUse(port: number): boolean { try { const result = spawnSync('nc', ['-zv', 'localhost', port.toString()], { @@ -242,35 +113,6 @@ export function isDockerMode(): boolean { return process.env.DOCKER_MODE === 'true' } -/** - * Executes a command and returns the output as a string - */ -export function executeCommandWithOutput( - command: string, - options: CommandExecutionOptions = {} -): string { - const [toBeExecuted, ...args] = command.split(' ') - - const result = spawnSync(toBeExecuted, args, { - stdio: 'pipe', - encoding: 'utf8', - shell: true, - ...options - }) - - if (result.error) { - throw result.error - } - - if (result.status !== 0) { - throw new Error( - `Command failed with exit code ${result.status}: ${result.stderr}` - ) - } - - return result.stdout?.toString().trim() || '' -} - /** * Prompts the user for confirmation */ diff --git a/tools/src/utils/logging.ts b/tools/src/utils/logging.ts index b9b6a4f61..041036090 100644 --- a/tools/src/utils/logging.ts +++ b/tools/src/utils/logging.ts @@ -3,7 +3,7 @@ import { LoggingService } from '@server/core/functions/logging/loggingService' import chalk from 'chalk' -const LEVEL_ORDER = { +export const LEVEL_ORDER = { debug: 1, info: 2, warn: 3, @@ -11,18 +11,63 @@ const LEVEL_ORDER = { fatal: 5 } +type LogLevel = keyof typeof LEVEL_ORDER + /** * CLI Logging service that wraps the server's LoggingService * Provides consistent logging across the entire CLI with file persistence */ export default class Logging { private static readonly SERVICE_NAME = 'CLI' - private static level: number = LEVEL_ORDER['info'] + public static level: number = LEVEL_ORDER['info'] - static setLevel(level: keyof typeof LEVEL_ORDER): void { + static setLevel(level: LogLevel): void { Logging.level = LEVEL_ORDER[level] } + // ───────────────────────────────────────────────────────────── + // Formatting Utilities + // ───────────────────────────────────────────────────────────── + + /** Format text as bold */ + static bold(text: string): string { + return chalk.bold(text) + } + + /** Format text as dim/muted */ + static dim(text: string): string { + return chalk.dim(text) + } + + /** Format text as highlighted (bold blue) - for important values */ + static highlight(text: string): string { + return chalk.bold.blue(text) + } + + /** Format text as green - for success/positive items */ + static green(text: string): string { + return chalk.green(text) + } + + /** Format text as yellow - for warnings */ + static yellow(text: string): string { + return chalk.yellow(text) + } + + /** Format text as red - for errors */ + static red(text: string): string { + return chalk.red(text) + } + + /** Format text as cyan - for info/neutral items */ + static cyan(text: string): string { + return chalk.cyan(text) + } + + // ───────────────────────────────────────────────────────────── + // Core Logging Methods + // ───────────────────────────────────────────────────────────── + /** * Log an informational message */ @@ -77,82 +122,19 @@ export default class Logging { return } - LoggingService.info(chalk.green(`${message}`), this.SERVICE_NAME) + LoggingService.info(`${chalk.green('✔')} ${message}`, this.SERVICE_NAME) } /** - * Log a step in a process with consistent formatting + * Print raw output without log prefix (for lists, tables, etc.) + * Respects log level - only prints if info level is enabled */ - static step(message: string): void { + static print(message: string): void { if (Logging.level > LEVEL_ORDER['info']) { return } - LoggingService.info(chalk.blue(`${message}`), this.SERVICE_NAME) - } - - /** - * Log a process start with spinner-like indicator - */ - static progress(message: string): void { - if (Logging.level > LEVEL_ORDER['info']) { - return - } - - LoggingService.info(chalk.magenta(`${message}...`), this.SERVICE_NAME) - } - - /** - * Display a formatted list of items - */ - static list(title: string, items: string[]): void { - if (Logging.level > LEVEL_ORDER['info']) { - return - } - - this.info(title) - this.newline() - items.forEach((item, index) => { - console.log( - ` ${chalk.cyan((index + 1).toString().padStart(2))}. ${chalk.white(item)}` - ) - }) - this.newline() - } - - /** - * Display available options in a formatted way - */ - static options(title: string, options: string[]): void { - if (Logging.level > LEVEL_ORDER['error']) { - return - } - - this.error(title) - this.error(`Available options: ${options.join(', ')}`) - } - - /** - * Add a visual separator/newline for better readability - */ - static newline(): void { - if (Logging.level > LEVEL_ORDER['info']) { - return - } - - console.log() - } - - /** - * Log a fatal error and exit the process - */ - static fatal(message: string, exitCode = 1): never { - if (Logging.level > LEVEL_ORDER['fatal']) { - process.exit(exitCode) - } - - this.error(chalk.red(`Fatal: ${message}`)) - process.exit(exitCode) + console.log(message) } /** diff --git a/tools/src/utils/normalizePackage.ts b/tools/src/utils/normalizePackage.ts new file mode 100644 index 000000000..96a5ebf7f --- /dev/null +++ b/tools/src/utils/normalizePackage.ts @@ -0,0 +1,65 @@ +import fs from 'fs' +import path from 'path' + +import { PROJECT_ROOT } from '@/constants/constants' + +type PackageType = 'module' | 'locale' + +const TYPE_CONFIG = { + module: { + prefix: '@lifeforge/', + dir: 'apps' + }, + locale: { + prefix: '@lifeforge/lang-', + dir: 'locales' + } +} as const + +/** + * Normalizes a package name and resolves its file system location. + * + * Takes a package name (with or without the `@lifeforge/` prefix) and returns + * the fully qualified package name, short name, and absolute path to the package directory. + * + * @param packageName - The package name to normalize (e.g., "movies" or "@lifeforge/movies") + * @param type - The type of package: 'module' (default) or 'locale' + * @returns Object containing: + * - `fullName` - The fully qualified package name (e.g., "@lifeforge/movies") + * - `shortName` - The package name without the prefix (e.g., "movies") + * - `targetDir` - The absolute path to the package directory + * + * @example + * // Module example + * normalizePackage('movies') + * // { fullName: '@lifeforge/movies', shortName: 'movies', targetDir: '/path/to/apps/movies' } + * + * @example + * // Locale example + * normalizePackage('zh-TW', 'locale') + * // { fullName: '@lifeforge/lang-zh-TW', shortName: 'zh-TW', targetDir: '/path/to/locales/zh-TW' } + */ +export default function normalizePackage( + packageName: string, + type: PackageType = 'module' +) { + const { prefix, dir } = TYPE_CONFIG[type] + + const fullName = packageName.startsWith(prefix) + ? packageName + : `${prefix}${packageName}` + + const shortName = fullName.replace(prefix, '') + + const targetDir = path.join(PROJECT_ROOT, dir, shortName) + + if (!fs.existsSync(path.dirname(targetDir))) { + fs.mkdirSync(path.dirname(targetDir), { recursive: true }) + } + + return { + fullName, + shortName, + targetDir + } +} diff --git a/tools/src/utils/pocketbase.ts b/tools/src/utils/pocketbase.ts index 264eb91c1..a0c937626 100644 --- a/tools/src/utils/pocketbase.ts +++ b/tools/src/utils/pocketbase.ts @@ -1,12 +1,11 @@ -import chalk from 'chalk' import { spawn } from 'child_process' import PocketBase from 'pocketbase' import { PB_BINARY_PATH, PB_KWARGS } from '@/constants/db' -import { executeCommand } from '@/utils/helpers' import { getEnvVars } from '@/utils/helpers' import Logging from '@/utils/logging' +import executeCommand from './commands' import { killExistingProcess } from './helpers' // Triple underscore separates username from module name @@ -30,7 +29,8 @@ const COLLECTION_SEPARATOR = { */ export function parseCollectionName( str: string, - type: 'code' | 'pb' + type: 'code' | 'pb', + fallbackModuleName?: string ): { username?: string moduleName: string @@ -44,8 +44,18 @@ export function parseCollectionName( const [username, remainder] = str.split(userNameSeparator, 2) if (!remainder.includes(collectionSeparator)) { - Logging.error(`Invalid collection name: ${str}`) - process.exit(1) + if (!fallbackModuleName) { + Logging.actionableError( + `Invalid collection name: ${Logging.highlight(str)}`, + 'Collection names must follow the format: module__collection or username___module__collection' + ) + process.exit(1) + } + + return { + moduleName: fallbackModuleName, + collectionName: str + } } const [moduleName, collectionName] = remainder.split(collectionSeparator, 2) @@ -58,8 +68,18 @@ export function parseCollectionName( } if (!str.includes(collectionSeparator)) { - Logging.error(`Invalid collection name: ${str}`) - process.exit(1) + if (!fallbackModuleName) { + Logging.actionableError( + `Invalid collection name: ${Logging.highlight(str)}`, + 'Collection names must follow the format: module__collection or username___module__collection' + ) + process.exit(1) + } + + return { + moduleName: fallbackModuleName, + collectionName: str + } } const [moduleName, collectionName] = str.split(collectionSeparator, 2) @@ -137,6 +157,8 @@ export function checkRunningPBInstances(exitOnError = true): boolean { * Starts a PocketBase server instance */ export async function startPBServer(): Promise { + Logging.debug('Starting PocketBase server...') + return new Promise((resolve, reject) => { const pbProcess = spawn(PB_BINARY_PATH, ['serve', ...PB_KWARGS], { stdio: ['ignore', 'pipe', 'pipe'] @@ -146,28 +168,34 @@ export async function startPBServer(): Promise { const output = data.toString() if (output.includes('Server started')) { + Logging.debug(`PocketBase server started (PID: ${pbProcess.pid})`) resolve(pbProcess.pid!) } if (output.includes('bind: address already in use')) { Logging.actionableError( - 'Port 8090 is already in use by another application.', - 'Please free up the port. Are you using the port for non-pocketbase applications? (e.g., port forwarding, etc.)' + 'Port 8090 is already in use', + 'Run "pkill -f pocketbase" to stop existing instances, or check for other apps using port 8090' ) process.exit(1) } }) pbProcess.stderr?.on('data', data => { - reject(new Error(data.toString())) + const error = data.toString().trim() + + Logging.debug(`PocketBase stderr: ${error}`) + reject(new Error(error)) }) pbProcess.on('error', error => { + Logging.debug(`PocketBase spawn error: ${error.message}`) reject(error) }) pbProcess.on('exit', code => { if (code !== 0) { + Logging.debug(`PocketBase exited with code ${code}`) reject(new Error(`PocketBase process exited with code ${code}`)) } }) @@ -182,34 +210,31 @@ export async function startPocketbase(): Promise<(() => void) | null> { const pbRunning = checkRunningPBInstances(false) if (pbRunning) { - Logging.step('PocketBase server is already running, skipping...') + Logging.debug('PocketBase is already running') return null } - Logging.step('Starting PocketBase server...') - const pbPid = await startPBServer() - Logging.success( - `PocketBase server started successfully with PID ${chalk.bold.blue( - pbPid.toString() - )}` - ) - return () => { killExistingProcess(pbPid) } } catch (error) { - Logging.error( - `Failed to start PocketBase server: ${ - error instanceof Error ? error.message : 'Unknown error' - }` + Logging.actionableError( + `Failed to start PocketBase server: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'Run "bun forge db init" to initialize the database or check if the PocketBase binary exists' ) process.exit(1) } } +/** + * Gets a PocketBase instance. + * + * If `createNewInstance` is true, and there is no existing instance, + * it will start a new PocketBase instance. + */ export default async function getPBInstance(createNewInstance = true): Promise<{ pb: PocketBase killPB: (() => void) | null @@ -236,8 +261,9 @@ export default async function getPBInstance(createNewInstance = true): Promise<{ killPB } } catch (error) { - Logging.error( - `Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}` + Logging.actionableError( + `Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'Check PB_EMAIL and PB_PASSWORD in env/.env.local are correct' ) process.exit(1) } diff --git a/tools/src/utils/registry.ts b/tools/src/utils/registry.ts index 9bcae2b3b..7ea83b615 100644 --- a/tools/src/utils/registry.ts +++ b/tools/src/utils/registry.ts @@ -1,9 +1,10 @@ import fs from 'fs' import path from 'path' -import { executeCommand } from '@/utils/helpers' import Logging from '@/utils/logging' +import executeCommand from './commands' + export function getRegistryUrl(): string { const bunfigPath = path.join(process.cwd(), 'bunfig.toml') @@ -55,8 +56,6 @@ export async function checkAuth(): Promise<{ const username = result?.toString().trim() if (username) { - Logging.success(`Authenticated as ${username}`) - return { authenticated: true, username } }