gipity 1.1.3 → 1.1.5
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/adopt-cwd.js +1 -1
- package/dist/banner.js +3 -5
- package/dist/catalog.js +1 -3
- package/dist/commands/add.js +1 -1
- package/dist/commands/build.js +111 -40
- package/dist/commands/chat.js +1 -1
- package/dist/commands/deploy.js +7 -3
- package/dist/commands/file.js +18 -0
- package/dist/commands/fn.js +6 -6
- package/dist/commands/generate.js +80 -16
- package/dist/commands/gmail.js +1 -1
- package/dist/commands/job.js +132 -10
- package/dist/commands/load.js +2 -2
- package/dist/commands/login.js +7 -2
- package/dist/commands/page-eval.js +82 -13
- package/dist/commands/page-fetch.js +14 -10
- package/dist/commands/page-inspect.js +28 -3
- package/dist/commands/page-screenshot.js +36 -7
- package/dist/commands/page-test.js +1 -1
- package/dist/commands/records.js +14 -1
- package/dist/commands/remove.js +1 -1
- package/dist/commands/sandbox.js +182 -10
- package/dist/commands/save.js +1 -1
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/service.js +7 -2
- package/dist/commands/setup.js +4 -6
- package/dist/commands/status.js +41 -12
- package/dist/commands/storage.js +2 -2
- package/dist/commands/sync.js +15 -0
- package/dist/commands/test.js +33 -1
- package/dist/commands/update.js +4 -0
- package/dist/commands/upload.js +69 -121
- package/dist/commands/workflow.js +1 -1
- 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 +1173 -620
- package/dist/knowledge.js +7 -7
- package/dist/login-flow.js +44 -2
- package/dist/relay/onboarding.js +26 -44
- package/dist/sync.js +6 -6
- 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/dist/utils.js +60 -3
- 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.
|
|
@@ -150,12 +148,14 @@ Write files locally - Gipity's editor hooks (installed into Claude Code, Grok, a
|
|
|
150
148
|
|
|
151
149
|
To keep local-only material (research clones, scratch data, vendored references) in the project directory without syncing or deploying it, list it in a \`.gipityignore\` at the project root - gitignore-style, one pattern per line, \`#\` comments. Ignored paths are invisible to sync in both directions; anything that already synced before being ignored stays on the server until you delete it.
|
|
152
150
|
|
|
151
|
+
**WSL trap: user-dropped files may be in a Windows-side twin.** On WSL, a same-named folder often exists at \`C:\\Users\\<name>\\GipityProjects\\<project>\` (visible as \`/mnt/c/Users/<name>/GipityProjects/<project>\`) - from Windows Explorer it looks like "the project folder", so users drop new files (audio, images) there. Nothing syncs from it. If the user says files are "in the project" but you can't find them, check that path before searching wider; \`gipity sync\` prints a notice when such a twin exists.
|
|
152
|
+
|
|
153
153
|
### Where files go: deploy only ships \`src/\`
|
|
154
154
|
|
|
155
155
|
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
156
|
|
|
157
157
|
- **\`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.
|
|
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. 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
159
|
- **\`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
160
|
- **\`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
161
|
|
|
@@ -171,7 +171,7 @@ App services skills (load before calling \`/services/*\` endpoints):
|
|
|
171
171
|
- \`app-audio\` - sound effects, music, transcription
|
|
172
172
|
- \`app-auth\` - sign in with Gipity, popup vs redirect
|
|
173
173
|
- \`app-files\` - uploads, variants, file listing
|
|
174
|
-
- \`app-image\` - text-to-image
|
|
174
|
+
- \`app-image\` - text-to-image AND instruction editing (pass \`images\` to edit an uploaded photo); providers, sizes, aspect ratios
|
|
175
175
|
- \`app-llm\` - chat completions, streaming, image input
|
|
176
176
|
- \`app-location\` - user location & reverse geocoding for deployed apps (first-party - no third-party geocoder)
|
|
177
177
|
- \`app-notify\` - web push notifications for deployed apps (incl. iOS home-screen web apps) - notify kit + injected notify() service, platform owns the keys
|
|
@@ -186,6 +186,7 @@ App development skills:
|
|
|
186
186
|
- \`app-debugging\` - debug a deployed app: page inspect/eval, screenshots, function logs
|
|
187
187
|
- \`app-development\` - functions, database, and API
|
|
188
188
|
- \`app-import\` - import apps from GitHub/.gip bundles (incl. Vercel/Replit/Lovable porting) and export any project as a portable .gip - app_import tool, gipity save/load
|
|
189
|
+
- \`app-records\` - Gipity Records: declared tables → validated CRUD, provenance, workflows, auto UI
|
|
189
190
|
- \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the isolated test DB)
|
|
190
191
|
- \`deploy\` - the deploy pipeline & gipity.yaml manifest
|
|
191
192
|
- \`jobs\` - long-running CPU + GPU compute jobs (Python / Node / bash)
|
|
@@ -194,7 +195,6 @@ App development skills:
|
|
|
194
195
|
- \`web-ui-patterns\` - default Gipity look (theme tokens) + web UI recipes - feeds, copy-to-clipboard
|
|
195
196
|
|
|
196
197
|
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
198
|
- \`audio-align\` - the audio-align kit: forced alignment of audio + lyrics into word-level timing JSON
|
|
199
199
|
- \`chatbot\` - the chatbot kit: persona + scope guardrails + static knowledge, bubble widget or headless engine
|
|
200
200
|
|
package/dist/login-flow.js
CHANGED
|
@@ -10,9 +10,48 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { publicPost } from './api.js';
|
|
12
12
|
import { getAuth, saveAuth } from './auth.js';
|
|
13
|
+
import { getConfig } from './config.js';
|
|
13
14
|
import { prompt, decodeJwtExp } from './utils.js';
|
|
14
|
-
import { success, error as clrError, muted } from './colors.js';
|
|
15
|
+
import { success, error as clrError, warning, muted } from './colors.js';
|
|
15
16
|
import { flushBugQueue } from './bug-queue.js';
|
|
17
|
+
/** True when a fresh-account result would be a SURPRISE worth warning about:
|
|
18
|
+
* either this directory is linked to an existing project, or this machine
|
|
19
|
+
* already held a session for this exact email. A genuine first-time signup
|
|
20
|
+
* (nothing linked, no prior session) must stay silent. */
|
|
21
|
+
function newAccountWouldBeUnexpected(email, priorAuth) {
|
|
22
|
+
return !!getConfig() || priorAuth?.email.toLowerCase() === email.toLowerCase().trim();
|
|
23
|
+
}
|
|
24
|
+
/** Called right after `/auth/login` (send-code), before the code is entered -
|
|
25
|
+
* the server already knows at this point whether the email matches an
|
|
26
|
+
* existing account (bug cli#S2: logging into the wrong/new account was
|
|
27
|
+
* previously silent until a later "not found" error). */
|
|
28
|
+
export function warnBeforeCodeIfUnexpectedNewAccount(isNewUser, email, indent = '') {
|
|
29
|
+
if (isNewUser !== true)
|
|
30
|
+
return;
|
|
31
|
+
const priorAuth = getAuth();
|
|
32
|
+
if (!newAccountWouldBeUnexpected(email, priorAuth))
|
|
33
|
+
return;
|
|
34
|
+
const config = getConfig();
|
|
35
|
+
console.log(`${indent}${warning(`No existing Gipity account for ${email} — entering the code will CREATE a new one.`)}`);
|
|
36
|
+
if (config) {
|
|
37
|
+
console.log(`${indent}${muted(`This directory is linked to project ${config.projectSlug} (account ${config.accountSlug}). If you meant to log into that account, stop and re-check the email before entering the code.`)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Called right after `/auth/verify` succeeds. `priorAuth` must be captured
|
|
41
|
+
* BEFORE `saveAuth()` overwrites the cache - it's the only way to tell "this
|
|
42
|
+
* machine already had a session for this email" from "first login ever". */
|
|
43
|
+
export function warnIfUnexpectedNewAccount(isNewUser, email, priorAuth, indent = '') {
|
|
44
|
+
if (isNewUser !== true)
|
|
45
|
+
return;
|
|
46
|
+
if (!newAccountWouldBeUnexpected(email, priorAuth))
|
|
47
|
+
return;
|
|
48
|
+
const config = getConfig();
|
|
49
|
+
console.log(`${indent}${warning(`Logged into a NEW, empty account for ${email} — no prior account existed for this email.`)}`);
|
|
50
|
+
if (config) {
|
|
51
|
+
console.log(`${indent}${muted(`This directory is linked to project ${config.projectSlug} (account ${config.accountSlug}), which the new account does not own — project / skill / sync commands will fail with "not found".`)}`);
|
|
52
|
+
}
|
|
53
|
+
console.log(`${indent}${muted('If you expected an existing account, run: gipity status — then gipity login again with the correct email.')}`);
|
|
54
|
+
}
|
|
16
55
|
/** Prompt for email + 6-digit code, persist the tokens, and return the new
|
|
17
56
|
* auth. Exits the process on empty input or a bad token (the caller can't
|
|
18
57
|
* proceed without a session). */
|
|
@@ -22,13 +61,15 @@ export async function interactiveLogin() {
|
|
|
22
61
|
console.error(`\n ${clrError('Email required.')}`);
|
|
23
62
|
process.exit(1);
|
|
24
63
|
}
|
|
25
|
-
await publicPost('/auth/login', { email });
|
|
64
|
+
const sendRes = await publicPost('/auth/login', { email });
|
|
26
65
|
console.log(' Check your email for a 6-digit code.\n');
|
|
66
|
+
warnBeforeCodeIfUnexpectedNewAccount(sendRes.isNewUser, email, ' ');
|
|
27
67
|
const code = await prompt(' Code: ');
|
|
28
68
|
if (!code) {
|
|
29
69
|
console.error(`\n ${clrError('Code required.')}`);
|
|
30
70
|
process.exit(1);
|
|
31
71
|
}
|
|
72
|
+
const priorAuth = getAuth();
|
|
32
73
|
const res = await publicPost('/auth/verify', { email, code });
|
|
33
74
|
const exp = decodeJwtExp(res.accessToken);
|
|
34
75
|
if (!exp) {
|
|
@@ -38,6 +79,7 @@ export async function interactiveLogin() {
|
|
|
38
79
|
const expiresAt = new Date(exp * 1000).toISOString();
|
|
39
80
|
saveAuth({ accessToken: res.accessToken, refreshToken: res.refreshToken, email, expiresAt });
|
|
40
81
|
console.log(` ${success(`Logged in (${email}).`)}`);
|
|
82
|
+
warnIfUnexpectedNewAccount(res.isNewUser, email, priorAuth, ' ');
|
|
41
83
|
const delivered = await flushBugQueue().catch(() => 0);
|
|
42
84
|
if (delivered > 0) {
|
|
43
85
|
console.log(` ${muted(`Delivered ${delivered} queued bug report${delivered === 1 ? '' : 's'}.`)}`);
|
package/dist/relay/onboarding.js
CHANGED
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* All the non-interactive primitives live in `setup.ts` (`pairDevice`,
|
|
13
13
|
* `startDaemon`, `installAutostart`); this module is only the prompts + copy.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import { prompt, confirm } from '../utils.js';
|
|
15
|
+
import { prompt, confirm, isWsl } from '../utils.js';
|
|
17
16
|
import { bold, brand, dim, success, error as clrError, muted } from '../colors.js';
|
|
18
17
|
import * as state from './state.js';
|
|
19
18
|
import { UnsupportedPlatformError } from './installers.js';
|
|
@@ -27,21 +26,6 @@ export const ensureDaemonRunning = startDaemon;
|
|
|
27
26
|
* ended up enabled (or was already), `false` if the user declined or a step
|
|
28
27
|
* failed. Non-interactive flows (e.g. `gipity claude -p`) must not call this.
|
|
29
28
|
*/
|
|
30
|
-
/** True inside Windows Subsystem for Linux (either env marker or kernel
|
|
31
|
-
* string). Used to explain autostart failures accurately - WSL ships without
|
|
32
|
-
* a systemd user session unless the user opts in via /etc/wsl.conf. */
|
|
33
|
-
function isWsl() {
|
|
34
|
-
if (process.platform !== 'linux')
|
|
35
|
-
return false;
|
|
36
|
-
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
|
|
37
|
-
return true;
|
|
38
|
-
try {
|
|
39
|
-
return /microsoft/i.test(readFileSync('/proc/version', 'utf-8'));
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
29
|
export async function runRelaySetup(opts) {
|
|
46
30
|
const alreadyAnswered = state.getRelayEnabled() !== undefined;
|
|
47
31
|
// `gipity claude` first-run: honor the user's prior choice and don't re-ask.
|
|
@@ -66,13 +50,13 @@ export async function runRelaySetup(opts) {
|
|
|
66
50
|
console.log('');
|
|
67
51
|
console.log(` ${success('Registered as')} ${bold(existingDevice.name)} ${muted(`(${existingDevice.guid})`)}`);
|
|
68
52
|
if (state.isPaused()) {
|
|
69
|
-
console.log(` ${dim('The relay is paused
|
|
53
|
+
console.log(` ${dim('The relay is paused - resume it with')} ${brand('gipity relay resume')}${dim('.')}`);
|
|
70
54
|
}
|
|
71
55
|
else {
|
|
72
56
|
console.log(` ${dim('The relay is running. Open')} ${brand('gipity.ai')}${dim(' and type `/claude` to drive Claude Code here.')}`);
|
|
73
57
|
}
|
|
74
58
|
console.log('');
|
|
75
|
-
console.log(` ${dim('To register this computer again
|
|
59
|
+
console.log(` ${dim('To register this computer again (for example under a different name),')}`);
|
|
76
60
|
console.log(` ${dim('unregister it first, then re-run setup:')}`);
|
|
77
61
|
console.log(` ${brand('gipity relay revoke')} ${dim('# unpairs this computer and removes the login service')}`);
|
|
78
62
|
console.log(` ${brand('gipity connect')} ${dim('# register it again (asks for a new name)')}`);
|
|
@@ -82,34 +66,36 @@ export async function runRelaySetup(opts) {
|
|
|
82
66
|
}
|
|
83
67
|
// Header. `gipity setup` frames it as the deliberate action it is; the
|
|
84
68
|
// `gipity claude` first-run frames it as an optional add-on it's offering.
|
|
69
|
+
// Lines are pre-wrapped short so they never rewrap in a normal terminal.
|
|
85
70
|
if (opts.mode === 'run-now') {
|
|
86
71
|
console.log(` ${bold('Set up this computer as a relay')}`);
|
|
87
|
-
console.log(` ${dim('
|
|
88
|
-
console.log(` ${dim('
|
|
72
|
+
console.log(` ${dim('Gipity drives the coding agents on this computer from the web (')}${brand('gipity.ai')}${dim(')')}`);
|
|
73
|
+
console.log(` ${dim('on any browser, desktop or phone. Uses your Claude, Codex, or Grok')}`);
|
|
74
|
+
console.log(` ${dim('subscription - the cheapest way to pay for tokens.')}`);
|
|
89
75
|
}
|
|
90
76
|
else {
|
|
91
|
-
console.log(` ${bold('
|
|
92
|
-
console.log(` ${dim('
|
|
93
|
-
console.log('');
|
|
94
|
-
console.log(` ${dim('Enable now (takes 2 seconds) or turn on later with')} ${brand('gipity connect')}`);
|
|
77
|
+
console.log(` ${bold('Optional:')} ${dim('Gipity can drive the coding agents on this computer from the')}`);
|
|
78
|
+
console.log(` ${dim('web (')}${brand('gipity.ai')}${dim(') on any browser, desktop or phone. Like Claude Code remote')}`);
|
|
79
|
+
console.log(` ${dim('control, but one pane for all your coding agents.')}`);
|
|
95
80
|
}
|
|
96
81
|
console.log('');
|
|
97
82
|
const promptText = opts.mode === 'run-now'
|
|
98
|
-
? ' Set up remote control on this computer
|
|
99
|
-
: ' Enable remote control
|
|
83
|
+
? ' Set up remote control on this computer'
|
|
84
|
+
: ' Enable remote control';
|
|
100
85
|
const enable = await confirm(promptText, { default: 'yes' });
|
|
101
86
|
if (!enable) {
|
|
102
87
|
state.setRelayEnabled(false);
|
|
103
|
-
console.log(` ${muted('Skipped.')}`);
|
|
88
|
+
console.log(` ${muted('Skipped. Turn on later with')} ${brand('gipity connect')}${muted('.')}`);
|
|
104
89
|
console.log('');
|
|
105
90
|
return false;
|
|
106
91
|
}
|
|
92
|
+
console.log('');
|
|
107
93
|
// Device name - show a friendly default (owner + device kind); Enter accepts.
|
|
108
94
|
const defaultName = friendlyDeviceName();
|
|
109
95
|
const rawName = await prompt(` Device name [${bold(defaultName)}]: `);
|
|
110
96
|
const name = (rawName || defaultName).trim();
|
|
111
97
|
if (!name || name.length > 100) {
|
|
112
|
-
console.error(` ${clrError('Device name must be 1
|
|
98
|
+
console.error(` ${clrError('Device name must be 1-100 non-whitespace characters. Skipping.')}`);
|
|
113
99
|
state.setRelayEnabled(false);
|
|
114
100
|
return false;
|
|
115
101
|
}
|
|
@@ -128,13 +114,12 @@ export async function runRelaySetup(opts) {
|
|
|
128
114
|
// run instead of silently opting the user out forever.
|
|
129
115
|
return false;
|
|
130
116
|
}
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
117
|
+
// The user said yes to remote control - start the daemon now, no extra
|
|
118
|
+
// question. `gipity build` keeps it running on future runs.
|
|
119
|
+
startDaemon();
|
|
120
|
+
console.log('');
|
|
136
121
|
// Offer OS-level autostart (launchd / systemd --user / Task Scheduler).
|
|
137
|
-
const autostartOs = await confirm(' Also start at OS login
|
|
122
|
+
const autostartOs = await confirm(' Also start at OS login', { default: 'yes' });
|
|
138
123
|
if (autostartOs) {
|
|
139
124
|
try {
|
|
140
125
|
const res = installAutostart();
|
|
@@ -152,9 +137,7 @@ export async function runRelaySetup(opts) {
|
|
|
152
137
|
console.log(` ${muted('Autostart install returned non-zero - you can run')} ${brand('gipity relay install')} ${muted('later.')}`);
|
|
153
138
|
}
|
|
154
139
|
}
|
|
155
|
-
|
|
156
|
-
console.log(` ${success('Auto-start installed.')} ${dim(res.summary)}`);
|
|
157
|
-
}
|
|
140
|
+
// Success is silent - the Done line below covers it.
|
|
158
141
|
}
|
|
159
142
|
catch (err) {
|
|
160
143
|
if (err instanceof UnsupportedPlatformError) {
|
|
@@ -166,16 +149,15 @@ export async function runRelaySetup(opts) {
|
|
|
166
149
|
}
|
|
167
150
|
}
|
|
168
151
|
// Diagnostics consent - default on, clearly opt-out. Only ask once.
|
|
152
|
+
// (No personal data or file paths are ever sent; `gipity relay diagnostics
|
|
153
|
+
// on|off` flips it later.)
|
|
169
154
|
if (state.getDiagnosticsConsent() === undefined) {
|
|
170
|
-
|
|
155
|
+
console.log('');
|
|
156
|
+
const diag = await confirm(' Share anonymous diagnostics info', { default: 'yes' });
|
|
171
157
|
state.setDiagnosticsConsent(diag);
|
|
172
|
-
console.log(` ${dim(diag
|
|
173
|
-
? 'Thanks - no personal data or file paths are ever sent. Turn off anytime with `gipity relay diagnostics off`.'
|
|
174
|
-
: 'Diagnostics off. Turn on anytime with `gipity relay diagnostics on`.')}`);
|
|
175
158
|
}
|
|
176
159
|
console.log('');
|
|
177
|
-
console.log(` ${success(`
|
|
178
|
-
console.log(` ${dim('In the Gipity web CLI, type `/claude` to dispatch messages to this PC.')}`);
|
|
160
|
+
console.log(` ${success(`Done! ${bold(device.name)} is set up. New chats from gipity.ai can now execute here.`)}`);
|
|
179
161
|
console.log('');
|
|
180
162
|
return true;
|
|
181
163
|
}
|
package/dist/sync.js
CHANGED
|
@@ -154,7 +154,7 @@ export async function acquireLock(progress) {
|
|
|
154
154
|
// First time we're forced to wait: open the spinner so the wait is visible
|
|
155
155
|
// (with an elapsed timer) rather than reading as a frozen process.
|
|
156
156
|
if (!waitSpinner) {
|
|
157
|
-
waitSpinner = progress?.spinner('Waiting for another sync to finish
|
|
157
|
+
waitSpinner = progress?.spinner('Waiting for another sync to finish...') ?? null;
|
|
158
158
|
}
|
|
159
159
|
await new Promise(r => setTimeout(r, LOCK_POLL_MS));
|
|
160
160
|
}
|
|
@@ -735,9 +735,9 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
735
735
|
const config = { projectGuid, ignore };
|
|
736
736
|
const p = opts.progress;
|
|
737
737
|
const baseline = readBaseline(projectGuid);
|
|
738
|
-
p?.phase('Scanning local files
|
|
738
|
+
p?.phase('Scanning local files...');
|
|
739
739
|
const local = walkLocal(root, ignore, baseline.files);
|
|
740
|
-
p?.phase('Checking Gipity for changes
|
|
740
|
+
p?.phase('Checking Gipity for changes...');
|
|
741
741
|
const remote = await fetchRemote(projectGuid);
|
|
742
742
|
// Ignored paths are invisible on BOTH sides: filtering only the local walk
|
|
743
743
|
// would classify a remote copy as "added" (pull it), then next pass as a
|
|
@@ -761,7 +761,7 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
761
761
|
needHash.push(path);
|
|
762
762
|
}
|
|
763
763
|
if (needHash.length)
|
|
764
|
-
p?.phase(`Hashing ${needHash.length} file${needHash.length === 1 ? '' : 's'}
|
|
764
|
+
p?.phase(`Hashing ${needHash.length} file${needHash.length === 1 ? '' : 's'}...`);
|
|
765
765
|
await ensureLocalHashes(root, local, needHash);
|
|
766
766
|
const planned = plan(local, remote, baseline.files);
|
|
767
767
|
if (opts.plan) {
|
|
@@ -890,7 +890,7 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
890
890
|
// Either way we fall through to the single-file recovery below rather than
|
|
891
891
|
// proceeding on a half-empty tree - a partial download must never be
|
|
892
892
|
// mistaken for a complete one.
|
|
893
|
-
errors.push(`Bulk download incomplete (${err.message}); recovering files individually
|
|
893
|
+
errors.push(`Bulk download incomplete (${err.message}); recovering files individually...`);
|
|
894
894
|
}
|
|
895
895
|
finally {
|
|
896
896
|
// Settle the bar even if the extracted-byte tally fell short of the
|
|
@@ -903,7 +903,7 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
903
903
|
// an earlier truncated pull.
|
|
904
904
|
const missing = wantedDownloads.filter(a => !downloadedBytes.has(a.path));
|
|
905
905
|
if (missing.length) {
|
|
906
|
-
p?.phase(`Recovering ${missing.length} file${missing.length === 1 ? '' : 's'} the bulk download dropped
|
|
906
|
+
p?.phase(`Recovering ${missing.length} file${missing.length === 1 ? '' : 's'} the bulk download dropped...`);
|
|
907
907
|
for (const a of missing) {
|
|
908
908
|
// Verify against the remote's content hash: the downloads pass records
|
|
909
909
|
// this sha into the baseline, so bytes that don't match it must be
|
|
@@ -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
|