feat(cli): add view and whoami commands, remove dependencies to npm

This commit is contained in:
melvinchia3636
2026-01-29 19:08:36 +08:00
parent 7ffebd6b49
commit 1fcafb486c
10 changed files with 173 additions and 50 deletions

View File

@@ -1,3 +1,3 @@
[install]
[install.scopes]
"@lifeforge" = "https://registry.lifeforge.dev/"
"@lifeforge" = "https://registry.lifeforge.dev/:_authToken=${FORGISTRY_TOKEN}"

View File

@@ -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
]

View File

@@ -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)

View File

@@ -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<void> {
checkNPM()
const { fullName, targetDir } = normalizePackage(langCode, 'locale')
if (!fs.existsSync(targetDir)) {
@@ -38,7 +36,7 @@ export async function publishLocaleHandler(langCode: string): Promise<void> {
logger.info(`Publishing ${chalk.blue(fullName)}...`)
try {
executeCommand(`npm publish --registry ${getRegistryUrl()}`, {
executeCommand(`bun publish --registry ${getRegistryUrl()}`, {
cwd: targetDir
})

View File

@@ -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,

View File

@@ -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<void> {
checkNPM()
const modulePath = path.join(ROOT_DIR, 'apps', moduleName)
if (!fs.existsSync(modulePath)) {
@@ -84,7 +82,7 @@ export async function publishModuleHandler(moduleName: string): Promise<void> {
logger.debug(`Publishing ${chalk.blue(moduleName)}...`)
try {
executeCommand(`npm publish --registry ${getRegistryUrl()}`, {
executeCommand(`bun publish --registry ${getRegistryUrl()}`, {
cwd: modulePath
})

View File

@@ -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<void> {
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('')
}

View File

@@ -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>',
'Module to view, e.g., calendar or @lifeforge/lifeforge--calendar'
)
.action(viewModuleHandler)
}

View File

@@ -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)}`)
}

View File

@@ -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<boolean> {
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<string, string>
dist?: { tarball?: string }
}
export interface PackageMetadata {
name?: string
'dist-tags'?: { latest?: string }
versions?: Record<string, { dist?: { tarball?: string } }>
versions?: Record<string, PackageVersionData>
}
/**
@@ -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<PackageMetadata | null> {
const registry = getRegistryUrl()