gipity 1.1.3 → 1.1.4

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,12 +37,10 @@ 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). 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
40
  - \`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
41
  - \`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
- - \`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.`;
42
+ - \`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.
43
+ - \`gipity add servicenow\` - Use ServiceNow tables as a data source: OAuth (client-credentials) authenticated polling pull into a local Postgres mirror (sn_records, any table, configurable), plus write-back into ServiceNow via the Table API. Optional real-time sync layers a ServiceNow Business Rule + Script Include on top, pushing changes to a webhook the instant they happen. Ships a cron workflow for the pull cycle, sn-pull/sn-write/sn-webhook functions, and setup scripts that automate the ServiceNow-side configuration. Needs a database (web-fullstack/api template).`;
46
44
  export const SKILLS_CONTENT = `# Gipity Integration
47
45
 
48
46
  Gipity is the cloud platform your project runs on - hosting, databases, deployment, file storage, code execution, workflows, and monitoring. Gip is the cloud agent that runs on Gipity.
@@ -155,7 +153,7 @@ To keep local-only material (research clones, scratch data, vendored references)
155
153
  Deploy is opt-in, not opt-out: the \`files\` phase uploads **only** what's under \`src/\` (plus \`functions/\` and \`migrations/\` as backend, not CDN files). Anything else at the project root is kept but never deployed. Put each kind of file in the right bucket so scratch and reference material can't bloat a deploy:
156
154
 
157
155
  - **\`src/\`** - the app itself. Synced **and** deployed to the CDN. Only app code, assets, and pages belong here.
158
- - **\`tmp/\`** - ephemeral scratch: file conversions, intermediate outputs, design staging. **Already ignored** (never synced, never deployed) - the one place to do throwaway work. Use this single root. (\`*_tmp/\` dirs and \`.gipityscratch/\` are auto-ignored too, as a safety net, so legacy scattered scratch like \`_vsd_tmp/\` can't leak - but write new scratch to \`tmp/\`, not scattered dirs.) Because it never syncs, \`tmp/\` is also never mirrored into the sandbox: **don't stage \`gipity sandbox run\` inputs here** - the sandbox won't see them. Stage inputs under \`src/\`/\`docs/\` and delete them after.
156
+ - **\`tmp/\`** - ephemeral scratch: file conversions, intermediate outputs, design staging. **Already ignored** (never synced, never deployed) - the one place to do throwaway work. Use this single root. Leave scratch files where they are when you're done; there's nothing to clean up (no \`rm\` turn) - they're invisible to sync and deploy. (\`*_tmp/\` dirs and \`.gipityscratch/\` are auto-ignored too, as a safety net, so legacy scattered scratch like \`_vsd_tmp/\` can't leak - but write new scratch to \`tmp/\`, not scattered dirs.) Because it never syncs, \`tmp/\` is also never mirrored into the sandbox: **don't stage \`gipity sandbox run\` inputs here** - the sandbox won't see them. Stage inputs under \`src/\`/\`docs/\` and delete them after.
159
157
  - **\`docs/\`** - reference material you want to keep: UI/architecture diagrams, design decks, notes, ADRs. Synced and versioned on the server (backed up, rollback-able) but **never deployed**, because it's outside \`src/\`. This is the home for "keep forever, don't ship" artifacts.
160
158
  - **\`tests/\`** - \`*.test.js\` suites. Synced, run by \`gipity test\`, never deployed. \`gipity test list [path]\` lists the test files (and what a filter selects) without running them.
161
159
 
@@ -171,7 +169,7 @@ App services skills (load before calling \`/services/*\` endpoints):
171
169
  - \`app-audio\` - sound effects, music, transcription
172
170
  - \`app-auth\` - sign in with Gipity, popup vs redirect
173
171
  - \`app-files\` - uploads, variants, file listing
174
- - \`app-image\` - text-to-image only (no input image / editing); providers, sizes, aspect ratios
172
+ - \`app-image\` - text-to-image AND instruction editing (pass \`images\` to edit an uploaded photo); providers, sizes, aspect ratios
175
173
  - \`app-llm\` - chat completions, streaming, image input
176
174
  - \`app-location\` - user location & reverse geocoding for deployed apps (first-party - no third-party geocoder)
177
175
  - \`app-notify\` - web push notifications for deployed apps (incl. iOS home-screen web apps) - notify kit + injected notify() service, platform owns the keys
@@ -194,7 +192,6 @@ App development skills:
194
192
  - \`web-ui-patterns\` - default Gipity look (theme tokens) + web UI recipes - feeds, copy-to-clipboard
195
193
 
196
194
  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)
198
195
  - \`audio-align\` - the audio-align kit: forced alignment of audio + lyrics into word-level timing JSON
199
196
  - \`chatbot\` - the chatbot kit: persona + scope guardrails + static knowledge, bubble widget or headless engine
200
197
 
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { LOCAL_DIR, LOCAL_ENTRY, LOCAL_PKG_DIR, writeState, readState } from './state.js';
4
- import { resolveCommand, spawnSyncCommand } from '../platform.js';
4
+ import { npmInstallGipity, isWedged, resetLocalTree, acquireUpdateLock, releaseUpdateLock } from './install.js';
5
5
  export function isBootstrapped() {
6
6
  return existsSync(LOCAL_ENTRY);
7
7
  }
@@ -20,28 +20,39 @@ export function bootstrap(version, quiet = false) {
20
20
  if (!existsSync(pkgJsonPath)) {
21
21
  writeFileSync(pkgJsonPath, JSON.stringify({ name: 'gipity-local', private: true, version: '0.0.0' }, null, 2));
22
22
  }
23
+ if (!acquireUpdateLock()) {
24
+ // A background updater is mid-install; run from the current build now and
25
+ // pick up the local install on a later invocation.
26
+ if (!quiet)
27
+ process.stderr.write(`gipity: another update is in progress - using the currently installed build.\n`);
28
+ return false;
29
+ }
23
30
  if (!quiet)
24
31
  process.stderr.write(`Setting up gipity local install at ~/.gipity/local (one-time)...\n`);
25
- // --ignore-scripts: don't run install lifecycle hooks (gipity ships
26
- // precompiled, deps need no build), so a compromised package can't execute
27
- // code during this self-managed install.
28
- const res = spawnSyncCommand(resolveCommand('npm'), ['install', '--no-audit', '--no-fund', '--ignore-scripts', `gipity@${version}`], {
29
- cwd: LOCAL_DIR,
30
- stdio: ['ignore', 'pipe', 'pipe'],
31
- encoding: 'utf-8',
32
- });
33
- if (res.status !== 0 || !existsSync(LOCAL_ENTRY)) {
32
+ let res;
33
+ try {
34
+ res = npmInstallGipity(version);
35
+ if (!res.ok && !res.spawnError && isWedged(res.stderr)) {
36
+ // Interrupted-install corruption fails every npm run in the dir forever;
37
+ // wipe the tree and retry once.
38
+ resetLocalTree();
39
+ res = npmInstallGipity(version);
40
+ }
41
+ }
42
+ finally {
43
+ releaseUpdateLock();
44
+ }
45
+ if (!res.ok) {
34
46
  if (!quiet) {
35
- const stderr = (res.stderr || '').toString();
36
- const notPublished = /E404|No matching version|notarget/i.test(stderr);
47
+ const notPublished = /E404|No matching version|notarget/i.test(res.stderr);
37
48
  if (notPublished) {
38
49
  process.stderr.write(`gipity v${version} is not yet published to npm - using the currently installed build.\n`);
39
50
  }
40
51
  else {
41
- // res.error (e.g. ENOENT when npm can't be launched) carries the real
42
- // cause; res.status is null in that case, so prefer the error message.
43
- const firstLine = stderr.split('\n').map(l => l.trim()).find(l => l.length > 0)
44
- || (res.error ? res.error.message : `npm exit ${res.status}`);
52
+ // res.spawnError (e.g. ENOENT when npm can't be launched) carries the
53
+ // real cause; res.status is null in that case, so prefer its message.
54
+ const firstLine = res.stderr.split('\n').map(l => l.trim()).find(l => l.length > 0)
55
+ || (res.spawnError ? res.spawnError.message : `npm exit ${res.status}`);
45
56
  const reason = firstLine.length > 160 ? firstLine.slice(0, 157) + '...' : firstLine;
46
57
  process.stderr.write(`gipity: could not set up local install (${reason}). Using the currently installed build.\n`);
47
58
  }
@@ -3,7 +3,7 @@ import { createRequire as __cliCreateRequire } from 'module';
3
3
  const require = __cliCreateRequire(import.meta.url);
4
4
 
5
5
  // src/updater/check.ts
6
- import { appendFileSync, existsSync as existsSync2 } from "fs";
6
+ import { appendFileSync } from "fs";
7
7
 
8
8
  // src/updater/state.ts
9
9
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
@@ -55,6 +55,10 @@ function updatesDisabled() {
55
55
  return { disabled: false };
56
56
  }
57
57
 
58
+ // src/updater/install.ts
59
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
60
+ import { dirname, join as join2 } from "path";
61
+
58
62
  // src/platform.ts
59
63
  import { execSync, spawn, spawnSync } from "child_process";
60
64
  function resolveCommand(cmd) {
@@ -83,6 +87,56 @@ function spawnSyncCommand(cmd, args = [], options = {}) {
83
87
  return spawnSync(cmd, [...args], { windowsHide: true, ...options });
84
88
  }
85
89
 
90
+ // src/updater/install.ts
91
+ var UPDATE_LOCK = join2(GIPITY_DIR, "update.lock");
92
+ var LOCK_STALE_MS = 10 * 60 * 1e3;
93
+ function npmInstallGipity(version) {
94
+ const res = spawnSyncCommand(resolveCommand("npm"), ["install", "--no-audit", "--no-fund", "--ignore-scripts", `gipity@${version}`], {
95
+ cwd: LOCAL_DIR,
96
+ stdio: ["ignore", "ignore", "pipe"],
97
+ encoding: "utf-8"
98
+ });
99
+ return {
100
+ ok: res.status === 0 && existsSync2(LOCAL_ENTRY),
101
+ status: res.status,
102
+ stderr: (res.stderr || "").toString(),
103
+ spawnError: res.error
104
+ };
105
+ }
106
+ var WEDGE_CODES = /\b(ENOTEMPTY|EEXIST|ENOTDIR|EISDIR|EJSONPARSE)\b/;
107
+ function isWedged(stderr) {
108
+ return WEDGE_CODES.test(stderr);
109
+ }
110
+ function resetLocalTree(dir = LOCAL_DIR) {
111
+ mkdirSync2(dir, { recursive: true });
112
+ rmSync(join2(dir, "node_modules"), { recursive: true, force: true });
113
+ rmSync(join2(dir, "package-lock.json"), { force: true });
114
+ writeFileSync2(join2(dir, "package.json"), JSON.stringify({ name: "gipity-local", private: true, version: "0.0.0" }, null, 2));
115
+ }
116
+ function acquireUpdateLock(lockPath = UPDATE_LOCK) {
117
+ mkdirSync2(dirname(lockPath), { recursive: true });
118
+ try {
119
+ writeFileSync2(lockPath, String(process.pid), { flag: "wx" });
120
+ return true;
121
+ } catch {
122
+ try {
123
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
124
+ unlinkSync(lockPath);
125
+ writeFileSync2(lockPath, String(process.pid), { flag: "wx" });
126
+ return true;
127
+ }
128
+ } catch {
129
+ }
130
+ return false;
131
+ }
132
+ }
133
+ function releaseUpdateLock(lockPath = UPDATE_LOCK) {
134
+ try {
135
+ unlinkSync(lockPath);
136
+ } catch {
137
+ }
138
+ }
139
+
86
140
  // src/updater/check.ts
87
141
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
88
142
  function log(line) {
@@ -112,12 +166,24 @@ async function fetchLatestVersion() {
112
166
  return json.version;
113
167
  }
114
168
  function installVersion(version) {
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);
169
+ let res = npmInstallGipity(version);
170
+ if (!res.ok && !res.spawnError && isWedged(res.stderr)) {
171
+ log(`wedged install tree detected; wiping node_modules and retrying:
172
+ ${res.stderr.trim().slice(-2e3)}`);
173
+ resetLocalTree();
174
+ res = npmInstallGipity(version);
175
+ if (res.ok) log("clean reinstall succeeded");
176
+ }
177
+ if (res.ok) return { ok: true };
178
+ if (res.spawnError) {
179
+ log(`npm spawn failed: ${res.spawnError.message}`);
180
+ return { ok: false, detail: res.spawnError.message };
181
+ }
182
+ if (res.stderr.trim()) log(`npm stderr (exit ${res.status}):
183
+ ${res.stderr.trim().slice(-2e3)}`);
184
+ if (res.status === 0) return { ok: false, detail: "npm succeeded but the installed package is missing dist/index.js" };
185
+ const firstLine = res.stderr.split("\n").map((l) => l.trim()).find((l) => l.length > 0) || `npm exit ${res.status}`;
186
+ return { ok: false, detail: firstLine.length > 160 ? firstLine.slice(0, 157) + "..." : firstLine };
121
187
  }
122
188
  async function runCheck(opts = {}) {
123
189
  const state = readState();
@@ -150,17 +216,27 @@ async function runCheck(opts = {}) {
150
216
  log(`up-to-date (current=${current}, latest=${latest})`);
151
217
  return { updated: false, reason: "up-to-date" };
152
218
  }
219
+ if (!acquireUpdateLock()) {
220
+ log("skipped: another update is already in progress");
221
+ return { updated: false, reason: "another update is already in progress" };
222
+ }
153
223
  log(`upgrading ${current} \u2192 ${latest}`);
154
- const ok = installVersion(latest);
155
224
  state.lastCheckAt = Date.now();
156
- if (ok) {
225
+ writeState(state);
226
+ let install;
227
+ try {
228
+ install = installVersion(latest);
229
+ } finally {
230
+ releaseUpdateLock();
231
+ }
232
+ if (install.ok) {
157
233
  state.installedVersion = latest;
158
234
  state.lastError = null;
159
235
  writeState(state);
160
236
  log(`upgraded to ${latest}`);
161
237
  return { updated: true, from: current, to: latest };
162
238
  }
163
- state.lastError = `npm install gipity@${latest} failed`;
239
+ state.lastError = `npm install gipity@${latest} failed` + (install.detail ? `: ${install.detail}` : "");
164
240
  writeState(state);
165
241
  log(state.lastError);
166
242
  return { updated: false, reason: state.lastError };
@@ -0,0 +1,82 @@
1
+ // Shared npm-install runner for the bootstrap and background-update paths,
2
+ // plus recovery for the wedged-tree failure mode and a lock that keeps two
3
+ // updaters out of ~/.gipity/local at the same time.
4
+ import { existsSync, mkdirSync, rmSync, statSync, unlinkSync, writeFileSync } from 'fs';
5
+ import { dirname, join } from 'path';
6
+ import { GIPITY_DIR, LOCAL_DIR, LOCAL_ENTRY } from './state.js';
7
+ import { resolveCommand, spawnSyncCommand } from '../platform.js';
8
+ export const UPDATE_LOCK = join(GIPITY_DIR, 'update.lock');
9
+ const LOCK_STALE_MS = 10 * 60 * 1000;
10
+ /**
11
+ * Run `npm install gipity@<version>` in ~/.gipity/local with stderr captured
12
+ * so failures carry the real cause (npm --silent suppresses even its error
13
+ * report, so it is deliberately absent).
14
+ *
15
+ * --ignore-scripts: this can run unattended in the background, so don't let a
16
+ * compromised package's install lifecycle hooks execute. gipity ships
17
+ * precompiled (dist/) and its deps need no build step, so nothing is lost.
18
+ */
19
+ export function npmInstallGipity(version) {
20
+ const res = spawnSyncCommand(resolveCommand('npm'), ['install', '--no-audit', '--no-fund', '--ignore-scripts', `gipity@${version}`], {
21
+ cwd: LOCAL_DIR,
22
+ stdio: ['ignore', 'ignore', 'pipe'],
23
+ encoding: 'utf-8',
24
+ });
25
+ return {
26
+ ok: res.status === 0 && existsSync(LOCAL_ENTRY),
27
+ status: res.status,
28
+ stderr: (res.stderr || '').toString(),
29
+ spawnError: res.error,
30
+ };
31
+ }
32
+ // An interrupted install (shutdown mid-upgrade, an overlapping npm process)
33
+ // leaves node_modules half-renamed, after which npm fails every install in
34
+ // the dir with one of these until the tree is wiped. Reproduced: ENOTEMPTY
35
+ // renaming node_modules/gipity over a leftover node_modules/.gipity-<hash>
36
+ // staging dir. Deliberately narrow - transient failures (network, registry,
37
+ // E404) must NOT wipe a still-working tree.
38
+ const WEDGE_CODES = /\b(ENOTEMPTY|EEXIST|ENOTDIR|EISDIR|EJSONPARSE)\b/;
39
+ export function isWedged(stderr) {
40
+ return WEDGE_CODES.test(stderr);
41
+ }
42
+ /** Wipe the local install tree back to a pristine, installable state.
43
+ * `dir` is overridable for tests only. */
44
+ export function resetLocalTree(dir = LOCAL_DIR) {
45
+ mkdirSync(dir, { recursive: true });
46
+ rmSync(join(dir, 'node_modules'), { recursive: true, force: true });
47
+ rmSync(join(dir, 'package-lock.json'), { force: true });
48
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'gipity-local', private: true, version: '0.0.0' }, null, 2));
49
+ }
50
+ /**
51
+ * Take the cross-process update lock. The shim spawns a detached updater on
52
+ * every gipity command, so during a long install another invocation would
53
+ * otherwise start a second npm install in the same dir (a reproduced way to
54
+ * corrupt it). A lock older than LOCK_STALE_MS is from a dead updater
55
+ * (machine shutdown mid-install) and is taken over.
56
+ * `lockPath` is overridable for tests only.
57
+ */
58
+ export function acquireUpdateLock(lockPath = UPDATE_LOCK) {
59
+ mkdirSync(dirname(lockPath), { recursive: true });
60
+ try {
61
+ writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
62
+ return true;
63
+ }
64
+ catch {
65
+ try {
66
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
67
+ unlinkSync(lockPath);
68
+ writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
69
+ return true;
70
+ }
71
+ }
72
+ catch { /* lost the race to another process */ }
73
+ return false;
74
+ }
75
+ }
76
+ export function releaseUpdateLock(lockPath = UPDATE_LOCK) {
77
+ try {
78
+ unlinkSync(lockPath);
79
+ }
80
+ catch { /* already gone */ }
81
+ }
82
+ //# sourceMappingURL=install.js.map
@@ -38,9 +38,9 @@ function spawnSyncCommand(cmd, args = [], options = {}) {
38
38
  }
39
39
 
40
40
  // src/updater/shim.ts
41
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
41
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
42
42
  import { fileURLToPath, pathToFileURL } from "url";
43
- import { dirname, resolve, join as join4 } from "path";
43
+ import { dirname as dirname2, resolve, join as join5 } from "path";
44
44
 
45
45
  // src/updater/state.ts
46
46
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
@@ -76,33 +76,96 @@ function writeState(state) {
76
76
  }
77
77
 
78
78
  // src/updater/bootstrap.ts
79
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
80
- import { join as join2 } from "path";
79
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
80
+ import { join as join3 } from "path";
81
+
82
+ // src/updater/install.ts
83
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
84
+ import { dirname, join as join2 } from "path";
85
+ var UPDATE_LOCK = join2(GIPITY_DIR, "update.lock");
86
+ var LOCK_STALE_MS = 10 * 60 * 1e3;
87
+ function npmInstallGipity(version) {
88
+ const res = spawnSyncCommand(resolveCommand("npm"), ["install", "--no-audit", "--no-fund", "--ignore-scripts", `gipity@${version}`], {
89
+ cwd: LOCAL_DIR,
90
+ stdio: ["ignore", "ignore", "pipe"],
91
+ encoding: "utf-8"
92
+ });
93
+ return {
94
+ ok: res.status === 0 && existsSync2(LOCAL_ENTRY),
95
+ status: res.status,
96
+ stderr: (res.stderr || "").toString(),
97
+ spawnError: res.error
98
+ };
99
+ }
100
+ var WEDGE_CODES = /\b(ENOTEMPTY|EEXIST|ENOTDIR|EISDIR|EJSONPARSE)\b/;
101
+ function isWedged(stderr) {
102
+ return WEDGE_CODES.test(stderr);
103
+ }
104
+ function resetLocalTree(dir = LOCAL_DIR) {
105
+ mkdirSync2(dir, { recursive: true });
106
+ rmSync(join2(dir, "node_modules"), { recursive: true, force: true });
107
+ rmSync(join2(dir, "package-lock.json"), { force: true });
108
+ writeFileSync2(join2(dir, "package.json"), JSON.stringify({ name: "gipity-local", private: true, version: "0.0.0" }, null, 2));
109
+ }
110
+ function acquireUpdateLock(lockPath = UPDATE_LOCK) {
111
+ mkdirSync2(dirname(lockPath), { recursive: true });
112
+ try {
113
+ writeFileSync2(lockPath, String(process.pid), { flag: "wx" });
114
+ return true;
115
+ } catch {
116
+ try {
117
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
118
+ unlinkSync(lockPath);
119
+ writeFileSync2(lockPath, String(process.pid), { flag: "wx" });
120
+ return true;
121
+ }
122
+ } catch {
123
+ }
124
+ return false;
125
+ }
126
+ }
127
+ function releaseUpdateLock(lockPath = UPDATE_LOCK) {
128
+ try {
129
+ unlinkSync(lockPath);
130
+ } catch {
131
+ }
132
+ }
133
+
134
+ // src/updater/bootstrap.ts
81
135
  function isBootstrapped() {
82
- return existsSync2(LOCAL_ENTRY);
136
+ return existsSync3(LOCAL_ENTRY);
83
137
  }
84
138
  function bootstrap(version, quiet = false) {
85
- mkdirSync2(LOCAL_DIR, { recursive: true });
86
- const pkgJsonPath = join2(LOCAL_DIR, "package.json");
87
- if (!existsSync2(pkgJsonPath)) {
88
- writeFileSync2(pkgJsonPath, JSON.stringify({ name: "gipity-local", private: true, version: "0.0.0" }, null, 2));
139
+ mkdirSync3(LOCAL_DIR, { recursive: true });
140
+ const pkgJsonPath = join3(LOCAL_DIR, "package.json");
141
+ if (!existsSync3(pkgJsonPath)) {
142
+ writeFileSync3(pkgJsonPath, JSON.stringify({ name: "gipity-local", private: true, version: "0.0.0" }, null, 2));
143
+ }
144
+ if (!acquireUpdateLock()) {
145
+ if (!quiet) process.stderr.write(`gipity: another update is in progress - using the currently installed build.
146
+ `);
147
+ return false;
89
148
  }
90
149
  if (!quiet) process.stderr.write(`Setting up gipity local install at ~/.gipity/local (one-time)...
91
150
  `);
92
- const res = spawnSyncCommand(resolveCommand("npm"), ["install", "--no-audit", "--no-fund", "--ignore-scripts", `gipity@${version}`], {
93
- cwd: LOCAL_DIR,
94
- stdio: ["ignore", "pipe", "pipe"],
95
- encoding: "utf-8"
96
- });
97
- if (res.status !== 0 || !existsSync2(LOCAL_ENTRY)) {
151
+ let res;
152
+ try {
153
+ res = npmInstallGipity(version);
154
+ if (!res.ok && !res.spawnError && isWedged(res.stderr)) {
155
+ resetLocalTree();
156
+ res = npmInstallGipity(version);
157
+ }
158
+ } finally {
159
+ releaseUpdateLock();
160
+ }
161
+ if (!res.ok) {
98
162
  if (!quiet) {
99
- const stderr = (res.stderr || "").toString();
100
- const notPublished = /E404|No matching version|notarget/i.test(stderr);
163
+ const notPublished = /E404|No matching version|notarget/i.test(res.stderr);
101
164
  if (notPublished) {
102
165
  process.stderr.write(`gipity v${version} is not yet published to npm - using the currently installed build.
103
166
  `);
104
167
  } else {
105
- const firstLine = stderr.split("\n").map((l) => l.trim()).find((l) => l.length > 0) || (res.error ? res.error.message : `npm exit ${res.status}`);
168
+ const firstLine = res.stderr.split("\n").map((l) => l.trim()).find((l) => l.length > 0) || (res.spawnError ? res.spawnError.message : `npm exit ${res.status}`);
106
169
  const reason = firstLine.length > 160 ? firstLine.slice(0, 157) + "..." : firstLine;
107
170
  process.stderr.write(`gipity: could not set up local install (${reason}). Using the currently installed build.
108
171
  `);
@@ -119,8 +182,8 @@ function bootstrap(version, quiet = false) {
119
182
  }
120
183
 
121
184
  // src/trace.ts
122
- import { openSync, writeSync, mkdirSync as mkdirSync3 } from "fs";
123
- import { join as join3 } from "path";
185
+ import { openSync, writeSync, mkdirSync as mkdirSync4 } from "fs";
186
+ import { join as join4 } from "path";
124
187
  import { homedir as homedir2 } from "os";
125
188
  var TRACE_KEY = /* @__PURE__ */ Symbol.for("gipity.traceOutput");
126
189
  function installOutputTrace(label) {
@@ -132,10 +195,10 @@ function installOutputTrace(label) {
132
195
  existing.emit({ event: "reenter", label });
133
196
  return;
134
197
  }
135
- const dir = join3(process.env.GIPITY_DIR || join3(homedir2(), ".gipity"), "trace");
136
- mkdirSync3(dir, { recursive: true });
198
+ const dir = join4(process.env.GIPITY_DIR || join4(homedir2(), ".gipity"), "trace");
199
+ mkdirSync4(dir, { recursive: true });
137
200
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
138
- const fd = openSync(join3(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
201
+ const fd = openSync(join4(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
139
202
  const emit = (rec) => {
140
203
  try {
141
204
  writeSync(fd, JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), ...rec }) + "\n");
@@ -170,12 +233,12 @@ function installOutputTrace(label) {
170
233
 
171
234
  // src/updater/shim.ts
172
235
  installOutputTrace("shim");
173
- var __dirname = dirname(fileURLToPath(import.meta.url));
236
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
174
237
  var pkgRoot = resolve(__dirname, "..", "..");
175
- var shimPkg = JSON.parse(readFileSync2(join4(pkgRoot, "package.json"), "utf-8"));
176
- var isDevLink = existsSync3(join4(pkgRoot, "src"));
238
+ var shimPkg = JSON.parse(readFileSync2(join5(pkgRoot, "package.json"), "utf-8"));
239
+ var isDevLink = existsSync4(join5(pkgRoot, "src"));
177
240
  function startBackgroundUpdater() {
178
- const checkScript = join4(__dirname, "check.js");
241
+ const checkScript = join5(__dirname, "check.js");
179
242
  try {
180
243
  const child = spawnCommand(process.execPath, [checkScript], {
181
244
  detached: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gipity",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
5
5
  "bin": {
6
6
  "gipity": "dist/updater/shim.js",
@@ -14,7 +14,7 @@
14
14
  "prepack": "npm run build",
15
15
  "dev": "tsc --watch",
16
16
  "test": "npm run test:smoke",
17
- "test:smoke": "npm run build && node --test dist/__tests__/utils.test.js dist/__tests__/platform.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-unretrievable.test.js dist/__tests__/sync-clean-check.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/api-401-retry.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/session-pool.test.js dist/__tests__/relay-diagnostics.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/ingest-queue.test.js dist/__tests__/media-upload.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/capture-parsers.test.js dist/__tests__/agents.test.js dist/__tests__/capture-lock.test.js dist/__tests__/capture-resolve.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-bug.test.js dist/__tests__/cli-cmd-bug-queue.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-storage.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-setup.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js dist/__tests__/setup-codex-hooks.test.js dist/__tests__/trace.test.js",
17
+ "test:smoke": "npm run build && node --test dist/__tests__/utils.test.js dist/__tests__/platform.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-unretrievable.test.js dist/__tests__/sync-clean-check.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/api-401-retry.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/build-picker.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/session-pool.test.js dist/__tests__/relay-diagnostics.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/ingest-queue.test.js dist/__tests__/media-upload.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/capture-parsers.test.js dist/__tests__/agents.test.js dist/__tests__/capture-lock.test.js dist/__tests__/capture-resolve.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-bug.test.js dist/__tests__/cli-cmd-bug-queue.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-storage.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-setup.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js dist/__tests__/setup-codex-hooks.test.js dist/__tests__/trace.test.js",
18
18
  "test:smoke:quick": "node scripts/smoke-quick.mjs",
19
19
  "test:e2e": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-live.test.js dist/__tests__/cli-e2e-sync-live.test.js dist/__tests__/cli-e2e-sync-stress-live.test.js dist/__tests__/cli-e2e-rollback-live.test.js dist/__tests__/cli-e2e-services-media-live.test.js dist/__tests__/cli-e2e-workflow-live.test.js dist/__tests__/cli-e2e-sandbox-live.test.js dist/__tests__/cli-e2e-page-fetch-live.test.js dist/__tests__/cli-e2e-page-test-live.test.js",
20
20
  "test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",