gipity 1.1.2 → 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/catalog.js +1 -3
- package/dist/commands/build.js +99 -29
- package/dist/commands/deploy.js +6 -2
- package/dist/commands/file.js +18 -0
- package/dist/commands/fn.js +6 -6
- package/dist/commands/generate.js +76 -12
- package/dist/commands/job.js +125 -8
- package/dist/commands/page-eval.js +79 -10
- package/dist/commands/page-inspect.js +26 -1
- package/dist/commands/page-screenshot.js +33 -4
- package/dist/commands/records.js +14 -1
- package/dist/commands/sandbox.js +181 -9
- package/dist/commands/service.js +6 -1
- package/dist/commands/test.js +32 -0
- package/dist/commands/update.js +4 -0
- package/dist/commands/upload.js +69 -121
- package/dist/helpers/body.js +117 -0
- package/dist/helpers/duration.js +34 -0
- package/dist/helpers/index.js +2 -0
- package/dist/index.js +848 -375
- package/dist/knowledge.js +4 -7
- package/dist/updater/bootstrap.js +27 -16
- package/dist/updater/check.js +86 -10
- package/dist/updater/install.js +82 -0
- package/dist/updater/shim.js +90 -27
- package/package.json +2 -2
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
|
|
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 {
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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.
|
|
42
|
-
// cause; res.status is null in that case, so prefer
|
|
43
|
-
const firstLine = stderr.split('\n').map(l => l.trim()).find(l => l.length > 0)
|
|
44
|
-
|| (res.
|
|
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
|
}
|
package/dist/updater/check.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
package/dist/updater/shim.js
CHANGED
|
@@ -38,9 +38,9 @@ function spawnSyncCommand(cmd, args = [], options = {}) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// src/updater/shim.ts
|
|
41
|
-
import { existsSync as
|
|
41
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
42
42
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
43
|
-
import { dirname, resolve, join as
|
|
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
|
|
80
|
-
import { join as
|
|
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
|
|
136
|
+
return existsSync3(LOCAL_ENTRY);
|
|
83
137
|
}
|
|
84
138
|
function bootstrap(version, quiet = false) {
|
|
85
|
-
|
|
86
|
-
const pkgJsonPath =
|
|
87
|
-
if (!
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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.
|
|
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
|
|
123
|
-
import { join as
|
|
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 =
|
|
136
|
-
|
|
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(
|
|
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 =
|
|
236
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
174
237
|
var pkgRoot = resolve(__dirname, "..", "..");
|
|
175
|
-
var shimPkg = JSON.parse(readFileSync2(
|
|
176
|
-
var isDevLink =
|
|
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 =
|
|
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
|
+
"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",
|