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