From 3c82811dea61aa3357a10ff3d1f99418fb97312a Mon Sep 17 00:00:00 2001 From: melvinchia3636 Date: Thu, 29 Jan 2026 19:08:36 +0800 Subject: [PATCH] feat(cli): add view and whoami commands, remove dependencies to npm --- bunfig.toml | 2 +- tools/src/cli/commands.ts | 4 +- tools/src/cli/setup.ts | 3 +- .../locales/handlers/publishLocaleHandler.ts | 6 +- .../modules/handlers/compareModuleHandler.ts | 2 +- .../modules/handlers/publishModuleHandler.ts | 8 +- .../modules/handlers/viewModuleHandler.ts | 88 +++++++++++++++++++ tools/src/commands/modules/index.ts | 12 +++ tools/src/commands/whoami/index.ts | 22 +++++ tools/src/utils/registry.ts | 76 ++++++++-------- 10 files changed, 173 insertions(+), 50 deletions(-) create mode 100644 tools/src/commands/modules/handlers/viewModuleHandler.ts create mode 100644 tools/src/commands/whoami/index.ts diff --git a/bunfig.toml b/bunfig.toml index 3a82fca5d..cbf7f2a12 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,3 +1,3 @@ [install] [install.scopes] -"@lifeforge" = "https://registry.lifeforge.dev/" +"@lifeforge" = "https://registry.lifeforge.dev/:_authToken=${FORGISTRY_TOKEN}" diff --git a/tools/src/cli/commands.ts b/tools/src/cli/commands.ts index a479fae9d..465327749 100644 --- a/tools/src/cli/commands.ts +++ b/tools/src/cli/commands.ts @@ -11,6 +11,7 @@ import dev from '@/commands/dev' import locales from '@/commands/locales' import modules from '@/commands/modules' import project from '@/commands/project' +import whoami from '@/commands/whoami' type CommandSetup = (program: Command) => void @@ -20,5 +21,6 @@ export const commands: CommandSetup[] = [ dev, locales, modules, - project + project, + whoami ] diff --git a/tools/src/cli/setup.ts b/tools/src/cli/setup.ts index 282a05ad3..887d02e1a 100644 --- a/tools/src/cli/setup.ts +++ b/tools/src/cli/setup.ts @@ -1,4 +1,5 @@ import { LOG_LEVELS } from '@lifeforge/log' +import chalk from 'chalk' import { Command, program } from 'commander' import fs from 'fs' import path from 'path' @@ -57,7 +58,7 @@ export function setupCLI(): void { cmd = cmd.parent } - logger.debug(`Executing command "${commandPath.join(' ')}"`) + logger.info(`Executing command "${chalk.green(commandPath.join(' '))}"`) }) setupCommands(program) diff --git a/tools/src/commands/locales/handlers/publishLocaleHandler.ts b/tools/src/commands/locales/handlers/publishLocaleHandler.ts index d342e26df..917cba352 100644 --- a/tools/src/commands/locales/handlers/publishLocaleHandler.ts +++ b/tools/src/commands/locales/handlers/publishLocaleHandler.ts @@ -8,13 +8,11 @@ import executeCommand from '@/utils/commands' import logger from '@/utils/logger' import normalizePackage from '@/utils/normalizePackage' -import { checkNPM, getRegistryUrl } from '../../../utils/registry' +import { getRegistryUrl } from '../../../utils/registry' import validateLocalesAuthor from '../functions/validateLocalesAuthor' import { validateLocaleStructureHandler } from './validateLocaleStructure' export async function publishLocaleHandler(langCode: string): Promise { - checkNPM() - const { fullName, targetDir } = normalizePackage(langCode, 'locale') if (!fs.existsSync(targetDir)) { @@ -38,7 +36,7 @@ export async function publishLocaleHandler(langCode: string): Promise { logger.info(`Publishing ${chalk.blue(fullName)}...`) try { - executeCommand(`npm publish --registry ${getRegistryUrl()}`, { + executeCommand(`bun publish --registry ${getRegistryUrl()}`, { cwd: targetDir }) diff --git a/tools/src/commands/modules/handlers/compareModuleHandler.ts b/tools/src/commands/modules/handlers/compareModuleHandler.ts index 51a7843c4..783b9b3d7 100644 --- a/tools/src/commands/modules/handlers/compareModuleHandler.ts +++ b/tools/src/commands/modules/handlers/compareModuleHandler.ts @@ -104,7 +104,7 @@ function compareDirectories(localDir: string, registryDir: string): FileDiff { } /** - * Downloads and extracts an npm package tarball. + * Downloads and extracts a package tarball. */ async function downloadAndExtractTarball( tarballUrl: string, diff --git a/tools/src/commands/modules/handlers/publishModuleHandler.ts b/tools/src/commands/modules/handlers/publishModuleHandler.ts index b4cb98223..51e042752 100644 --- a/tools/src/commands/modules/handlers/publishModuleHandler.ts +++ b/tools/src/commands/modules/handlers/publishModuleHandler.ts @@ -9,7 +9,7 @@ import logger from '@/utils/logger' import bumpPackageVersion, { revertPackageVersion } from '../../../utils/bumpPackageVersion' -import { checkNPM, getRegistryUrl } from '../../../utils/registry' +import { getRegistryUrl } from '../../../utils/registry' import validateModuleAuthor from '../functions/validateModuleAuthor' import validateModuleStructure from '../functions/validateModuleStructure' @@ -52,13 +52,11 @@ function restoreGitignoreAfterPublish(modulePath: string): void { * 2. Validates author permissions * 3. Bumps version in package.json * 4. Renames .gitignore to gitignore (npm excludes .gitignore) - * 5. Publishes to npm registry + * 5. Publishes to LifeForge registry * 6. Restores gitignore to .gitignore * 7. Reverts version on failure */ export async function publishModuleHandler(moduleName: string): Promise { - checkNPM() - const modulePath = path.join(ROOT_DIR, 'apps', moduleName) if (!fs.existsSync(modulePath)) { @@ -84,7 +82,7 @@ export async function publishModuleHandler(moduleName: string): Promise { logger.debug(`Publishing ${chalk.blue(moduleName)}...`) try { - executeCommand(`npm publish --registry ${getRegistryUrl()}`, { + executeCommand(`bun publish --registry ${getRegistryUrl()}`, { cwd: modulePath }) diff --git a/tools/src/commands/modules/handlers/viewModuleHandler.ts b/tools/src/commands/modules/handlers/viewModuleHandler.ts new file mode 100644 index 000000000..e34165fd9 --- /dev/null +++ b/tools/src/commands/modules/handlers/viewModuleHandler.ts @@ -0,0 +1,88 @@ +import chalk from 'chalk' +import _ from 'lodash' + +import logger from '@/utils/logger' +import normalizePackage from '@/utils/normalizePackage' +import { getPackageMetadata } from '@/utils/registry' + +/** + * Views package information from the registry. + * + * @param moduleName - The module name to view (e.g., calendar or @lifeforge/lifeforge--calendar) + */ +export async function viewModuleHandler(moduleName: string): Promise { + const { fullName } = normalizePackage(moduleName) + + const data = await getPackageMetadata(fullName) + + if (!data) { + logger.error(`Package ${chalk.blue(fullName)} not found in registry`) + process.exit(1) + } + + const latestVersion = data['dist-tags']?.latest + + const latest = latestVersion ? data.versions?.[latestVersion] : undefined + + const info = { + version: latestVersion, + versions: data.versions ? Object.keys(data.versions) : [], + deps: latest?.dependencies, + author: + typeof latest?.author === 'string' ? latest.author : latest?.author?.name, + repo: + typeof latest?.repository === 'string' + ? latest.repository + : latest?.repository?.url, + displayName: latest?.displayName, + description: latest?.description, + homepage: latest?.homepage, + license: latest?.license + } + + logger.print('') + logger.print( + info.displayName + ? `${chalk.bold.blue(info.displayName)} ${chalk.dim(`(${data.name || fullName})`)}` + : chalk.bold.blue(data.name || fullName) + ) + + logger.print(info.description) + logger.print('') + + const fields = [ + ['version', info.version ? chalk.green(info.version) : undefined], + ['license', info.license], + ['author', info.author], + ['homepage', info.homepage ? chalk.cyan(info.homepage) : undefined], + ['repository', info.repo ? chalk.cyan(info.repo) : undefined] + ] as const + + fields + .filter(([_, value]) => value) + .forEach(([label, value]) => { + logger.print(`${chalk.dim(label.padEnd(12))} ${value}`) + }) + + if (info.deps && Object.keys(info.deps).length > 0) { + logger.print('') + logger.print(chalk.dim('dependencies')) + + Object.entries(info.deps).forEach(([dep, version]) => { + logger.print(` ${dep} ${chalk.dim(version)}`) + }) + } + + if (info.versions.length > 1) { + logger.print('') + logger.print(chalk.dim(`versions (${info.versions.length})`)) + + const chunkedVersions = _.chunk(info.versions, 8) + + chunkedVersions.forEach((chunk: string[]) => { + logger.print(` ${chunk.join(chalk.dim(' ยท '))}`) + }) + } + + logger.print('') +} diff --git a/tools/src/commands/modules/index.ts b/tools/src/commands/modules/index.ts index 8fe35919c..da1c87b22 100644 --- a/tools/src/commands/modules/index.ts +++ b/tools/src/commands/modules/index.ts @@ -8,6 +8,7 @@ import { listModulesHandler } from './handlers/listModuleHandler' import { publishModuleHandler } from './handlers/publishModuleHandler' import { uninstallModuleHandler } from './handlers/uninstallModuleHandler' import { upgradeModuleHandler } from './handlers/upgradeModuleHandler' +import { viewModuleHandler } from './handlers/viewModuleHandler' export default function setup(program: Command): void { const command = program @@ -85,4 +86,15 @@ export default function setup(program: Command): void { .description('Compare local module content with registry version') .argument('[module]', 'Module to compare (optional, checks all if omitted)') .action(compareModuleHandler) + + command + .command('view') + .alias('v') + .alias('info') + .description('View package info from the registry') + .argument( + '', + 'Module to view, e.g., calendar or @lifeforge/lifeforge--calendar' + ) + .action(viewModuleHandler) } diff --git a/tools/src/commands/whoami/index.ts b/tools/src/commands/whoami/index.ts new file mode 100644 index 000000000..8c7ee6e48 --- /dev/null +++ b/tools/src/commands/whoami/index.ts @@ -0,0 +1,22 @@ +import chalk from 'chalk' +import type { Command } from 'commander' + +import logger from '@/utils/logger' +import { checkAuth, getRegistryUrl } from '@/utils/registry' + +export default function setup(program: Command): void { + program + .command('whoami') + .description('Show the currently authenticated user on the registry') + .action(whoamiHandler) +} + +async function whoamiHandler() { + const registry = getRegistryUrl() + + logger.debug(`Registry: ${chalk.blue(registry)}`) + + const { username } = await checkAuth() + + logger.info(`Logged in as: ${chalk.blue(username)}`) +} diff --git a/tools/src/utils/registry.ts b/tools/src/utils/registry.ts index 50b7b023e..2f610555e 100644 --- a/tools/src/utils/registry.ts +++ b/tools/src/utils/registry.ts @@ -4,22 +4,7 @@ import path from 'path' import { ROOT_DIR } from '@/constants/constants' import logger from '@/utils/logger' -import executeCommand from './commands' - -export function checkNPM(): void { - try { - executeCommand('npm --version', { cwd: ROOT_DIR }) - } catch (err) { - const errorMsg = err instanceof Error ? err.message : String(err) - - if (errorMsg.includes('not found')) { - logger.error( - 'npm not found. Please make sure npm is installed and accessible.' - ) - process.exit(1) - } - } -} +import { getEnvVar } from './helpers' /** * Gets the registry URL for the @lifeforge scope from bunfig.toml. @@ -51,17 +36,16 @@ export function getRegistryUrl(): string { export async function checkPackageExists( packageName: string ): Promise { - checkNPM() - const registry = getRegistryUrl() try { - executeCommand(`npm view ${packageName} --registry ${registry}`, { - cwd: ROOT_DIR, - exitOnError: false - }) + const targetURL = new URL(registry) - return true + targetURL.pathname = packageName + + const response = await fetch(targetURL.toString()) + + return response.ok } catch { return false } @@ -77,23 +61,29 @@ export async function checkAuth(): Promise<{ authenticated: boolean username?: string }> { - checkNPM() - const registry = getRegistryUrl() + const token = getEnvVar('FORGISTRY_AUTH_TOKEN') + try { - const result = executeCommand( - `npm whoami --registry ${registry} 2>/dev/null`, - { - cwd: ROOT_DIR, - exitOnError: false + const targetURL = new URL(registry) + + targetURL.pathname = '/-/whoami' + + const response = await fetch(targetURL.toString(), { + headers: { + Authorization: `Bearer ${token}` } - ) + }) - const username = result?.toString().trim() + if (!response.ok) { + throw new Error('Not authenticated') + } - if (username) { - return { authenticated: true, username } + const data = (await response.json()) as { username?: string } + + if (data.username) { + return { authenticated: true, username: data.username } } throw new Error('Not authenticated') @@ -103,9 +93,21 @@ export async function checkAuth(): Promise<{ } } -interface PackageMetadata { +export interface PackageVersionData { + displayName?: string + description?: string + author?: string | { name?: string } + license?: string + homepage?: string + repository?: { url?: string } | string + dependencies?: Record + dist?: { tarball?: string } +} + +export interface PackageMetadata { + name?: string 'dist-tags'?: { latest?: string } - versions?: Record + versions?: Record } /** @@ -114,7 +116,7 @@ interface PackageMetadata { * @param packageName - The full package name to fetch metadata for * @returns The package metadata, or null if not found */ -async function getPackageMetadata( +export async function getPackageMetadata( packageName: string ): Promise { const registry = getRegistryUrl()