gipity 1.0.422 → 1.0.425

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/knowledge.js CHANGED
@@ -37,9 +37,9 @@ Kits are reusable building blocks added to an existing app, not whole templates
37
37
  - \`gipity add chatbot\` - Drop-in chatbot - configurable persona, scope guardrails, static knowledge (20k budget), streaming responses. Headless engine + bubble widget; bring your own UI if you want. Works in any app.
38
38
  - \`gipity add audio-align\` - Audio + lyrics -> word-level timing JSON. Demucs vocal isolation + MMS_FA forced alignment, runs as a Modal L4 GPU job (~$0.01 per 3-min song). For karaoke captions, subtitling, language learning, dubbing alignment.
39
39
  - \`gipity add i18n\` - Multi-language for web apps - language picker, locale persistence, RTL, plural/translation lookup. Scaffolds src/js/strings.js and wires it up; move your copy there and read it with t('key'). Web only.
40
- - \`gipity add records\` - Registry-driven records: declare objects/fields as data, get generic CRUD functions with validation, full-text search, soft delete, ACTOR provenance, and an audit event spine - every write is transactional (row + event). Field types include relations ({id,label}), currency, emails/phones/links composites. Ships backend functions + migrations. Needs a database (web-fullstack/api template).
41
- - \`gipity add views\` - Generic UI over records-kit objects: sortable/filterable table with full-text search, create/edit/delete forms with type-appropriate widgets, kanban board with drag-to-update. Renders entirely from the field registry - zero per-object UI code. Requires the records kit.
42
- - \`gipity add agent-api\` - Make your app agent-operable: named API keys (kit_api_keys) let agents and scripts write through the records kit's single write path with AGENT/API actor attribution - machine writes land on the same audit spine as human edits. Requires the records kit.
40
+ - \`gipity add records\` - Registry-driven records: declare objects/fields as data, get generic CRUD functions with validation, full-text search, soft delete, ACTOR provenance, and an audit event spine - every write is transactional (row + event). Field types include relations ({id,label}), currency, emails/phones/links composites. Ships backend functions + migrations. Needs a database (web-fullstack/api template). See the \`app-records\` skill.
41
+ - \`gipity add views\` - Generic UI over records-kit objects: sortable/filterable table with full-text search, create/edit/delete forms with type-appropriate widgets, kanban board with drag-to-update. Renders entirely from the field registry - zero per-object UI code. Requires the records kit. See the \`app-records\` skill.
42
+ - \`gipity add agent-api\` - Make your app agent-operable: named API keys (kit_api_keys) let agents and scripts write through the records kit's single write path with AGENT/API actor attribution - machine writes land on the same audit spine as human edits. Requires the records kit. See the \`app-records\` skill.
43
43
  - \`gipity add contacts\` - Source-agnostic contact data layer for lead-gen/CRM apps: import people from LinkedIn CSV + Gmail + pasted lists, resolve duplicates into one person while keeping EVERY value from every source with provenance (multi-valued attributes, never overwrites). Exact email/URL auto-merge; fuzzy name+company goes to a human merge-review queue (reversible). Re-imports detect job changes and emit signals. User-definable tags, full-text search, and a transactional event spine. Ships backend functions + migrations. Needs a database (web-fullstack/api template).
44
44
  - \`gipity add stripe\` - Charge your app's end-users for one-time purchases and subscriptions via Stripe. Owner connects their own Stripe account through Gipity-hosted onboarding (no API keys to paste); money lands in their account, Gipity takes a small platform fee. Ships a buy-button / pricing component, a subscription-status helper for gating UI, a webhook-verified fulfillment function, and the payments/subscriptions tables. The platform brokers checkout + signature-verified webhooks. Needs a database (web-fullstack/api template).
45
45
  - \`gipity add notify\` - Web push notifications for any web app, including iOS home-screen web apps (iOS 16.4+). The platform owns the VAPID keys, encryption, and delivery — no keys to paste, no crypto, no server. Ships a <gipity-notify-button>, a service worker, and a PWA manifest; the device subscribes itself and self-heals stale subscriptions. Send from a function with the injected notify() service (one flat credit per send, owner-billed) or test with \`gipity notify test\`. Works in any template — no database required.`;
@@ -88,7 +88,7 @@ Prefer a live Gipity URL over an ephemeral artifact. When you'd otherwise reach
88
88
 
89
89
  The full "when to add a template" rule and the definition of done are spelled out in the two sections at the end of this document. In short: if the user wants something deployable (web app, game, API), \`gipity add <template>\` first (default \`web-simple\`); for a one-off task (analysis, PDFs, data work), use \`gipity sandbox run\` instead; to add a reusable building block to an existing app (e.g. multiplayer), \`gipity add <kit>\`.
90
90
 
91
- Build loop: \`gipity add\` → edit files → \`gipity deploy dev\` \`gipity page inspect <url>\` → fix any errors → repeat until the definition of done is met.
91
+ Build loop: \`gipity add\` → edit files → \`gipity deploy dev --inspect\` → fix any errors → repeat until the definition of done is met. \`deploy --inspect\` deploys and then runs the page-inspect report on the live URL in one command; use a standalone \`gipity page inspect <url>\` only when re-checking without deploying.
92
92
 
93
93
  \`add\` writes real files to disk - Read a scaffolded file before your first Write/Edit to it, or the call fails \`"File has not been read yet"\`. Don't rewrite from memory of the template.
94
94
 
@@ -136,6 +136,7 @@ gipity bug report --category <cli|deploy|template|kit|db|docs|skill|service|sand
136
136
  - **Never include PII or user data** (emails, names, secrets, tokens, prompt/file contents) — describe the platform problem in the abstract.
137
137
  - File it for *platform* problems, not your own mistakes or the app's own bugs. One report per distinct problem.
138
138
  - Reports go to a review queue for the team to triage into fixes; see what you've filed with \`gipity bug list\`.
139
+ - Filed one by mistake? Withdraw it yourself: \`gipity bug retract <report-id> [--reason "<why>"]\` — works while it's still queued (status new/triaged). Never file a second report asking a human to close the first.
139
140
 
140
141
  ## Tool output is complete and synchronous
141
142
 
@@ -193,6 +194,7 @@ App development skills:
193
194
  - \`web-ui-patterns\` - default Gipity look (theme tokens) + web UI recipes - feeds, copy-to-clipboard
194
195
 
195
196
  Kit skills (reusable building blocks - \`gipity add <kit>\`):
197
+ - \`app-records\` - the records + views kits: registry-driven CRUD with search + audit history, and generated tables/kanban/forms (gipity add records)
196
198
  - \`audio-align\` - the audio-align kit: forced alignment of audio + lyrics into word-level timing JSON
197
199
  - \`chatbot\` - the chatbot kit: persona + scope guardrails + static knowledge, bubble widget or headless engine
198
200
 
@@ -202,7 +204,7 @@ Other key skills:
202
204
  - \`tts\` - agent-side speech tools (different from the \`app-tts\` HTTP service)`;
203
205
  export const DEFINITION_OF_DONE = `## Definition of done (build tasks)
204
206
  1. \`gipity deploy dev\` succeeds and you have a live URL.
205
- 2. \`gipity page inspect <url>\` returns no console errors and the page loads (HTTP 200, no blank screen).
207
+ 2. \`gipity page inspect <url>\` returns no console errors and the page loads (HTTP 200, no blank screen). (\`gipity deploy dev --inspect\` covers 1 and 2 in one command.)
206
208
  3. For apps with functions: \`gipity test\` passes.
207
209
  4. Non-rendered files the task called for (\`llms.txt\`, \`AGENTS.md\`, \`SKILL.md\`, \`robots.txt\`, served JSON, etc.): \`page inspect\` only sees rendered HTML, so verify them with \`gipity page fetch <url> <files...>\`. It flags any that 404 or come back as the static-host shell (a missing file is served as \`index.html\` with a 200, so a bare status check would pass) and checks each \`content-type\`.
208
210
  5. You told the user the live URL.
package/dist/sync.js CHANGED
@@ -29,7 +29,7 @@ import { get, del, downloadStream, ApiError } from './api.js';
29
29
  import { requireConfig, shouldIgnore, getConfigPath } from './config.js';
30
30
  import { formatSize, prompt, getAutoConfirm } from './utils.js';
31
31
  import { uploadOneFile, hashFile, guessMime, transferToS3, uploadInitBatch, uploadCompleteBatch, UploadConflictError, UPLOAD_CONCURRENCY, UPLOAD_INIT_BATCH_SIZE, UPLOAD_MAX_BYTES, UPLOAD_MAX_PATH_CHARS, } from './upload.js';
32
- import { DEFAULT_SYNC_IGNORE } from './setup.js';
32
+ import { DEFAULT_SYNC_IGNORE, SCRATCH_IGNORE } from './setup.js';
33
33
  const CONFIG_FILE = '.gipity.json';
34
34
  import * as tar from 'tar-stream';
35
35
  // ─── Tunables ──────────────────────────────────────────────────
@@ -659,10 +659,53 @@ export function readGipityIgnore(root) {
659
659
  /** The ignore list a sync/push actually runs with: the project config's
660
660
  * `ignore` (falling back to DEFAULT_SYNC_IGNORE when empty, so an empty list
661
661
  * never means "sync everything - node_modules, .git and all"), plus any
662
- * `.gipityignore` patterns. The ignore file itself never syncs. */
662
+ * `.gipityignore` patterns, plus - unconditionally - the scratch namespaces.
663
+ * The ignore file itself never syncs.
664
+ *
665
+ * SCRATCH_IGNORE is unioned in even when the config has its own `ignore`
666
+ * list, because scratch dirs are a PLATFORM CONVENTION, not a user
667
+ * preference: they must mirror the server's `isEphemeralSandboxPath`
668
+ * denylist (see the comment on SCRATCH_IGNORE in setup.ts) so the same
669
+ * paths are throwaway everywhere - the sandbox refuses to persist them and
670
+ * sync/deploy refuses to upload them. `config.ignore` is frozen into
671
+ * .gipity.json at link time, so any project linked before a scratch dir was
672
+ * added to the defaults would otherwise keep syncing it forever (tmp/ did
673
+ * exactly this). Appended LAST so a stray `!tmp/` negation earlier in the
674
+ * list can't re-include scratch; deduped so the matcher cache in
675
+ * config.ts keys on one canonical pattern set. */
663
676
  export function effectiveIgnore(root, configIgnore) {
664
677
  const base = configIgnore && configIgnore.length ? configIgnore : DEFAULT_SYNC_IGNORE;
665
- return [...base, GIPITY_IGNORE_FILE, ...readGipityIgnore(root)];
678
+ const merged = [...base, GIPITY_IGNORE_FILE, ...readGipityIgnore(root)];
679
+ return [...merged, ...SCRATCH_IGNORE.filter(p => !merged.includes(p))];
680
+ }
681
+ /** Fast, LOCAL-ONLY dirtiness probe: true when every local file matches its
682
+ * baseline entry by size+mtime and nothing was added or deleted since the
683
+ * last sync. Lets pre-action syncs (deploy) skip their server round trip:
684
+ * deploy reads server-side (VFS) state, so with nothing to push the sync
685
+ * adds no correctness, only latency. Conservative on purpose - any size or
686
+ * mtime movement reads as dirty (no hashing here), never-synced projects
687
+ * read as dirty, and callers then run the real sync(). */
688
+ export function isLocalTreeClean() {
689
+ try {
690
+ const config = requireConfig();
691
+ const root = projectDir();
692
+ const baseline = readBaseline(config.projectGuid);
693
+ if (!baseline.lastFullSync)
694
+ return false;
695
+ const local = walkLocal(root, effectiveIgnore(root, config.ignore), baseline.files);
696
+ const baselineFiles = baseline.files;
697
+ if (local.size !== Object.keys(baselineFiles).length)
698
+ return false;
699
+ for (const [path, info] of local) {
700
+ const prior = baselineFiles[path];
701
+ if (!prior || prior.size !== info.size || prior.mtime !== info.mtime)
702
+ return false;
703
+ }
704
+ return true;
705
+ }
706
+ catch {
707
+ return false;
708
+ }
666
709
  }
667
710
  export async function sync(opts = {}) {
668
711
  const config = requireConfig();
package/dist/trace.js ADDED
@@ -0,0 +1,69 @@
1
+ // Opt-in output tracing for diagnosing silent-success bugs (cli#125/#126/#108):
2
+ // invocations that exit 0 but print nothing. With GIPITY_TRACE_OUTPUT=1, every
3
+ // process.stdout/stderr write is teed as a JSONL record to
4
+ // ~/.gipity/trace/<yyyy-mm-dd_hh-mm-ss>-pid<pid>.jsonl, alongside start/exit
5
+ // markers carrying pid, argv, cwd, and TTY state. Diffing a trace against the
6
+ // captured tool output shows whether the bytes were ever emitted by this
7
+ // process (pointing at the environment) or never written (pointing at the CLI).
8
+ //
9
+ // Records are written with fs.writeSync on a raw fd, not a stream: the bug
10
+ // class under investigation is process teardown eating output, so the trace
11
+ // itself must not depend on stream flushing at exit.
12
+ import { openSync, writeSync, mkdirSync } from 'fs';
13
+ import { join } from 'path';
14
+ import { homedir } from 'os';
15
+ // Keyed on globalThis, not module state: the shim and the CLI entry are
16
+ // separate esbuild bundles loaded into one process (shim dynamic-imports the
17
+ // entry), so a module-level flag would double-install the tee.
18
+ const TRACE_KEY = Symbol.for('gipity.traceOutput');
19
+ /** Install the stdout/stderr tee if GIPITY_TRACE_OUTPUT=1. Idempotent per
20
+ * process; a re-entrant call (e.g. index.ts after shim.ts) logs a marker so
21
+ * the trace shows the in-process import chain actually ran. Never throws -
22
+ * tracing must not be able to break the CLI. */
23
+ export function installOutputTrace(label) {
24
+ if (process.env.GIPITY_TRACE_OUTPUT !== '1')
25
+ return;
26
+ try {
27
+ const g = globalThis;
28
+ const existing = g[TRACE_KEY];
29
+ if (existing) {
30
+ existing.emit({ event: 'reenter', label });
31
+ return;
32
+ }
33
+ const dir = join(process.env.GIPITY_DIR || join(homedir(), '.gipity'), 'trace');
34
+ mkdirSync(dir, { recursive: true });
35
+ const stamp = new Date().toISOString().slice(0, 19).replace('T', '_').replace(/:/g, '-');
36
+ const fd = openSync(join(dir, `${stamp}-pid${process.pid}.jsonl`), 'a');
37
+ const emit = (rec) => {
38
+ try {
39
+ writeSync(fd, JSON.stringify({ t: new Date().toISOString(), ...rec }) + '\n');
40
+ }
41
+ catch { /* trace write failure must never surface */ }
42
+ };
43
+ g[TRACE_KEY] = { emit };
44
+ emit({
45
+ event: 'start',
46
+ label,
47
+ pid: process.pid,
48
+ ppid: process.ppid,
49
+ argv: process.argv,
50
+ cwd: process.cwd(),
51
+ tty: { stdout: !!process.stdout.isTTY, stderr: !!process.stderr.isTTY },
52
+ });
53
+ for (const name of ['stdout', 'stderr']) {
54
+ const stream = process[name];
55
+ const original = stream.write.bind(stream);
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ stream.write = ((chunk, encoding, cb) => {
58
+ emit({
59
+ stream: name,
60
+ data: typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'),
61
+ });
62
+ return original(chunk, encoding, cb);
63
+ });
64
+ }
65
+ process.on('exit', (code) => emit({ event: 'exit', code }));
66
+ }
67
+ catch { /* tracing is best-effort only */ }
68
+ }
69
+ //# sourceMappingURL=trace.js.map
@@ -1,101 +1,176 @@
1
1
  #!/usr/bin/env node
2
- // Background updater. Invoked detached by the shim; can also be invoked
3
- // directly by `gipity update --force`.
4
- import { appendFileSync, existsSync } from 'fs';
5
- import { LOCAL_DIR, LOCAL_ENTRY, UPDATE_LOG, readState, writeState, updatesDisabled } from './state.js';
6
- import { resolveCommand, spawnSyncCommand } from '../platform.js';
7
- const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
2
+ import { createRequire as __cliCreateRequire } from 'module';
3
+ const require = __cliCreateRequire(import.meta.url);
4
+
5
+ // src/updater/check.ts
6
+ import { appendFileSync, existsSync as existsSync2 } from "fs";
7
+
8
+ // src/updater/state.ts
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
10
+ import { join } from "path";
11
+ import { homedir } from "os";
12
+ var GIPITY_DIR = join(homedir(), ".gipity");
13
+ var LOCAL_DIR = join(GIPITY_DIR, "local");
14
+ var LOCAL_PKG_DIR = join(LOCAL_DIR, "node_modules", "gipity");
15
+ var LOCAL_ENTRY = join(LOCAL_PKG_DIR, "dist", "index.js");
16
+ var STATE_FILE = join(GIPITY_DIR, "update-state.json");
17
+ var SETTINGS_FILE = join(GIPITY_DIR, "settings.json");
18
+ var UPDATE_LOG = join(GIPITY_DIR, "update.log");
19
+ var DEFAULT_STATE = {
20
+ installedVersion: null,
21
+ lastCheckAt: 0,
22
+ lastError: null,
23
+ updateChannel: "stable"
24
+ };
25
+ var DEFAULT_SETTINGS = {
26
+ autoUpdates: true
27
+ };
28
+ function ensureDir() {
29
+ mkdirSync(GIPITY_DIR, { recursive: true });
30
+ }
31
+ function readState() {
32
+ if (!existsSync(STATE_FILE)) return { ...DEFAULT_STATE };
33
+ try {
34
+ return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(STATE_FILE, "utf-8")) };
35
+ } catch {
36
+ return { ...DEFAULT_STATE };
37
+ }
38
+ }
39
+ function writeState(state) {
40
+ ensureDir();
41
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
42
+ }
43
+ function readSettings() {
44
+ if (!existsSync(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
45
+ try {
46
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync(SETTINGS_FILE, "utf-8")) };
47
+ } catch {
48
+ return { ...DEFAULT_SETTINGS };
49
+ }
50
+ }
51
+ function updatesDisabled() {
52
+ if (process.env["DISABLE_AUTOUPDATER"] === "1") return { disabled: true, reason: "DISABLE_AUTOUPDATER=1" };
53
+ if (process.env["CI"]) return { disabled: true, reason: "CI environment" };
54
+ if (!readSettings().autoUpdates) return { disabled: true, reason: "autoUpdates: false in settings.json" };
55
+ return { disabled: false };
56
+ }
57
+
58
+ // src/platform.ts
59
+ import { execSync, spawn, spawnSync } from "child_process";
60
+ function resolveCommand(cmd) {
61
+ if (process.platform !== "win32") return cmd;
62
+ try {
63
+ const lines = execSync(`where ${cmd}`, { encoding: "utf-8", windowsHide: true }).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
64
+ return lines.find((l) => l.endsWith(".exe")) || lines.find((l) => l.endsWith(".cmd")) || cmd;
65
+ } catch {
66
+ return `${cmd}.cmd`;
67
+ }
68
+ }
69
+ function isBatchShim(cmd) {
70
+ return process.platform === "win32" && /\.(cmd|bat)$/i.test(cmd);
71
+ }
72
+ function winBatchInvocation(cmd, args) {
73
+ const comspec = process.env.ComSpec || "cmd.exe";
74
+ const quote = (t) => `"${String(t).replace(/"/g, '""')}"`;
75
+ const line = [cmd, ...args].map(quote).join(" ");
76
+ return { file: comspec, args: ["/d", "/s", "/c", `"${line}"`] };
77
+ }
78
+ function spawnSyncCommand(cmd, args = [], options = {}) {
79
+ if (isBatchShim(cmd)) {
80
+ const inv = winBatchInvocation(cmd, args);
81
+ return spawnSync(inv.file, inv.args, { windowsHide: true, ...options, windowsVerbatimArguments: true });
82
+ }
83
+ return spawnSync(cmd, [...args], { windowsHide: true, ...options });
84
+ }
85
+
86
+ // src/updater/check.ts
87
+ var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
8
88
  function log(line) {
9
- try {
10
- appendFileSync(UPDATE_LOG, `[${new Date().toISOString()}] ${line}\n`);
11
- }
12
- catch { /* ignore */ }
89
+ try {
90
+ appendFileSync(UPDATE_LOG, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}
91
+ `);
92
+ } catch {
93
+ }
13
94
  }
14
- export function compareSemver(a, b) {
15
- const pa = a.split('.').map(Number);
16
- const pb = b.split('.').map(Number);
17
- for (let i = 0; i < 3; i++) {
18
- const da = pa[i] ?? 0;
19
- const db = pb[i] ?? 0;
20
- if (da !== db)
21
- return da - db;
22
- }
23
- return 0;
95
+ function compareSemver(a, b) {
96
+ const pa = a.split(".").map(Number);
97
+ const pb = b.split(".").map(Number);
98
+ for (let i = 0; i < 3; i++) {
99
+ const da = pa[i] ?? 0;
100
+ const db = pb[i] ?? 0;
101
+ if (da !== db) return da - db;
102
+ }
103
+ return 0;
24
104
  }
25
105
  async function fetchLatestVersion() {
26
- const res = await fetch('https://registry.npmjs.org/gipity/latest', {
27
- headers: { accept: 'application/json' },
28
- });
29
- if (!res.ok)
30
- throw new Error(`registry HTTP ${res.status}`);
31
- const json = await res.json();
32
- if (!json.version)
33
- throw new Error('no version in registry response');
34
- return json.version;
106
+ const res = await fetch("https://registry.npmjs.org/gipity/latest", {
107
+ headers: { accept: "application/json" }
108
+ });
109
+ if (!res.ok) throw new Error(`registry HTTP ${res.status}`);
110
+ const json = await res.json();
111
+ if (!json.version) throw new Error("no version in registry response");
112
+ return json.version;
35
113
  }
36
114
  function installVersion(version) {
37
- // --ignore-scripts: this runs unattended in the background, so don't let a
38
- // compromised package's install lifecycle hooks execute. gipity ships
39
- // precompiled (dist/) and its deps need no build step, so nothing is lost.
40
- const res = spawnSyncCommand(resolveCommand('npm'), ['install', '--silent', '--no-audit', '--no-fund', '--ignore-scripts', `gipity@${version}`], {
41
- cwd: LOCAL_DIR,
42
- stdio: 'ignore',
43
- });
44
- if (res.error)
45
- log(`npm spawn failed: ${res.error.message}`);
46
- return res.status === 0 && existsSync(LOCAL_ENTRY);
115
+ const res = spawnSyncCommand(resolveCommand("npm"), ["install", "--silent", "--no-audit", "--no-fund", "--ignore-scripts", `gipity@${version}`], {
116
+ cwd: LOCAL_DIR,
117
+ stdio: "ignore"
118
+ });
119
+ if (res.error) log(`npm spawn failed: ${res.error.message}`);
120
+ return res.status === 0 && existsSync2(LOCAL_ENTRY);
47
121
  }
48
- export async function runCheck(opts = {}) {
49
- const state = readState();
50
- if (!opts.force) {
51
- const dis = updatesDisabled();
52
- if (dis.disabled) {
53
- log(`skipped: ${dis.reason}`);
54
- return { updated: false, reason: dis.reason };
55
- }
56
- if (Date.now() - state.lastCheckAt < CHECK_INTERVAL_MS) {
57
- return { updated: false, reason: 'cache-fresh' };
58
- }
59
- }
60
- let latest;
61
- try {
62
- latest = await fetchLatestVersion();
63
- }
64
- catch (e) {
65
- const msg = e instanceof Error ? e.message : String(e);
66
- state.lastError = `fetch failed: ${msg}`;
67
- state.lastCheckAt = Date.now();
68
- writeState(state);
69
- log(state.lastError);
70
- return { updated: false, reason: state.lastError };
122
+ async function runCheck(opts = {}) {
123
+ const state = readState();
124
+ if (!opts.force) {
125
+ const dis = updatesDisabled();
126
+ if (dis.disabled) {
127
+ log(`skipped: ${dis.reason}`);
128
+ return { updated: false, reason: dis.reason };
71
129
  }
72
- const current = state.installedVersion ?? '0.0.0';
73
- if (compareSemver(latest, current) <= 0) {
74
- state.lastError = null;
75
- state.lastCheckAt = Date.now();
76
- writeState(state);
77
- log(`up-to-date (current=${current}, latest=${latest})`);
78
- return { updated: false, reason: 'up-to-date' };
130
+ if (Date.now() - state.lastCheckAt < CHECK_INTERVAL_MS) {
131
+ return { updated: false, reason: "cache-fresh" };
79
132
  }
80
- log(`upgrading ${current} → ${latest}`);
81
- const ok = installVersion(latest);
133
+ }
134
+ let latest;
135
+ try {
136
+ latest = await fetchLatestVersion();
137
+ } catch (e) {
138
+ const msg = e instanceof Error ? e.message : String(e);
139
+ state.lastError = `fetch failed: ${msg}`;
82
140
  state.lastCheckAt = Date.now();
83
- if (ok) {
84
- state.installedVersion = latest;
85
- state.lastError = null;
86
- writeState(state);
87
- log(`upgraded to ${latest}`);
88
- return { updated: true, from: current, to: latest };
89
- }
90
- state.lastError = `npm install gipity@${latest} failed`;
91
141
  writeState(state);
92
142
  log(state.lastError);
93
143
  return { updated: false, reason: state.lastError };
144
+ }
145
+ const current = state.installedVersion ?? "0.0.0";
146
+ if (compareSemver(latest, current) <= 0) {
147
+ state.lastError = null;
148
+ state.lastCheckAt = Date.now();
149
+ writeState(state);
150
+ log(`up-to-date (current=${current}, latest=${latest})`);
151
+ return { updated: false, reason: "up-to-date" };
152
+ }
153
+ log(`upgrading ${current} \u2192 ${latest}`);
154
+ const ok = installVersion(latest);
155
+ state.lastCheckAt = Date.now();
156
+ if (ok) {
157
+ state.installedVersion = latest;
158
+ state.lastError = null;
159
+ writeState(state);
160
+ log(`upgraded to ${latest}`);
161
+ return { updated: true, from: current, to: latest };
162
+ }
163
+ state.lastError = `npm install gipity@${latest} failed`;
164
+ writeState(state);
165
+ log(state.lastError);
166
+ return { updated: false, reason: state.lastError };
94
167
  }
95
- // Direct invocation (detached background)
96
- const isDirect = import.meta.url === `file://${process.argv[1]}`;
168
+ var isDirect = import.meta.url === `file://${process.argv[1]}`;
97
169
  if (isDirect) {
98
- const force = process.argv.includes('--force');
99
- runCheck({ force }).catch((e) => log(`unhandled: ${e instanceof Error ? e.message : String(e)}`));
170
+ const force = process.argv.includes("--force");
171
+ runCheck({ force }).catch((e) => log(`unhandled: ${e instanceof Error ? e.message : String(e)}`));
100
172
  }
101
- //# sourceMappingURL=check.js.map
173
+ export {
174
+ compareSemver,
175
+ runCheck
176
+ };