@officexapp/vidfarm-devcli 0.21.54 → 0.21.56

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.
@@ -117,7 +117,8 @@ export const PACK_TOPICS = [
117
117
  blurb: "The 5-stage ladder — what the viewer knows, what the video must do, and what it may ask for" },
118
118
  { topic: "problem-angles", aliases: ["angle", "lenses", "problem-angle"], doc: "references/content-ideas.md", heading: "The problem angles",
119
119
  blurb: "44 angles on the problem — hold the frame, change the angle when a topic is \"already covered\"" },
120
- { topic: "meme-recaption", aliases: ["meme", "recaption", "meme-caption", "meme_recaption"], doc: "references/editor-workflows.md", heading: "Writing a meme recaption",
120
+ { topic: "meme-recaption", // `meme_recaption` needs no alias: resolvePackTopic folds `_` to `-` first.
121
+ aliases: ["meme", "recaption", "meme-caption"], doc: "references/editor-workflows.md", heading: "Writing a meme recaption",
121
122
  blurb: "Recaption a meme at a pain or a win the niche knows — the cold-viewer test. Building one from scratch? the full format is vidfarm.cc/experimental/meme-recaption.md" },
122
123
  { topic: "product-explainer", aliases: ["product-explainers"], doc: "harnesses/product-explainer.HARNESS.md",
123
124
  blurb: "The product-explainer harness — the bundled base for explaining what a product does" },
@@ -0,0 +1,340 @@
1
+ // `vidfarm update-check` — is this install current?
2
+ //
3
+ // Vidfarm ships as TWO moving parts that are versioned TOGETHER: the `vidfarm`
4
+ // agent skill (the workflows on disk) and the `vidfarm-devcli` (the command).
5
+ // Updating one but not the other is the most common cause of "this command
6
+ // doesn't exist" and "the skill says to do X but it fails" — the skill documents
7
+ // routes and flags the installed CLI may not have yet. So this checks both and
8
+ // reports them as one answer.
9
+ //
10
+ // It is built to be run by an AI AGENT on a cadence, not by a human on a whim:
11
+ // - `--quiet` prints ONE line (or nothing at all when current), so it can sit
12
+ // at the top of a session without burning context.
13
+ // - `--if-stale` is a no-op unless the last check is older than the interval,
14
+ // so "check at least once every 24 hours" costs one cheap call a day rather
15
+ // than one per command.
16
+ // - `--skip` records the user's "not now" so the agent stops asking. A prompt
17
+ // that cannot be dismissed gets ignored, and then the real one is ignored too.
18
+ //
19
+ // BACKEND-FREE: Node built-ins plus one unauthenticated fetch of the public npm
20
+ // registry and one of vidfarm.cc/skill.md. No key, no account, no wallet.
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
+ import path from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { resolveDevcliHome } from "./auth-store.js";
25
+ const BOLD = "\x1b[1m";
26
+ const DIM = "\x1b[2m";
27
+ const GREEN = "\x1b[32m";
28
+ const YELLOW = "\x1b[33m";
29
+ const RESET = "\x1b[0m";
30
+ const PACKAGE_NAME = "@officexapp/vidfarm-devcli";
31
+ const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
32
+ const DEFAULT_HOST = "https://vidfarm.cc";
33
+ /** "at least once every 24 hours" — the cadence SKILL.md asks agents to keep. */
34
+ export const DEFAULT_STALE_HOURS = 24;
35
+ /** Never let a version probe hold up the real work. */
36
+ const FETCH_TIMEOUT_MS = 6000;
37
+ // ── semver ───────────────────────────────────────────────────────────────────
38
+ /**
39
+ * Compare two semvers numerically. A STRING compare is the trap here: "0.9.0"
40
+ * sorts above "0.21.55", which would report a current install as ahead of the
41
+ * registry and silently suppress every update prompt.
42
+ */
43
+ export function compareSemver(a, b) {
44
+ const parse = (v) => v.trim().replace(/^v/, "").split("-")[0].split(".").map((part) => {
45
+ const n = Number(part);
46
+ return Number.isFinite(n) ? n : 0;
47
+ });
48
+ const left = parse(a);
49
+ const right = parse(b);
50
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
51
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
52
+ if (diff !== 0)
53
+ return diff < 0 ? -1 : 1;
54
+ }
55
+ return 0;
56
+ }
57
+ /** Which part moved: a major bump is the one that can break a documented flow. */
58
+ export function bumpKind(from, to) {
59
+ const parse = (v) => v.trim().replace(/^v/, "").split("-")[0].split(".").map((p) => Number(p) || 0);
60
+ const [aMajor = 0, aMinor = 0, aPatch = 0] = parse(from);
61
+ const [bMajor = 0, bMinor = 0, bPatch = 0] = parse(to);
62
+ if (bMajor !== aMajor)
63
+ return "major";
64
+ if (bMinor !== aMinor)
65
+ return "minor";
66
+ if (bPatch !== aPatch)
67
+ return "patch";
68
+ return "none";
69
+ }
70
+ // ── what is installed ────────────────────────────────────────────────────────
71
+ /** Walk up from this module to the package root and read its version. */
72
+ export function installedDevcliVersion() {
73
+ let dir = path.dirname(fileURLToPath(import.meta.url));
74
+ for (let i = 0; i < 6; i += 1) {
75
+ const candidate = path.join(dir, "package.json");
76
+ if (existsSync(candidate)) {
77
+ try {
78
+ const pkg = JSON.parse(readFileSync(candidate, "utf8"));
79
+ if (pkg.version && (pkg.name === PACKAGE_NAME || pkg.name === "vidfarm-devcli" || i > 0)) {
80
+ return String(pkg.version);
81
+ }
82
+ }
83
+ catch {
84
+ // keep walking — a malformed package.json higher up is not fatal
85
+ }
86
+ }
87
+ const parent = path.dirname(dir);
88
+ if (parent === dir)
89
+ break;
90
+ dir = parent;
91
+ }
92
+ return null;
93
+ }
94
+ /**
95
+ * `version:` out of a SKILL.md front-matter block. Deliberately tiny — the same
96
+ * stance as the harness front-matter reader: a pack that needs a YAML parser to
97
+ * state its own version has stopped being legible to the human maintaining it.
98
+ */
99
+ export function readSkillVersion(markdown) {
100
+ const front = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/);
101
+ const body = front ? front[1] : markdown.slice(0, 2000);
102
+ const found = body.match(/^\s*(?:skill_version|version)\s*:\s*["']?v?([0-9]+(?:\.[0-9]+)*)["']?\s*$/m);
103
+ return found ? found[1] : null;
104
+ }
105
+ /** The vidfarm skill pack installed on this machine, if there is one. */
106
+ export function installedSkillPath() {
107
+ const roots = [process.cwd(), path.dirname(fileURLToPath(import.meta.url))];
108
+ for (const root of roots) {
109
+ let dir = root;
110
+ for (let i = 0; i < 6; i += 1) {
111
+ for (const rel of [
112
+ path.join(".agents", "skills", "vidfarm", "SKILL.md"),
113
+ path.join(".claude", "skills", "vidfarm", "SKILL.md")
114
+ ]) {
115
+ const candidate = path.join(dir, rel);
116
+ if (existsSync(candidate))
117
+ return candidate;
118
+ }
119
+ const parent = path.dirname(dir);
120
+ if (parent === dir)
121
+ break;
122
+ dir = parent;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ // ── what is published ────────────────────────────────────────────────────────
128
+ async function fetchText(url, headers = {}) {
129
+ try {
130
+ const response = await fetch(url, {
131
+ headers: { accept: "*/*", ...headers },
132
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
133
+ });
134
+ if (!response.ok)
135
+ return null;
136
+ return await response.text();
137
+ }
138
+ catch {
139
+ // Offline is a normal state for this CLI — the whole local half works with
140
+ // no network. A failed probe must never look like "you are out of date".
141
+ return null;
142
+ }
143
+ }
144
+ async function latestDevcliVersion() {
145
+ const text = await fetchText(REGISTRY_URL, { accept: "application/json" });
146
+ if (!text)
147
+ return null;
148
+ try {
149
+ const body = JSON.parse(text);
150
+ return body.version ? String(body.version) : null;
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }
156
+ async function latestSkillVersion(host) {
157
+ const text = await fetchText(`${host.replace(/\/+$/, "")}/SKILL.md`);
158
+ return text ? readSkillVersion(text) : null;
159
+ }
160
+ function statePath(home) {
161
+ return path.join(resolveDevcliHome(home), "update-check.json");
162
+ }
163
+ export function readUpdateState(home) {
164
+ try {
165
+ return JSON.parse(readFileSync(statePath(home), "utf8"));
166
+ }
167
+ catch {
168
+ return {};
169
+ }
170
+ }
171
+ export function writeUpdateState(next, home) {
172
+ const file = statePath(home);
173
+ mkdirSync(path.dirname(file), { recursive: true });
174
+ writeFileSync(file, JSON.stringify(next, null, 2), "utf8");
175
+ }
176
+ /** Hours since the last check, or null if it has never run. */
177
+ export function hoursSinceLastCheck(state, now = Date.now()) {
178
+ if (!state.lastCheckedAt)
179
+ return null;
180
+ const then = Date.parse(state.lastCheckedAt);
181
+ if (!Number.isFinite(then))
182
+ return null;
183
+ return (now - then) / 3_600_000;
184
+ }
185
+ export async function runUpdateCheck(options = {}) {
186
+ const host = options.host || process.env.VIDFARM_HOST || DEFAULT_HOST;
187
+ const now = options.now ?? Date.now();
188
+ const state = readUpdateState(options.home);
189
+ const devcliInstalled = installedDevcliVersion();
190
+ const skillPath = installedSkillPath();
191
+ const skillInstalled = skillPath ? readSkillVersion(readFileSync(skillPath, "utf8")) : null;
192
+ const [devcliLatest, skillLatest] = await Promise.all([latestDevcliVersion(), latestSkillVersion(host)]);
193
+ const offline = devcliLatest === null && skillLatest === null;
194
+ const devcliBehind = Boolean(devcliInstalled && devcliLatest && compareSemver(devcliInstalled, devcliLatest) < 0);
195
+ const skillBehind = Boolean(skillInstalled && skillLatest && compareSemver(skillInstalled, skillLatest) < 0);
196
+ // The version the user would be moving TO — a skip is recorded against it, so
197
+ // a NEWER release always asks again rather than staying silent forever.
198
+ const target = devcliLatest && skillLatest
199
+ ? (compareSemver(devcliLatest, skillLatest) >= 0 ? devcliLatest : skillLatest)
200
+ : (devcliLatest ?? skillLatest ?? "");
201
+ const skipped = Boolean(state.skippedVersion && target && compareSemver(target, state.skippedVersion) <= 0);
202
+ const report = {
203
+ devcli: {
204
+ installed: devcliInstalled,
205
+ latest: devcliLatest,
206
+ behind: devcliBehind,
207
+ bump: devcliInstalled && devcliLatest ? bumpKind(devcliInstalled, devcliLatest) : "none"
208
+ },
209
+ skill: { installed: skillInstalled, latest: skillLatest, behind: skillBehind, path: skillPath },
210
+ update_available: (devcliBehind || skillBehind) && !skipped,
211
+ suppressed: (devcliBehind || skillBehind) && skipped,
212
+ skipped,
213
+ checked_at: new Date(now).toISOString(),
214
+ hours_since_last_check: hoursSinceLastCheck(state, now),
215
+ offline
216
+ };
217
+ // Only a check that actually reached the network resets the clock. Stamping it
218
+ // while offline would suppress the next 24 hours of checks on no information.
219
+ if (!offline)
220
+ writeUpdateState({ ...state, lastCheckedAt: report.checked_at }, options.home);
221
+ return report;
222
+ }
223
+ // ── the command ──────────────────────────────────────────────────────────────
224
+ export const UPDATE_CHECK_HELP = `vidfarm update-check — is this install current?
225
+
226
+ update-check Check both halves (skill + devcli) and print the verdict
227
+ --if-stale [--hours 24] No-op unless the last check is older than N hours.
228
+ THIS is the one to put in an agent's session opener.
229
+ --quiet One line, and NOTHING at all when already current
230
+ --json Structured: {devcli, skill, update_available, ...}
231
+ --skip Record "not now" for the current latest version.
232
+ A NEWER release asks again; this one stays quiet.
233
+ --unskip Clear that, and ask again from now on
234
+ --host <url> Where to read the published SKILL.md (default vidfarm.cc)
235
+
236
+ The skill and the devcli are versioned TOGETHER. Updating one without the other
237
+ is the most common cause of "that command doesn't exist" and "the skill says to
238
+ do X but it fails". Update both: https://vidfarm.cc/update.md`;
239
+ export async function runUpdateCheckCommand(argv) {
240
+ const { parseArgs } = await import("node:util");
241
+ const parsed = parseArgs({
242
+ args: argv,
243
+ allowPositionals: true,
244
+ options: {
245
+ json: { type: "boolean" },
246
+ quiet: { type: "boolean" },
247
+ "if-stale": { type: "boolean" },
248
+ hours: { type: "string" },
249
+ skip: { type: "boolean" },
250
+ unskip: { type: "boolean" },
251
+ host: { type: "string" },
252
+ help: { type: "boolean" }
253
+ }
254
+ });
255
+ const values = parsed.values;
256
+ if (values.help) {
257
+ console.log(UPDATE_CHECK_HELP);
258
+ return;
259
+ }
260
+ if (values.unskip) {
261
+ const state = readUpdateState();
262
+ delete state.skippedVersion;
263
+ delete state.skippedAt;
264
+ writeUpdateState(state);
265
+ console.log(`${GREEN}✓${RESET} Update prompts re-enabled.`);
266
+ return;
267
+ }
268
+ // `--if-stale` is what makes a 24-hour cadence cheap: the agent can call this
269
+ // at the top of every session and it costs one network round-trip a day.
270
+ if (values["if-stale"]) {
271
+ const hours = Number(values.hours ?? DEFAULT_STALE_HOURS);
272
+ const since = hoursSinceLastCheck(readUpdateState());
273
+ if (since !== null && since < (Number.isFinite(hours) ? hours : DEFAULT_STALE_HOURS)) {
274
+ if (values.json)
275
+ console.log(JSON.stringify({ checked: false, hours_since_last_check: since }, null, 2));
276
+ else if (!values.quiet)
277
+ console.log(`${DIM}Checked ${since.toFixed(1)}h ago — skipping (next check after ${hours}h).${RESET}`);
278
+ return;
279
+ }
280
+ }
281
+ const report = await runUpdateCheck({ host: values.host });
282
+ if (values.skip) {
283
+ const target = report.devcli.latest ?? report.skill.latest;
284
+ if (target) {
285
+ writeUpdateState({ ...readUpdateState(), skippedVersion: target, skippedAt: new Date().toISOString() });
286
+ console.log(`${GREEN}✓${RESET} Skipped ${BOLD}${target}${RESET}. A newer release will ask again — ${DIM}vidfarm update-check --unskip${RESET} to undo.`);
287
+ }
288
+ else {
289
+ console.log(`${DIM}Nothing to skip — could not read the published versions.${RESET}`);
290
+ }
291
+ return;
292
+ }
293
+ if (values.json) {
294
+ console.log(JSON.stringify(report, null, 2));
295
+ return;
296
+ }
297
+ if (report.offline) {
298
+ if (!values.quiet)
299
+ console.log(`${DIM}Could not reach the registry or ${values.host ?? DEFAULT_HOST} — skipping the version check. Everything local still works.${RESET}`);
300
+ return;
301
+ }
302
+ if (!report.update_available) {
303
+ if (values.quiet)
304
+ return; // silence is the point of --quiet when current
305
+ const parts = [
306
+ `devcli ${report.devcli.installed ?? "?"}`,
307
+ `skill ${report.skill.installed ?? "not installed"}`
308
+ ];
309
+ console.log(`${GREEN}✓${RESET} Vidfarm is current ${DIM}(${parts.join(" · ")})${RESET}`);
310
+ if (report.suppressed)
311
+ console.log(` ${DIM}An update is available but you skipped it: vidfarm update-check --unskip${RESET}`);
312
+ return;
313
+ }
314
+ const lines = [];
315
+ if (report.devcli.behind)
316
+ lines.push(` devcli ${BOLD}${report.devcli.installed}${RESET} → ${BOLD}${GREEN}${report.devcli.latest}${RESET} ${DIM}(${report.devcli.bump})${RESET}`);
317
+ if (report.skill.behind)
318
+ lines.push(` skill ${BOLD}${report.skill.installed}${RESET} → ${BOLD}${GREEN}${report.skill.latest}${RESET}`);
319
+ if (report.skill.installed === null && report.skill.latest) {
320
+ lines.push(` skill ${DIM}not installed${RESET} → ${BOLD}${GREEN}${report.skill.latest}${RESET} ${DIM}(vidfarm skills add vidfarm)${RESET}`);
321
+ }
322
+ if (values.quiet) {
323
+ console.log(`${YELLOW}Vidfarm update available${RESET}${DIM} — ${report.devcli.behind ? `devcli ${report.devcli.installed}→${report.devcli.latest}` : ""}${report.devcli.behind && report.skill.behind ? ", " : ""}${report.skill.behind ? `skill ${report.skill.installed}→${report.skill.latest}` : ""} · vidfarm update-check for detail, --skip to dismiss${RESET}`);
324
+ return;
325
+ }
326
+ console.log(`${YELLOW}⟳ A Vidfarm update is available.${RESET}`);
327
+ for (const line of lines)
328
+ console.log(line);
329
+ console.log("");
330
+ if (report.devcli.bump === "major") {
331
+ console.log(`${YELLOW}This is a MAJOR bump${RESET}${DIM} — read the breaking-change notes before you assume an old workflow still applies.${RESET}`);
332
+ }
333
+ console.log(`${DIM}Update BOTH halves together — they are versioned together, and updating one alone is the${RESET}`);
334
+ console.log(`${DIM}usual cause of "that command doesn't exist". The runbook: ${RESET}https://vidfarm.cc/update.md`);
335
+ console.log("");
336
+ console.log(` ${BOLD}npm i -g ${PACKAGE_NAME}@latest${RESET} ${DIM}&&${RESET} ${BOLD}vidfarm skills add vidfarm${RESET}`);
337
+ console.log("");
338
+ console.log(`${DIM}Not now? ${RESET}vidfarm update-check --skip${DIM} — this version stays quiet, a newer one asks again.${RESET}`);
339
+ }
340
+ //# sourceMappingURL=update-check.js.map