@liebstoeckel/cli 0.3.7
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/LICENSE +373 -0
- package/README.md +74 -0
- package/package.json +60 -0
- package/skill/AGENTS.md +18 -0
- package/skill/SKILL.md +144 -0
- package/skill/references/authoring.md +81 -0
- package/skill/references/build-plugins.md +116 -0
- package/skill/references/components.md +107 -0
- package/skill/references/editing.md +78 -0
- package/skill/references/plugins.md +121 -0
- package/skill/references/troubleshooting.md +39 -0
- package/src/add.ts +369 -0
- package/src/build.ts +354 -0
- package/src/cli.ts +78 -0
- package/src/cloud.ts +570 -0
- package/src/creds.ts +31 -0
- package/src/new.ts +310 -0
- package/src/registry.ts +124 -0
- package/src/skill.ts +167 -0
- package/src/targeting.ts +8 -0
- package/src/trust.ts +73 -0
- package/src/update.ts +148 -0
package/src/update.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Update reminders (npm-notifier pattern): every human-facing run prints from a
|
|
2
|
+
// CACHED registry check and refreshes that cache in a detached background child,
|
|
3
|
+
// so no command ever waits on the network. The check shells out to
|
|
4
|
+
// `bun pm view`, so registry resolution (scoped .npmrc / bunfig) matches
|
|
5
|
+
// installs exactly, Verdaccio today, public npm later, with zero config here.
|
|
6
|
+
// The same module also compares a deck's installed agent skill (version-pinned
|
|
7
|
+
// by `skill install`) against the running CLI and points at `skill update`.
|
|
8
|
+
//
|
|
9
|
+
// Reminders are stderr-only and OFF for agents/CI/pipes (`remindersEnabled`),
|
|
10
|
+
// so the machine-readable contract (ADR 0045) stays clean.
|
|
11
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { cliVersion, SKILL_DIR } from "./skill";
|
|
16
|
+
|
|
17
|
+
const PKG = "@liebstoeckel/cli";
|
|
18
|
+
// Resolve the cache path lazily, honouring $HOME (homedir() is the Windows fallback);
|
|
19
|
+
// it lives next to the CLI's config (mirrors creds' CONFIG_DIR).
|
|
20
|
+
const stateFile = () => join(process.env.HOME || homedir(), ".config", "liebstoeckel", "update-check.json");
|
|
21
|
+
const CHECK_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
export interface CheckState {
|
|
24
|
+
checkedAt: number;
|
|
25
|
+
/** Latest published version, or null when the last check failed (offline, no registry). */
|
|
26
|
+
latest: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseVersion(v: string): { nums: [number, number, number]; pre: string | null } | null {
|
|
30
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?/.exec(v.trim());
|
|
31
|
+
if (!m) return null;
|
|
32
|
+
return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Semver-ish compare: numeric triple, then "a prerelease sorts before its release". */
|
|
36
|
+
export function compareVersions(a: string, b: string): number {
|
|
37
|
+
const pa = parseVersion(a);
|
|
38
|
+
const pb = parseVersion(b);
|
|
39
|
+
if (!pa || !pb) return 0; // unparseable: never claim an update
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
if (pa.nums[i]! !== pb.nums[i]!) return pa.nums[i]! - pb.nums[i]!;
|
|
42
|
+
}
|
|
43
|
+
if (pa.pre === pb.pre) return 0;
|
|
44
|
+
if (pa.pre === null) return 1;
|
|
45
|
+
if (pb.pre === null) return -1;
|
|
46
|
+
return pa.pre < pb.pre ? -1 : 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const isNewer = (candidate: string | null | undefined, current: string): boolean =>
|
|
50
|
+
!!candidate && compareVersions(candidate, current) > 0;
|
|
51
|
+
|
|
52
|
+
/** Refresh when there is no cache, it expired, or the clock went backwards. */
|
|
53
|
+
export function shouldRefresh(state: CheckState | null, now: number): boolean {
|
|
54
|
+
return !state || now - state.checkedAt > CHECK_EVERY_MS || now < state.checkedAt;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Reminders print only on an interactive terminal: never for `--json`, pipes
|
|
58
|
+
* (agents), CI, or when explicitly disabled. */
|
|
59
|
+
export function remindersEnabled(
|
|
60
|
+
argv: string[],
|
|
61
|
+
env: Record<string, string | undefined> = process.env,
|
|
62
|
+
stderrTty: boolean | undefined = process.stderr.isTTY,
|
|
63
|
+
): boolean {
|
|
64
|
+
if (env.LIEBSTOECKEL_NO_UPDATE_CHECK) return false;
|
|
65
|
+
if (env.CI) return false;
|
|
66
|
+
if (argv.includes("--json")) return false;
|
|
67
|
+
return !!stderrTty;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readState(): Promise<CheckState | null> {
|
|
71
|
+
try {
|
|
72
|
+
const s = (await Bun.file(stateFile()).json()) as CheckState;
|
|
73
|
+
return typeof s?.checkedAt === "number" ? s : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Print "update available" from the cache; kick a detached refresh when stale. */
|
|
80
|
+
export async function updateReminder(argv: string[]): Promise<void> {
|
|
81
|
+
if (!remindersEnabled(argv)) return;
|
|
82
|
+
const state = await readState();
|
|
83
|
+
const current = await cliVersion();
|
|
84
|
+
if (state && isNewer(state.latest, current)) {
|
|
85
|
+
console.error(`↑ ${PKG} ${state.latest} is available (you run ${current}), update: bun update --latest ${PKG}`);
|
|
86
|
+
}
|
|
87
|
+
if (shouldRefresh(state, Date.now())) {
|
|
88
|
+
// Detached child re-runs THIS file (import.meta.main → refresh()). It inherits
|
|
89
|
+
// the cwd, so `bun pm view` sees the same .npmrc/bunfig the user's installs use.
|
|
90
|
+
const child = Bun.spawn([process.execPath, fileURLToPath(import.meta.url)], {
|
|
91
|
+
stdin: "ignore",
|
|
92
|
+
stdout: "ignore",
|
|
93
|
+
stderr: "ignore",
|
|
94
|
+
});
|
|
95
|
+
child.unref();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The `version:` stamped into an installed SKILL.md by `skill install`. */
|
|
100
|
+
export function parseSkillVersion(skillMd: string): string | null {
|
|
101
|
+
return /\n\s*version:\s*(\S+)/.exec(skillMd)?.[1] ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function installedSkillVersion(deckDir: string): Promise<string | null> {
|
|
105
|
+
for (const rel of Object.values(SKILL_DIR)) {
|
|
106
|
+
const p = join(deckDir, rel, "SKILL.md");
|
|
107
|
+
if (!existsSync(p)) continue;
|
|
108
|
+
const v = parseSkillVersion(await Bun.file(p).text());
|
|
109
|
+
if (v) return v;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Warn when the deck's installed agent skill is older than the running CLI. */
|
|
115
|
+
export async function skillReminder(deckDir: string, argv: string[]): Promise<void> {
|
|
116
|
+
if (!remindersEnabled(argv)) return;
|
|
117
|
+
const installed = await installedSkillVersion(deckDir);
|
|
118
|
+
if (!installed) return;
|
|
119
|
+
const current = await cliVersion();
|
|
120
|
+
if (isNewer(current, installed)) {
|
|
121
|
+
console.error(`↑ this deck's agent skill is v${installed}, the CLI is v${current}, refresh: liebstoeckel skill update`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Background half: ask the registry for the latest version and cache the answer.
|
|
126
|
+
* A failed check caches `latest: null` so an offline machine retries at most
|
|
127
|
+
* once per interval instead of on every command. */
|
|
128
|
+
async function refresh(): Promise<void> {
|
|
129
|
+
// Survive the parent's terminal session ending mid-check (best-effort; a
|
|
130
|
+
// missed refresh self-heals: the cache stays stale, so the next run retries).
|
|
131
|
+
process.on("SIGHUP", () => {});
|
|
132
|
+
let latest: string | null = null;
|
|
133
|
+
try {
|
|
134
|
+
const proc = Bun.spawnSync([process.execPath, "pm", "view", PKG, "dist-tags.latest"], {
|
|
135
|
+
stdout: "pipe",
|
|
136
|
+
stderr: "ignore",
|
|
137
|
+
timeout: 15_000,
|
|
138
|
+
});
|
|
139
|
+
const out = proc.stdout.toString().trim();
|
|
140
|
+
if (proc.success && parseVersion(out)) latest = out;
|
|
141
|
+
} catch {
|
|
142
|
+
// offline / no project / no registry: cache the miss
|
|
143
|
+
}
|
|
144
|
+
mkdirSync(dirname(stateFile()), { recursive: true });
|
|
145
|
+
await Bun.write(stateFile(), JSON.stringify({ checkedAt: Date.now(), latest } satisfies CheckState));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (import.meta.main) void refresh();
|