Skip to content

Commit ceeb116

Browse files
committed
feat(clawdhub): colorized help with build label
1 parent 5e86028 commit ceeb116

File tree

3 files changed

+150
-2
lines changed

3 files changed

+150
-2
lines changed

packages/clawdhub/src/cli.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
#!/usr/bin/env node
22
import { resolve } from 'node:path'
33
import { Command } from 'commander'
4+
import { getCliBuildLabel, getCliVersion } from './cli/buildInfo.js'
45
import { cmdLoginFlow, cmdLogout, cmdWhoami } from './cli/commands/auth.js'
56
import { cmdPublish } from './cli/commands/publish.js'
67
import { cmdInstall, cmdList, cmdSearch, cmdUpdate } from './cli/commands/skills.js'
8+
import { configureCommanderHelp, styleEnvBlock, styleTitle } from './cli/helpStyle.js'
79
import { DEFAULT_REGISTRY, DEFAULT_SITE } from './cli/registry.js'
810
import type { GlobalOpts } from './cli/types.js'
911
import { fail } from './cli/ui.js'
1012

1113
const program = new Command()
1214
.name('clawdhub')
13-
.description('ClawdHub CLI — install, update, search, and publish agent skills.')
15+
.description(
16+
`${styleTitle(`ClawdHub CLI ${getCliBuildLabel()}`)}\n${styleEnvBlock(
17+
'install, update, search, and publish agent skills.',
18+
)}`,
19+
)
20+
.version(getCliVersion(), '-V, --version', 'Show version')
1421
.option('--workdir <dir>', 'Working directory (default: cwd)')
1522
.option('--dir <dir>', 'Skills directory (relative to workdir, default: skills)')
1623
.option('--site <url>', 'Site base URL (for browser login)')
1724
.option('--registry <url>', 'Registry API base URL')
1825
.option('--no-input', 'Disable prompts')
1926
.showHelpAfterError()
2027
.showSuggestionAfterError()
21-
.addHelpText('after', '\nEnv:\n CLAWDHUB_SITE\n CLAWDHUB_REGISTRY\n')
28+
.addHelpText('after', styleEnvBlock('\nEnv:\n CLAWDHUB_SITE\n CLAWDHUB_REGISTRY\n'))
29+
30+
configureCommanderHelp(program)
2231

2332
function resolveGlobalOpts(): GlobalOpts {
2433
const raw = program.opts<{ workdir?: string; dir?: string; site?: string; registry?: string }>()
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { existsSync, readFileSync, statSync } from 'node:fs'
2+
import { dirname, join, resolve } from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
5+
type PackageJson = { version?: string }
6+
7+
function readPackageVersion() {
8+
try {
9+
const path = join(dirname(fileURLToPath(import.meta.url)), '../../package.json')
10+
const raw = readFileSync(path, 'utf8')
11+
const pkg = JSON.parse(raw) as PackageJson
12+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0'
13+
} catch {
14+
return '0.0.0'
15+
}
16+
}
17+
18+
function shortCommit(value: string) {
19+
const trimmed = value.trim()
20+
if (!trimmed) return null
21+
if (trimmed.length <= 8) return trimmed
22+
return trimmed.slice(0, 8)
23+
}
24+
25+
export function getCliCommit() {
26+
const candidates = [
27+
process.env.CLAWDHUB_COMMIT,
28+
process.env.VERCEL_GIT_COMMIT_SHA,
29+
process.env.GITHUB_SHA,
30+
process.env.COMMIT_SHA,
31+
]
32+
for (const candidate of candidates) {
33+
if (!candidate) continue
34+
const short = shortCommit(candidate)
35+
if (short) return short
36+
}
37+
return readGitCommitFromCwd()
38+
}
39+
40+
export function getCliVersion() {
41+
return readPackageVersion()
42+
}
43+
44+
export function getCliBuildLabel() {
45+
const version = getCliVersion()
46+
const commit = getCliCommit()
47+
return commit ? `v${version} (${commit})` : `v${version}`
48+
}
49+
50+
function readGitCommitFromCwd() {
51+
try {
52+
const gitDir = findGitDir(process.cwd())
53+
if (!gitDir) return null
54+
const headPath = join(gitDir, 'HEAD')
55+
if (!existsSync(headPath)) return null
56+
const head = readFileSync(headPath, 'utf8').trim()
57+
if (!head) return null
58+
if (!head.startsWith('ref:')) return shortCommit(head)
59+
const ref = head.replace(/^ref:\s*/, '').trim()
60+
if (!ref) return null
61+
const refPath = join(gitDir, ref)
62+
if (!existsSync(refPath)) return null
63+
const sha = readFileSync(refPath, 'utf8').trim()
64+
return shortCommit(sha)
65+
} catch {
66+
return null
67+
}
68+
}
69+
70+
function findGitDir(start: string) {
71+
let current = resolve(start)
72+
for (;;) {
73+
const dotGit = join(current, '.git')
74+
if (existsSync(dotGit)) {
75+
try {
76+
const stat = statSync(dotGit)
77+
if (stat.isDirectory()) return dotGit
78+
} catch {
79+
// ignore
80+
}
81+
try {
82+
const content = readFileSync(dotGit, 'utf8').trim()
83+
const match = content.match(/^gitdir:\s*(.+)$/)
84+
if (match?.[1]) return resolve(current, match[1])
85+
} catch {
86+
return dotGit
87+
}
88+
return dotGit
89+
}
90+
const parent = resolve(current, '..')
91+
if (parent === current) return null
92+
current = parent
93+
}
94+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
type Color = (value: string) => string
2+
3+
function wrap(start: string, end = '\x1b[0m'): Color {
4+
return (value) => `${start}${value}${end}`
5+
}
6+
7+
const ansi = {
8+
reset: '\x1b[0m',
9+
bold: wrap('\x1b[1m'),
10+
dim: wrap('\x1b[2m'),
11+
cyan: wrap('\x1b[36m'),
12+
green: wrap('\x1b[32m'),
13+
yellow: wrap('\x1b[33m'),
14+
}
15+
16+
function isColorEnabled() {
17+
if (!process.stdout.isTTY) return false
18+
if (process.env.NO_COLOR) return false
19+
return true
20+
}
21+
22+
export function styleTitle(value: string) {
23+
if (!isColorEnabled()) return value
24+
return `${ansi.bold(ansi.cyan(value))}${ansi.reset}`
25+
}
26+
27+
export function configureCommanderHelp(program: {
28+
configureHelp: (config: {
29+
sectionTitle?: (title: string) => string
30+
optionTerm?: (option: { flags: string }) => string
31+
commandTerm?: (cmd: { name: () => string }) => string
32+
}) => unknown
33+
}) {
34+
if (!isColorEnabled()) return
35+
program.configureHelp({
36+
sectionTitle: (title) => ansi.bold(ansi.cyan(title)),
37+
optionTerm: (option) => ansi.yellow(option.flags),
38+
commandTerm: (cmd) => ansi.green(cmd.name()),
39+
})
40+
}
41+
42+
export function styleEnvBlock(value: string) {
43+
if (!isColorEnabled()) return value
44+
return `${ansi.dim(value)}${ansi.reset}`
45+
}

0 commit comments

Comments
 (0)