takibibase 1.0.2 → 1.0.3

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.
Files changed (3) hide show
  1. package/SKILL.md +4 -0
  2. package/package.json +2 -2
  3. package/takibi.mjs +82 -5
package/SKILL.md CHANGED
@@ -23,6 +23,10 @@ error hints.
23
23
  takes a name or a UUID; single-grant keys may omit it.
24
24
  `takibi projects --add <name> <uuid>` keeps local aliases for offline use.
25
25
  - First probe: `takibi version` (needs no key; shows build + `jev` status).
26
+ - Update notice: the CLI polls the npm registry once a day and nudges on
27
+ stderr when behind (never on `--json`). Silence it with
28
+ `TAKIBI_NO_UPDATE_CHECK=1`; point it at a mirror with
29
+ `TAKIBI_REGISTRY_URL`.
26
30
 
27
31
  ## Commands
28
32
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "takibibase",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Thin CLI for the Takibi API: ask, search, tasks, docs. Single file, zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "takibi": "./takibi.mjs"
7
+ "takibi": "takibi.mjs"
8
8
  },
9
9
  "files": [
10
10
  "takibi.mjs",
package/takibi.mjs CHANGED
@@ -22,21 +22,24 @@
22
22
  * Exit codes: 0 ok (honest abstains included), 1 transport/API error,
23
23
  * 2 usage error.
24
24
  */
25
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeSync } from 'node:fs';
25
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, writeSync } from 'node:fs';
26
26
  import { homedir } from 'node:os';
27
27
  import { join } from 'node:path';
28
28
 
29
29
  /** Baked fallback; the published package re-reads package.json next door. */
30
- const BAKED_VERSION = '1.0.2';
31
- const CLI_VERSION = (() => {
30
+ const BAKED_VERSION = '1.0.3';
31
+ const CLI_INFO = (() => {
32
32
  try {
33
33
  const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
34
- if (pkg.name === 'takibibase' && typeof pkg.version === 'string' && pkg.version.length > 0) return pkg.version;
34
+ if (pkg.name === 'takibibase' && typeof pkg.version === 'string' && pkg.version.length > 0) {
35
+ return { version: pkg.version, singleFile: false };
36
+ }
35
37
  } catch {
36
38
  // Single-file download: no package.json travels with takibi.mjs.
37
39
  }
38
- return BAKED_VERSION;
40
+ return { version: BAKED_VERSION, singleFile: true };
39
41
  })();
42
+ const CLI_VERSION = CLI_INFO.version;
40
43
  const DEFAULT_BASE_URL = 'http://localhost:3849';
41
44
  const REQUEST_TIMEOUT_MS = 90_000;
42
45
  const USER_AGENT = `takibibase/${CLI_VERSION}`;
@@ -66,6 +69,78 @@ const usageError = (message) => new CliError(message, { exitCode: 2 });
66
69
 
67
70
  const takibiDir = () => join(homedir(), '.takibi');
68
71
 
72
+ const UPDATE_CHECK_TTL_MS = 86_400_000;
73
+ const REGISTRY_URL = (process.env.TAKIBI_REGISTRY_URL ?? '').trim().replace(/\/+$/, '') || 'https://registry.npmjs.org';
74
+
75
+ /** Numeric dotted-version compare; false on anything unparseable. */
76
+ function versionBehind(current, latest) {
77
+ const parts = (v) => String(v ?? '').trim().split('.');
78
+ const c = parts(current);
79
+ const l = parts(latest);
80
+ for (let i = 0; i < Math.max(c.length, l.length); i++) {
81
+ const a = i < c.length ? Number(c[i]) : 0;
82
+ const b = i < l.length ? Number(l[i]) : 0;
83
+ if (!Number.isInteger(a) || !Number.isInteger(b)) return false;
84
+ if (b > a) return true;
85
+ if (a > b) return false;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ /** Once-a-day latest-version poll. Stderr only (stdout stays clean),
91
+ * best-effort, never fails the command. Failures are cached too, so an
92
+ * unreachable registry costs one slow run a day, not every invocation.
93
+ * Skipped for --json (errors there are a single JSON object), in CI, and
94
+ * with TAKIBI_NO_UPDATE_CHECK=1. */
95
+ async function maybeNotifyUpdate({ json = false } = {}) {
96
+ try {
97
+ if (json || process.env.CI || process.env.TAKIBI_NO_UPDATE_CHECK === '1') return;
98
+ const path = join(takibiDir(), 'update-check');
99
+ let cached = null;
100
+ try {
101
+ cached = JSON.parse(readFileSync(path, 'utf8'));
102
+ } catch {
103
+ // First run: no cache yet.
104
+ }
105
+ const fresh = cached && typeof cached.at === 'number' && Date.now() - cached.at < UPDATE_CHECK_TTL_MS;
106
+ let latest = null;
107
+ if (fresh) {
108
+ if (typeof cached.latest !== 'string' || !cached.latest) return;
109
+ latest = cached.latest;
110
+ } else {
111
+ try {
112
+ const res = await fetch(`${REGISTRY_URL}/takibibase/latest`, { signal: AbortSignal.timeout(1200) });
113
+ const v = res.ok ? (await res.json())?.version : null;
114
+ if (typeof v === 'string' && v) latest = v;
115
+ } catch {
116
+ // Offline: fall through below.
117
+ }
118
+ if (!latest && cached && typeof cached.latest === 'string' && cached.latest) {
119
+ // Failed refresh keeps the last known version: a timeout or 5xx must
120
+ // not delete a nudge we already knew about. The fresh timestamp still
121
+ // stops us hammering the registry.
122
+ latest = cached.latest;
123
+ }
124
+ try {
125
+ mkdirSync(takibiDir(), { recursive: true });
126
+ writeFileSync(path, JSON.stringify({ at: Date.now(), latest }));
127
+ } catch {
128
+ // Read-only home: notifying still works, caching just retries daily.
129
+ }
130
+ if (!latest) return;
131
+ }
132
+ if (!versionBehind(CLI_VERSION, latest)) return;
133
+ err(`takibi: ${latest} is available (you have ${CLI_VERSION}).`);
134
+ err(
135
+ CLI_INFO.singleFile
136
+ ? 'hint: download the latest takibi.mjs from https://takibibase.com/downloads/takibi.mjs'
137
+ : 'hint: `npm i -g takibibase@latest`, or `npx takibibase@latest` for one-shot runs.',
138
+ );
139
+ } catch {
140
+ // Offline, broken cache, slow registry: stay silent, run the command.
141
+ }
142
+ }
143
+
69
144
  /** First non-blank, non-comment line of a file, or null when missing. */
70
145
  function firstLine(path) {
71
146
  if (!existsSync(path)) return null;
@@ -753,8 +828,10 @@ async function main(argv) {
753
828
  const { globals, rest } = parseArgv(argv);
754
829
  if (globals.version) {
755
830
  out(`takibi ${CLI_VERSION}`);
831
+ await maybeNotifyUpdate({ json: globals.json });
756
832
  return;
757
833
  }
834
+ await maybeNotifyUpdate({ json: globals.json });
758
835
  const [cmd, ...tokens] = rest;
759
836
  if (globals.help || !cmd) {
760
837
  out(HELP);