@hmharness/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +20 -2
- package/dist/npm-stats.d.ts +8 -0
- package/dist/npm-stats.js +41 -0
- package/dist/tui.js +8 -0
- package/dist/update-check.d.ts +19 -0
- package/dist/update-check.js +67 -0
- package/package.json +7 -7
package/dist/main.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* hmh resume [id-prefix] continue a past session by id prefix (or latest)
|
|
9
9
|
* hmh web [--port=7788] local web frontend (SSE streaming + approvals)
|
|
10
10
|
* hmh tui lite terminal UI (status header + slash commands)
|
|
11
|
-
* hmh ops [scan|brief|status] ops keeper:
|
|
11
|
+
* hmh ops [scan|brief|stats|status] ops keeper: radar / npm download stats
|
|
12
12
|
* hmh devices|check direct tool run, no model
|
|
13
13
|
* hmh tools list all registered tools (native + MCP)
|
|
14
14
|
* hmh mcp show configured MCP servers and their tools
|
|
@@ -98,6 +98,14 @@ async function repl(yes, initialHistory) {
|
|
|
98
98
|
let t = strings((cfg.locale ?? 'zh'));
|
|
99
99
|
const header = () => stdout.write(CYAN('hmh') + DIM(` · ${cfg.provider.model} · ${home}\n`));
|
|
100
100
|
stdout.write(CYAN('hmh') + DIM(` · ${cfg.provider.model} · ${home}\n`) + DIM(`${t.replHint} · /help ${String(t.cmdHelp)}\n\n`));
|
|
101
|
+
// npm is pull-based; the update reminder is a cached (1/day) registry
|
|
102
|
+
// check printed when resolved - never blocks, never nags offline
|
|
103
|
+
const { notifyUpdate } = await import("./update-check.js");
|
|
104
|
+
const { createRequire } = await import('node:module');
|
|
105
|
+
const CURRENT_VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
106
|
+
void notifyUpdate(home, CURRENT_VERSION, (latest) => {
|
|
107
|
+
stdout.write(DIM(`↑ ${t.updateHint(latest)}\n\n`));
|
|
108
|
+
});
|
|
101
109
|
const { reg, clients } = await buildRegistry();
|
|
102
110
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
103
111
|
// stdin EOF (piped input, closed terminal) must exit the loop - a bare
|
|
@@ -325,7 +333,7 @@ usage:
|
|
|
325
333
|
hmh web [--port=7788] web UI in the foreground (debugging)
|
|
326
334
|
hmh tui [--no-web] fullscreen terminal UI (slash palette, mouse wheel);
|
|
327
335
|
also starts the web UI in the background (--no-web skips)
|
|
328
|
-
hmh ops [scan|brief|status] ops keeper:
|
|
336
|
+
hmh ops [scan|brief|stats|status] ops keeper: radar / npm download stats
|
|
329
337
|
hmh mcp-serve run as an MCP stdio SERVER: expose harmony_* tools to
|
|
330
338
|
Claude Code / Codex / any MCP host
|
|
331
339
|
(host config: npx -y @hmharness/cli mcp-serve)
|
|
@@ -577,6 +585,16 @@ flags:
|
|
|
577
585
|
const r = await harmonyOpsRadarBrief.execute({}, ctx);
|
|
578
586
|
stdout.write(r.output + '\n');
|
|
579
587
|
}
|
|
588
|
+
else if (sub === 'stats') {
|
|
589
|
+
// npm download counts for the seven packages (public API, no auth).
|
|
590
|
+
const { fetchNpmStats, renderStats } = await import("./npm-stats.js");
|
|
591
|
+
try {
|
|
592
|
+
stdout.write(renderStats(await fetchNpmStats()) + '\n');
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
stdout.write('npm downloads API unreachable right now - try again later\n');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
580
598
|
else {
|
|
581
599
|
const r = await harmonyOpsStatus.execute({}, ctx);
|
|
582
600
|
stdout.write(r.output + '\n');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface PkgStat {
|
|
2
|
+
name: string;
|
|
3
|
+
day: number | null;
|
|
4
|
+
week: number | null;
|
|
5
|
+
month: number | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function fetchNpmStats(fetchImpl?: typeof fetch): Promise<PkgStat[]>;
|
|
8
|
+
export declare function renderStats(rows: PkgStat[]): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/cli - npm-stats
|
|
3
|
+
* `hmh ops stats`: download counts for the seven @hmharness packages from
|
|
4
|
+
* npm's public downloads API. Counts are DOWNLOADS, not users - mirror sync
|
|
5
|
+
* and scanners are included; the line under the table says so (honesty over
|
|
6
|
+
* vanity metrics).
|
|
7
|
+
*/
|
|
8
|
+
const PKGS = ['kernel', 'evolution', 'domain-harmony', 'domain-ops', 'agent', 'web', 'cli'];
|
|
9
|
+
async function one(fetchImpl, period, name) {
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetchImpl(`https://api.npmjs.org/downloads/point/${period}/${encodeURIComponent(name)}`, {
|
|
12
|
+
signal: AbortSignal.timeout(8000),
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok)
|
|
15
|
+
return null;
|
|
16
|
+
const j = await res.json();
|
|
17
|
+
return typeof j.downloads === 'number' ? j.downloads : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function fetchNpmStats(fetchImpl = fetch) {
|
|
24
|
+
const rows = await Promise.all(PKGS.map(async (p) => {
|
|
25
|
+
const name = '@hmharness/' + p;
|
|
26
|
+
const [day, week, month] = await Promise.all([
|
|
27
|
+
one(fetchImpl, 'last-day', name),
|
|
28
|
+
one(fetchImpl, 'last-week', name),
|
|
29
|
+
one(fetchImpl, 'last-month', name),
|
|
30
|
+
]);
|
|
31
|
+
return { name, day, week, month };
|
|
32
|
+
}));
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
export function renderStats(rows) {
|
|
36
|
+
const n = (v) => (v === null ? '-' : String(v));
|
|
37
|
+
const w = (s, len) => s.padEnd(len);
|
|
38
|
+
const head = w('package', 28) + w('day', 7) + w('week', 8) + 'month';
|
|
39
|
+
const body = rows.map((r) => w(r.name, 28) + w(n(r.day), 7) + w(n(r.week), 8) + n(r.month)).join('\n');
|
|
40
|
+
return head + '\n' + body + '\n(downloads, not users: mirror sync + scanners included; CN installs via npmmirror are NOT counted)';
|
|
41
|
+
}
|
package/dist/tui.js
CHANGED
|
@@ -831,6 +831,14 @@ export async function tui(yes, noWeb = false) {
|
|
|
831
831
|
rt.addText(t.tuiWelcome(chatModel), 'dim');
|
|
832
832
|
if (webUp)
|
|
833
833
|
rt.addText(t.tuiWebLinked(DEFAULT_WEB_PORT), 'dim');
|
|
834
|
+
// update reminder: cached (1/day) registry check, resolved async into the
|
|
835
|
+
// transcript via addText (frame-safe); offline stays silent
|
|
836
|
+
{
|
|
837
|
+
const { notifyUpdate } = await import("./update-check.js");
|
|
838
|
+
const { createRequire } = await import('node:module');
|
|
839
|
+
const current = createRequire(import.meta.url)('../package.json').version;
|
|
840
|
+
void notifyUpdate(home, current, (latest) => rt.addText(`↑ ${t.updateHint(latest)}`, 'dim'));
|
|
841
|
+
}
|
|
834
842
|
let history = [];
|
|
835
843
|
rt.onSubmit(() => {
|
|
836
844
|
const line = rt.consumeInput().trim();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Numeric per-component semver compare (no dependency, dot-split).
|
|
2
|
+
* Prerelease suffixes degrade to their leading number ('1-beta' -> 1) -
|
|
3
|
+
* good enough for an update hint, never claims to be full semver. */
|
|
4
|
+
export declare function cmpSemver(a: string, b: string): number;
|
|
5
|
+
export interface UpdateInfo {
|
|
6
|
+
current: string;
|
|
7
|
+
latest: string;
|
|
8
|
+
}
|
|
9
|
+
/** Returns info when a NEWER version exists on the registry, else null.
|
|
10
|
+
* Cache-first: a fresh (<24h) cached latest answer means zero network. */
|
|
11
|
+
export declare function checkForUpdate(opts: {
|
|
12
|
+
home: string;
|
|
13
|
+
current: string;
|
|
14
|
+
now?: number;
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
}): Promise<UpdateInfo | null>;
|
|
17
|
+
/** Fire-and-forget wrapper for interactive frontends: resolve-and-say, or
|
|
18
|
+
* say nothing at all. Never rejects. */
|
|
19
|
+
export declare function notifyUpdate(home: string, current: string, say: (line: string) => void): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/cli - update-check
|
|
3
|
+
* npm is pull-based: there is no server-side push. The honest "update
|
|
4
|
+
* reminder" is a client-side version check against the registry's latest
|
|
5
|
+
* dist-tag, printed once per interactive session - never blocking startup,
|
|
6
|
+
* never nagging offline, results cached for a day.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
const REGISTRY = 'https://registry.npmjs.org/-/package/@hmharness/cli/dist-tags';
|
|
11
|
+
const CACHE_TTL_MS = 24 * 3600_000;
|
|
12
|
+
/** Numeric per-component semver compare (no dependency, dot-split).
|
|
13
|
+
* Prerelease suffixes degrade to their leading number ('1-beta' -> 1) -
|
|
14
|
+
* good enough for an update hint, never claims to be full semver. */
|
|
15
|
+
export function cmpSemver(a, b) {
|
|
16
|
+
const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
17
|
+
const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
|
|
18
|
+
for (let i = 0; i < 3; i++) {
|
|
19
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
20
|
+
return (pa[i] ?? 0) < (pb[i] ?? 0) ? -1 : 1;
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
/** Returns info when a NEWER version exists on the registry, else null.
|
|
25
|
+
* Cache-first: a fresh (<24h) cached latest answer means zero network. */
|
|
26
|
+
export async function checkForUpdate(opts) {
|
|
27
|
+
const now = opts.now ?? Date.now();
|
|
28
|
+
const cacheFile = join(opts.home, 'update-check.json');
|
|
29
|
+
let latest = null;
|
|
30
|
+
try {
|
|
31
|
+
const c = JSON.parse(await readFile(cacheFile, 'utf8'));
|
|
32
|
+
if (typeof c.latest === 'string' && now - c.time < CACHE_TTL_MS)
|
|
33
|
+
latest = c.latest;
|
|
34
|
+
}
|
|
35
|
+
catch { /* no cache yet */ }
|
|
36
|
+
if (latest === null) {
|
|
37
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
38
|
+
try {
|
|
39
|
+
const res = await doFetch(REGISTRY, { signal: AbortSignal.timeout(3000) });
|
|
40
|
+
if (res.ok) {
|
|
41
|
+
const tags = await res.json();
|
|
42
|
+
if (typeof tags.latest === 'string') {
|
|
43
|
+
latest = tags.latest;
|
|
44
|
+
try {
|
|
45
|
+
await mkdir(opts.home, { recursive: true });
|
|
46
|
+
await writeFile(cacheFile, JSON.stringify({ time: now, latest }), 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch { /* cache write is best-effort */ }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch { /* offline / slow registry: silent, never nag */ }
|
|
53
|
+
}
|
|
54
|
+
if (latest === null || cmpSemver(opts.current, latest) >= 0)
|
|
55
|
+
return null;
|
|
56
|
+
return { current: opts.current, latest };
|
|
57
|
+
}
|
|
58
|
+
/** Fire-and-forget wrapper for interactive frontends: resolve-and-say, or
|
|
59
|
+
* say nothing at all. Never rejects. */
|
|
60
|
+
export async function notifyUpdate(home, current, say) {
|
|
61
|
+
try {
|
|
62
|
+
const info = await checkForUpdate({ home, current });
|
|
63
|
+
if (info)
|
|
64
|
+
say(info.latest);
|
|
65
|
+
}
|
|
66
|
+
catch { /* never surface update-check failures */ }
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "hmharness command line: one-shot tasks, an interactive REPL, a fullscreen TUI, the web frontend, and direct tool invocation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"build": "tsc -p tsconfig.build.json"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@hmharness/kernel": "0.
|
|
47
|
-
"@hmharness/evolution": "0.
|
|
48
|
-
"@hmharness/domain-harmony": "0.
|
|
49
|
-
"@hmharness/domain-ops": "0.
|
|
50
|
-
"@hmharness/agent": "0.
|
|
51
|
-
"@hmharness/web": "0.
|
|
46
|
+
"@hmharness/kernel": "0.3.0",
|
|
47
|
+
"@hmharness/evolution": "0.3.0",
|
|
48
|
+
"@hmharness/domain-harmony": "0.3.0",
|
|
49
|
+
"@hmharness/domain-ops": "0.3.0",
|
|
50
|
+
"@hmharness/agent": "0.3.0",
|
|
51
|
+
"@hmharness/web": "0.3.0"
|
|
52
52
|
}
|
|
53
53
|
}
|