gipity 1.0.397 → 1.0.398
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 +10 -1
- package/dist/api.js +5 -3
- package/dist/client-context.js +63 -0
- package/dist/commands/claude.js +37 -32
- package/dist/commands/credits.js +53 -14
- package/dist/commands/generate.js +5 -4
- package/dist/commands/sync.js +8 -2
- package/dist/helpers/command.js +16 -1
- package/dist/platform.js +26 -0
- package/dist/relay/daemon.js +9 -2
- package/dist/relay/onboarding.js +5 -2
- package/dist/setup.js +11 -3
- package/dist/sync.js +112 -9
- package/dist/updater/bootstrap.js +6 -2
- package/dist/updater/check.js +4 -1
- package/package.json +4 -3
package/dist/adopt-cwd.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `.gipity.json`, sync, and install hooks/skills/gitignore.
|
|
12
12
|
*/
|
|
13
13
|
import { readdirSync, statSync } from 'fs';
|
|
14
|
-
import { join, resolve, sep } from 'path';
|
|
14
|
+
import { join, resolve, sep, parse as parsePath } from 'path';
|
|
15
15
|
import { homedir } from 'os';
|
|
16
16
|
import { get, post, ApiError } from './api.js';
|
|
17
17
|
import { isSyncIgnored } from './setup.js';
|
|
@@ -103,6 +103,15 @@ export function canAdoptCwd(cwd) {
|
|
|
103
103
|
'/tmp', '/var', '/etc', '/usr', '/opt',
|
|
104
104
|
'/mnt', '/mnt/c', '/mnt/d', '/mnt/wsl',
|
|
105
105
|
]);
|
|
106
|
+
// Filesystem root (POSIX `/` already covered by `sep`, but on Windows this
|
|
107
|
+
// catches drive roots like `C:\`). Plus the common Windows system dirs.
|
|
108
|
+
if (norm === parsePath(norm).root)
|
|
109
|
+
return false;
|
|
110
|
+
for (const envVar of ['windir', 'SystemRoot', 'ProgramFiles', 'ProgramFiles(x86)']) {
|
|
111
|
+
const v = process.env[envVar];
|
|
112
|
+
if (v && norm === resolve(v))
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
106
115
|
if (blocked.has(norm))
|
|
107
116
|
return false;
|
|
108
117
|
// Workspace-parent heuristic: a directory at depth ≤1 below $HOME that
|
package/dist/api.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Readable } from 'stream';
|
|
|
2
2
|
import * as tar from 'tar-stream';
|
|
3
3
|
import { getAuth, refreshTokenIfNeeded } from './auth.js';
|
|
4
4
|
import { resolveApiBase, requireConfig, saveConfig } from './config.js';
|
|
5
|
+
import { clientHeaders } from './client-context.js';
|
|
5
6
|
export class ApiError extends Error {
|
|
6
7
|
statusCode;
|
|
7
8
|
code;
|
|
@@ -32,6 +33,7 @@ async function bearerToken() {
|
|
|
32
33
|
}
|
|
33
34
|
async function getHeaders() {
|
|
34
35
|
return {
|
|
36
|
+
...clientHeaders(),
|
|
35
37
|
'Authorization': `Bearer ${await bearerToken()}`,
|
|
36
38
|
'Content-Type': 'application/json',
|
|
37
39
|
};
|
|
@@ -137,7 +139,7 @@ export async function sendMessage(message) {
|
|
|
137
139
|
export async function download(path) {
|
|
138
140
|
const url = `${baseUrl()}${path}`;
|
|
139
141
|
const res = await fetch(url, {
|
|
140
|
-
headers: { 'Authorization': `Bearer ${await bearerToken()}` },
|
|
142
|
+
headers: { ...clientHeaders(), 'Authorization': `Bearer ${await bearerToken()}` },
|
|
141
143
|
});
|
|
142
144
|
if (!res.ok) {
|
|
143
145
|
throw new ApiError(res.status, 'DOWNLOAD_ERROR', `Download failed: ${res.statusText}`);
|
|
@@ -149,7 +151,7 @@ export async function downloadStream(path) {
|
|
|
149
151
|
const { Readable } = await import('stream');
|
|
150
152
|
const url = `${baseUrl()}${path}`;
|
|
151
153
|
const res = await fetch(url, {
|
|
152
|
-
headers: { 'Authorization': `Bearer ${await bearerToken()}` },
|
|
154
|
+
headers: { ...clientHeaders(), 'Authorization': `Bearer ${await bearerToken()}` },
|
|
153
155
|
});
|
|
154
156
|
if (!res.ok) {
|
|
155
157
|
throw new ApiError(res.status, 'DOWNLOAD_ERROR', `Download failed: ${res.statusText}`);
|
|
@@ -199,7 +201,7 @@ export async function publicPost(path, body) {
|
|
|
199
201
|
const url = `${baseUrl()}${path}`;
|
|
200
202
|
const res = await fetch(url, {
|
|
201
203
|
method: 'POST',
|
|
202
|
-
headers: { 'Content-Type': 'application/json' },
|
|
204
|
+
headers: { ...clientHeaders(), 'Content-Type': 'application/json' },
|
|
203
205
|
body: JSON.stringify(body),
|
|
204
206
|
});
|
|
205
207
|
if (!res.ok) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
function cliVersion() {
|
|
5
|
+
try {
|
|
6
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const pkg = JSON.parse(readFileSync(resolve(dir, '../package.json'), 'utf-8'));
|
|
8
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return 'unknown';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function hasEnvPrefix(prefix) {
|
|
15
|
+
return Object.keys(process.env).some((k) => k.startsWith(prefix));
|
|
16
|
+
}
|
|
17
|
+
function isCi() {
|
|
18
|
+
const ci = (process.env.CI ?? '').toLowerCase();
|
|
19
|
+
return ci === '1' || ci === 'true' || !!process.env.GITHUB_ACTIONS;
|
|
20
|
+
}
|
|
21
|
+
/** Identify the coding harness from its env footprint. Order = specificity. Exported for tests. */
|
|
22
|
+
export function detectHarness() {
|
|
23
|
+
// Claude Code is the most reliable: CLAUDECODE=1 plus a versioned exec path.
|
|
24
|
+
if (process.env.CLAUDECODE === '1' || process.env.CLAUDE_CODE_ENTRYPOINT) {
|
|
25
|
+
const version = process.env.CLAUDE_CODE_EXECPATH?.match(/versions\/([^/\\]+)/)?.[1];
|
|
26
|
+
return { harness: 'claude-code', harnessVersion: version, harnessSession: process.env.CLAUDE_CODE_SESSION_ID };
|
|
27
|
+
}
|
|
28
|
+
if (hasEnvPrefix('CODEX_'))
|
|
29
|
+
return { harness: 'codex' };
|
|
30
|
+
if (process.env.CURSOR_TRACE_ID || hasEnvPrefix('CURSOR_') || (process.env.TERM_PROGRAM ?? '').toLowerCase().includes('cursor')) {
|
|
31
|
+
return { harness: 'cursor' };
|
|
32
|
+
}
|
|
33
|
+
if (hasEnvPrefix('AIDER_'))
|
|
34
|
+
return { harness: 'aider' };
|
|
35
|
+
if (process.env.GEMINI_CLI)
|
|
36
|
+
return { harness: 'gemini' };
|
|
37
|
+
if (isCi())
|
|
38
|
+
return { harness: 'ci' };
|
|
39
|
+
return { harness: 'manual' };
|
|
40
|
+
}
|
|
41
|
+
let cached;
|
|
42
|
+
export function clientContext() {
|
|
43
|
+
if (cached)
|
|
44
|
+
return cached;
|
|
45
|
+
cached = {
|
|
46
|
+
cli: cliVersion(),
|
|
47
|
+
...detectHarness(),
|
|
48
|
+
node: process.versions.node,
|
|
49
|
+
os: process.platform,
|
|
50
|
+
arch: process.arch,
|
|
51
|
+
ci: isCi() || undefined,
|
|
52
|
+
};
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
/** Headers to merge into every API request so the server can attribute the call. */
|
|
56
|
+
export function clientHeaders() {
|
|
57
|
+
const ctx = clientContext();
|
|
58
|
+
return {
|
|
59
|
+
'User-Agent': `gipity-cli/${ctx.cli} (${ctx.os}; ${ctx.arch}; harness=${ctx.harness})`,
|
|
60
|
+
'X-Gipity-Client': JSON.stringify(ctx),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=client-context.js.map
|
package/dist/commands/claude.js
CHANGED
|
@@ -4,19 +4,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdir
|
|
|
4
4
|
import { execSync, spawn } from 'child_process';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
|
|
8
|
-
function resolveCommand(cmd) {
|
|
9
|
-
if (process.platform !== 'win32')
|
|
10
|
-
return cmd;
|
|
11
|
-
try {
|
|
12
|
-
const lines = execSync(`where ${cmd}`, { encoding: 'utf-8' }).split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
13
|
-
// Prefer .exe (native) over .cmd (npm shim)
|
|
14
|
-
return lines.find(l => l.endsWith('.exe')) || lines.find(l => l.endsWith('.cmd')) || cmd;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return `${cmd}.cmd`;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
7
|
+
import { resolveCommand } from '../platform.js';
|
|
20
8
|
import { getAuth, saveAuth } from '../auth.js';
|
|
21
9
|
import { get, post, publicPost, ApiError, getAccountSlug } from '../api.js';
|
|
22
10
|
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, DEFAULT_API_BASE, getConfigPath } from '../config.js';
|
|
@@ -26,12 +14,35 @@ import { buildProjectContextBlock as buildProjectContextBlockText, buildNewProje
|
|
|
26
14
|
import * as relayState from '../relay/state.js';
|
|
27
15
|
import { maybeOfferRelayOn, ensureDaemonRunning } from '../relay/onboarding.js';
|
|
28
16
|
import { prompt, pickOne, decodeJwtExp, confirm } from '../utils.js';
|
|
29
|
-
import { brand, bold, info, success, error as clrError, muted } from '../colors.js';
|
|
17
|
+
import { brand, bold, info, success, warning, error as clrError, muted } from '../colors.js';
|
|
30
18
|
import { createProgressReporter } from '../progress.js';
|
|
31
19
|
import { printBanner } from '../banner.js';
|
|
32
20
|
import { scanForAdoption, isLikelyEmpty, canAdoptCwd, formatCwdLabel, formatBytes, adoptCurrentDir, ADOPT_THRESHOLDS, } from '../adopt-cwd.js';
|
|
33
21
|
const __clDir = dirname(fileURLToPath(import.meta.url));
|
|
34
22
|
const __clPkg = JSON.parse(readFileSync(resolve(__clDir, '../../package.json'), 'utf-8'));
|
|
23
|
+
/** Report a sync run to the user. Beyond the applied-changes line, this SURFACES
|
|
24
|
+
* sync.errors - a download that came back incomplete (truncated bulk tar, a file
|
|
25
|
+
* that couldn't be refetched) lands here, so we never print "ready" over a
|
|
26
|
+
* half-synced project as if nothing went wrong. Deletions are disarmed on an
|
|
27
|
+
* incomplete pull, so we can tell the user plainly that nothing was deleted. */
|
|
28
|
+
function reportSyncResult(result) {
|
|
29
|
+
if (result.aborted) {
|
|
30
|
+
console.log(` ${warning('Merge cancelled — this folder was NOT synced with the project.')}`);
|
|
31
|
+
console.log(` ${muted('For a clean copy, quit and open the project in an empty folder. To merge anyway, run `gipity sync`.')}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (result.applied > 0) {
|
|
35
|
+
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
36
|
+
}
|
|
37
|
+
if (result.errors.length) {
|
|
38
|
+
console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? '' : 's'} — your local copy may be incomplete:`)}`);
|
|
39
|
+
for (const e of result.errors.slice(0, 8))
|
|
40
|
+
console.log(` - ${e}`);
|
|
41
|
+
if (result.errors.length > 8)
|
|
42
|
+
console.log(` …and ${result.errors.length - 8} more.`);
|
|
43
|
+
console.log(` ${muted('Nothing was deleted. Re-run `gipity sync` to finish the pull.')}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
import { getProjectsRoot } from '../relay/paths.js';
|
|
36
47
|
/** Ask the server for recursive VFS counts. Server owns the file metadata
|
|
37
48
|
* (counts, bytes, paths), so a single aggregate query beats walking the
|
|
@@ -395,13 +406,11 @@ export const claudeCommand = new Command('claude')
|
|
|
395
406
|
});
|
|
396
407
|
console.log(`\n Using ${projectDir}`);
|
|
397
408
|
try {
|
|
398
|
-
const result = await sync({ interactive: !nonInteractive, progress: syncProgress });
|
|
399
|
-
|
|
400
|
-
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
401
|
-
}
|
|
409
|
+
const result = await sync({ interactive: !nonInteractive, confirmMerge: !nonInteractive, progress: syncProgress });
|
|
410
|
+
reportSyncResult(result);
|
|
402
411
|
}
|
|
403
|
-
catch {
|
|
404
|
-
console.log('
|
|
412
|
+
catch (err) {
|
|
413
|
+
console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
|
|
405
414
|
}
|
|
406
415
|
setupClaudeHooks();
|
|
407
416
|
setupClaudeMd();
|
|
@@ -445,13 +454,11 @@ export const claudeCommand = new Command('claude')
|
|
|
445
454
|
}
|
|
446
455
|
if (doSync) {
|
|
447
456
|
try {
|
|
448
|
-
const result = await sync({ interactive: !nonInteractive, progress: syncProgress });
|
|
449
|
-
|
|
450
|
-
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
451
|
-
}
|
|
457
|
+
const result = await sync({ interactive: !nonInteractive, confirmMerge: !nonInteractive, progress: syncProgress });
|
|
458
|
+
reportSyncResult(result);
|
|
452
459
|
}
|
|
453
|
-
catch {
|
|
454
|
-
console.log('
|
|
460
|
+
catch (err) {
|
|
461
|
+
console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
|
|
455
462
|
}
|
|
456
463
|
}
|
|
457
464
|
// Interactive: no seeded prompt - CLAUDE.md carries the context now.
|
|
@@ -570,13 +577,11 @@ export const claudeCommand = new Command('claude')
|
|
|
570
577
|
console.log(`\n Using ${projectDir}`);
|
|
571
578
|
// Unified sync - push and pull resolved via three-way merge (non-fatal)
|
|
572
579
|
try {
|
|
573
|
-
const result = await sync({ interactive: !nonInteractive, progress: syncProgress });
|
|
574
|
-
|
|
575
|
-
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
576
|
-
}
|
|
580
|
+
const result = await sync({ interactive: !nonInteractive, confirmMerge: !nonInteractive, progress: syncProgress });
|
|
581
|
+
reportSyncResult(result);
|
|
577
582
|
}
|
|
578
|
-
catch {
|
|
579
|
-
console.log('
|
|
583
|
+
catch (err) {
|
|
584
|
+
console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
|
|
580
585
|
}
|
|
581
586
|
}
|
|
582
587
|
// Interactive launch: no seeded first message (new or existing). The
|
package/dist/commands/credits.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { get } from '../api.js';
|
|
4
|
-
import { brand, muted } from '../colors.js';
|
|
4
|
+
import { brand, muted, success, error as clrError } from '../colors.js';
|
|
5
5
|
import { run, printList } from '../helpers/index.js';
|
|
6
6
|
const PRICING_URL = 'https://prompt.gipity.ai/pricing';
|
|
7
7
|
function openInBrowser(url) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// Windows `start` is a cmd.exe builtin, not an executable - spawn can't
|
|
9
|
+
// launch it directly, so go through `cmd /c start`. The empty "" is the
|
|
10
|
+
// window-title arg; without it a quoted URL would be swallowed as the title.
|
|
11
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]] :
|
|
12
|
+
process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] :
|
|
13
|
+
['xdg-open', [url]];
|
|
11
14
|
try {
|
|
12
|
-
spawn(cmd,
|
|
15
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
13
16
|
}
|
|
14
17
|
catch {
|
|
15
18
|
// Spawn failed (no xdg-open on minimal Linux, etc.) - caller prints the URL.
|
|
@@ -19,17 +22,23 @@ export const creditsCommand = new Command('credits')
|
|
|
19
22
|
.description('View credits')
|
|
20
23
|
.option('--json', 'Output as JSON')
|
|
21
24
|
.action((opts) => run('Credits', async () => {
|
|
22
|
-
const res = await get('/credits/balance');
|
|
23
25
|
if (opts.json) {
|
|
26
|
+
const res = await get('/credits/balance');
|
|
24
27
|
console.log(JSON.stringify(res.data));
|
|
28
|
+
return;
|
|
25
29
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const [balRes, subRes] = await Promise.all([
|
|
31
|
+
get('/credits/balance'),
|
|
32
|
+
get('/credits/subscription'),
|
|
33
|
+
]);
|
|
34
|
+
const sub = subRes.data;
|
|
35
|
+
const tierLabel = sub.tier === 'pro' ? 'Gipity Pro' : 'Free';
|
|
36
|
+
console.log(`Plan: ${brand(tierLabel)} ${muted(`(${sub.status})`)}`);
|
|
37
|
+
console.log(`Credits: ${brand(balRes.data.available.toLocaleString())}`);
|
|
38
|
+
if (balRes.data.balances.length > 0) {
|
|
39
|
+
for (const b of balRes.data.balances) {
|
|
40
|
+
const exp = new Date(b.expiresAt).toLocaleDateString();
|
|
41
|
+
console.log(`${b.source}: ${b.creditsRemaining.toLocaleString()} ${muted(`expires ${exp}`)}`);
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
}));
|
|
@@ -41,12 +50,42 @@ creditsCommand
|
|
|
41
50
|
openInBrowser(PRICING_URL);
|
|
42
51
|
console.log(muted("If your browser didn't open automatically, copy the URL above."));
|
|
43
52
|
}));
|
|
53
|
+
creditsCommand
|
|
54
|
+
.command('status')
|
|
55
|
+
.description('Show billing/Stripe configuration health (admin only)')
|
|
56
|
+
.option('--json', 'Output as JSON')
|
|
57
|
+
// optsWithGlobals(): the parent `credits` command also declares --json, which
|
|
58
|
+
// commander binds to the parent - so read merged opts to see the flag here.
|
|
59
|
+
.action((_opts, cmd) => run('Billing status', async () => {
|
|
60
|
+
const opts = cmd.optsWithGlobals();
|
|
61
|
+
const res = await get('/admin/billing-status');
|
|
62
|
+
const s = res.data;
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
console.log(JSON.stringify(s));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const mark = (ok) => (ok ? success('✓') : clrError('✗'));
|
|
68
|
+
console.log(`Stripe mode: ${brand(s.mode)}`);
|
|
69
|
+
console.log(`Secret key: ${mark(s.secretKeyConfigured)}`);
|
|
70
|
+
console.log(`Webhook secret: ${mark(s.webhookSecretConfigured)} ${muted(`endpoint ${s.webhookPath}`)}`);
|
|
71
|
+
console.log(`Credit packs: ${mark(s.packsConfigured)}${s.packsError ? ` ${clrError(s.packsError)}` : ''}`);
|
|
72
|
+
for (const p of s.packs) {
|
|
73
|
+
console.log(` ${p.name} ${muted(p.priceId)}${p.hidden ? muted(' [hidden]') : ''}`);
|
|
74
|
+
}
|
|
75
|
+
console.log(`Test pack ($1): ${mark(s.testPackConfigured)} ${muted('set STRIPE_PACK_TEST_PRICE_ID to enable a cheap purchase smoke test')}`);
|
|
76
|
+
console.log('Subscription plans:');
|
|
77
|
+
for (const pl of s.subscriptionPlans) {
|
|
78
|
+
console.log(` ${pl.name} (${pl.tier}) ${mark(pl.priceIdConfigured)}`);
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
44
81
|
creditsCommand
|
|
45
82
|
.command('usage')
|
|
46
83
|
.description('Show recent usage')
|
|
47
84
|
.option('--limit <n>', 'Number of entries', '20')
|
|
48
85
|
.option('--json', 'Output as JSON')
|
|
49
|
-
|
|
86
|
+
// optsWithGlobals(): parent `credits` also declares --json (see status above).
|
|
87
|
+
.action((_opts, cmd) => run('Usage', async () => {
|
|
88
|
+
const opts = cmd.optsWithGlobals();
|
|
50
89
|
const limit = parseInt(opts.limit, 10) || 20;
|
|
51
90
|
const res = await get(`/credits/usage?limit=${limit}`);
|
|
52
91
|
printList(res.data, opts, 'No usage history.', u => {
|
|
@@ -5,6 +5,7 @@ import { pushFile } from '../sync.js';
|
|
|
5
5
|
import { writeFileSync } from 'fs';
|
|
6
6
|
import { resolve as resolvePath, dirname, relative, isAbsolute } from 'path';
|
|
7
7
|
import { error as clrError, success, muted, info } from '../colors.js';
|
|
8
|
+
import { printCommandError } from '../helpers/command.js';
|
|
8
9
|
import { IMAGE_MODELS_DOC, IMAGE_GEMINI_ASPECT_RATIOS, IMAGE_GEMINI_SIZES, VIDEO_MODELS_DOC, TTS_PROVIDER_DESCRIPTIONS } from '../provider-docs.js';
|
|
9
10
|
/** Download a URL and save to a local file, then push it up to the project so
|
|
10
11
|
* the cloud (and anything that mirrors it) immediately matches local disk.
|
|
@@ -98,7 +99,7 @@ Examples:
|
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
catch (err) {
|
|
101
|
-
|
|
102
|
+
printCommandError('Image generation', err);
|
|
102
103
|
process.exit(1);
|
|
103
104
|
}
|
|
104
105
|
});
|
|
@@ -150,7 +151,7 @@ Examples:
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
catch (err) {
|
|
153
|
-
|
|
154
|
+
printCommandError('Video generation', err);
|
|
154
155
|
process.exit(1);
|
|
155
156
|
}
|
|
156
157
|
});
|
|
@@ -209,7 +210,7 @@ Examples:
|
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
catch (err) {
|
|
212
|
-
|
|
213
|
+
printCommandError('Speech generation', err);
|
|
213
214
|
process.exit(1);
|
|
214
215
|
}
|
|
215
216
|
});
|
|
@@ -259,7 +260,7 @@ Examples:
|
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
catch (err) {
|
|
262
|
-
|
|
263
|
+
printCommandError('Music generation', err);
|
|
263
264
|
process.exit(1);
|
|
264
265
|
}
|
|
265
266
|
});
|
package/dist/commands/sync.js
CHANGED
|
@@ -12,7 +12,11 @@ export const syncCommand = new Command('sync')
|
|
|
12
12
|
try {
|
|
13
13
|
// JSON mode stays machine-clean; otherwise show live progress on a TTY.
|
|
14
14
|
const progress = opts.json ? undefined : createProgressReporter();
|
|
15
|
-
|
|
15
|
+
// confirmMerge arms the uncertain-merge guard: a first-time sync into a
|
|
16
|
+
// folder that has its own files prompts on a TTY, proceeds with --yes, and
|
|
17
|
+
// fail-safe ABORTS when non-interactive without --yes - so a script never
|
|
18
|
+
// merges an unexpected folder into the project silently.
|
|
19
|
+
const result = await sync({ plan: opts.plan, force: opts.force, prune: opts.prune, confirmMerge: true, progress });
|
|
16
20
|
if (opts.json) {
|
|
17
21
|
console.log(JSON.stringify(result));
|
|
18
22
|
}
|
|
@@ -33,7 +37,9 @@ export const syncCommand = new Command('sync')
|
|
|
33
37
|
for (const e of result.errors)
|
|
34
38
|
console.error(clrError(e));
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
// A refused merge or any sync error is a non-zero exit so scripts/CI can
|
|
41
|
+
// detect that nothing was applied.
|
|
42
|
+
if (result.aborted || result.errors.length > 0)
|
|
37
43
|
process.exit(1);
|
|
38
44
|
}
|
|
39
45
|
catch (err) {
|
package/dist/helpers/command.js
CHANGED
|
@@ -10,9 +10,24 @@ import { error as clrError } from '../colors.js';
|
|
|
10
10
|
* Usage:
|
|
11
11
|
* .action((name, opts) => run('Create', async () => { ... }))
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Print a command error. Out-of-credits is a recoverable, user-actionable
|
|
15
|
+
* state, so flag it plainly (and keep the server's message, which carries the
|
|
16
|
+
* buy link) - that way an agent reading this output, e.g. Claude Code, can tell
|
|
17
|
+
* the user they're out of Gipity credits and where to top up rather than
|
|
18
|
+
* treating it as a hard failure. Does not exit; the caller decides.
|
|
19
|
+
*/
|
|
20
|
+
export function printCommandError(label, err) {
|
|
21
|
+
if (err?.code === 'INSUFFICIENT_CREDITS') {
|
|
22
|
+
console.error(clrError(`${label} failed - out of Gipity credits.`));
|
|
23
|
+
console.error(err.message);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.error(clrError(`${label} failed: ${err.message}`));
|
|
27
|
+
}
|
|
13
28
|
export function run(label, action) {
|
|
14
29
|
action().catch((err) => {
|
|
15
|
-
|
|
30
|
+
printCommandError(label, err);
|
|
16
31
|
process.exit(1);
|
|
17
32
|
});
|
|
18
33
|
}
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a command to something Node's spawn/spawnSync can launch directly.
|
|
4
|
+
*
|
|
5
|
+
* On Windows, npm-installed CLIs (npm, npx, gipity, claude) are `.cmd` batch
|
|
6
|
+
* shims, not `.exe`s. Node's spawn without `shell: true` cannot launch a bare
|
|
7
|
+
* `npm` - it fails with ENOENT and a null exit status. Resolving the real path
|
|
8
|
+
* (preferring `.exe`, falling back to `.cmd`) lets us spawn without `shell: true`
|
|
9
|
+
* (which would otherwise require escaping arguments).
|
|
10
|
+
*/
|
|
11
|
+
export function resolveCommand(cmd) {
|
|
12
|
+
if (process.platform !== 'win32')
|
|
13
|
+
return cmd;
|
|
14
|
+
try {
|
|
15
|
+
const lines = execSync(`where ${cmd}`, { encoding: 'utf-8' })
|
|
16
|
+
.split(/\r?\n/)
|
|
17
|
+
.map(l => l.trim())
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
// Prefer .exe (native) over .cmd (npm shim)
|
|
20
|
+
return lines.find(l => l.endsWith('.exe')) || lines.find(l => l.endsWith('.cmd')) || cmd;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return `${cmd}.cmd`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=platform.js.map
|
package/dist/relay/daemon.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* See docs/feature-backlog/gipity-relay-phases.md (Phase A Step 7).
|
|
22
22
|
*/
|
|
23
23
|
import { spawn } from 'child_process';
|
|
24
|
+
import { resolveCommand } from '../platform.js';
|
|
24
25
|
import { appendFileSync, mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync, closeSync, openSync, unlinkSync } from 'fs';
|
|
25
26
|
import { stat, readFile } from 'fs/promises';
|
|
26
27
|
import { createInterface } from 'readline';
|
|
@@ -855,7 +856,10 @@ export async function killRunningForConv(convGuid) {
|
|
|
855
856
|
* resolution (the daemon itself doesn't chdir into projects).
|
|
856
857
|
* Non-blocking on failure - caller catches and logs. */
|
|
857
858
|
async function spawnSync(cwd, timeoutMs) {
|
|
858
|
-
|
|
859
|
+
// resolveCommand: on Windows the bare `gipity` is a .cmd shim that spawn
|
|
860
|
+
// can't launch without an explicit path. An explicit env override is used
|
|
861
|
+
// verbatim (it may be a full path); only the default name is resolved.
|
|
862
|
+
const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand('gipity');
|
|
859
863
|
return new Promise((resolve, reject) => {
|
|
860
864
|
const child = spawn(cmd, ['sync', '--json'], {
|
|
861
865
|
cwd,
|
|
@@ -901,7 +905,10 @@ async function spawnSync(cwd, timeoutMs) {
|
|
|
901
905
|
});
|
|
902
906
|
}
|
|
903
907
|
export async function spawnGipityClaude(args, cwd, d) {
|
|
904
|
-
|
|
908
|
+
// resolveCommand: on Windows the bare `gipity` is a .cmd shim that spawn
|
|
909
|
+
// can't launch without an explicit path. An explicit env override is used
|
|
910
|
+
// verbatim (it may be a full path); only the default name is resolved.
|
|
911
|
+
const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand('gipity');
|
|
905
912
|
// Inject stream-json flags here rather than at the call site so every
|
|
906
913
|
// relay spawn path gets the same protocol. `--verbose` is required by
|
|
907
914
|
// Claude Code when combining `-p` with `--output-format stream-json`.
|
package/dist/relay/onboarding.js
CHANGED
|
@@ -30,10 +30,13 @@ export function ensureDaemonRunning() {
|
|
|
30
30
|
if (state.isDaemonRunning())
|
|
31
31
|
return;
|
|
32
32
|
try {
|
|
33
|
-
|
|
33
|
+
// Launch via the current Node binary + the CLI entry script. The previous
|
|
34
|
+
// `shell: true` on Windows ran the .js path through the shell, where the
|
|
35
|
+
// file association (Windows Script Host, not Node) would mis-handle it.
|
|
36
|
+
// process.execPath is cross-platform and needs no shell.
|
|
37
|
+
const child = spawn(process.execPath, [resolveCliPath(), 'relay', 'run'], {
|
|
34
38
|
detached: true,
|
|
35
39
|
stdio: 'ignore',
|
|
36
|
-
shell: process.platform === 'win32',
|
|
37
40
|
});
|
|
38
41
|
child.unref();
|
|
39
42
|
}
|
package/dist/setup.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolve, join, dirname } from 'path';
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
|
+
import { resolveCommand } from './platform.js';
|
|
8
9
|
import { SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE } from './knowledge.js';
|
|
9
10
|
export { SKILLS_CONTENT };
|
|
10
11
|
/** Canonical list of workstation artifacts that are NOT part of the project.
|
|
@@ -268,11 +269,15 @@ export function ensureGipityPluginInstalled() {
|
|
|
268
269
|
// Refresh the marketplace clone so `install` resolves the current version,
|
|
269
270
|
// then (re)install at user scope - idempotent, and upgrades an older or
|
|
270
271
|
// project-scoped install to the current one at user scope.
|
|
271
|
-
|
|
272
|
+
// resolveCommand: on Windows `claude` is a .cmd shim that spawn can't launch
|
|
273
|
+
// without an explicit path, so resolve it (otherwise the install silently
|
|
274
|
+
// ENOENTs and the plugin's hooks never land at user scope).
|
|
275
|
+
const claudeCmd = resolveCommand('claude');
|
|
276
|
+
spawnSync(claudeCmd, ['plugin', 'marketplace', 'update', GIPITY_MARKETPLACE_NAME], {
|
|
272
277
|
stdio: 'ignore',
|
|
273
278
|
timeout: 120_000,
|
|
274
279
|
});
|
|
275
|
-
spawnSync(
|
|
280
|
+
spawnSync(claudeCmd, ['plugin', 'install', GIPITY_PLUGIN_ID, '--scope', 'user'], {
|
|
276
281
|
stdio: 'ignore',
|
|
277
282
|
timeout: 120_000,
|
|
278
283
|
});
|
|
@@ -478,7 +483,10 @@ export function setupGitignore() {
|
|
|
478
483
|
const entries = ['.gipity/', '.gipity.json'];
|
|
479
484
|
if (existsSync(gitignorePath)) {
|
|
480
485
|
let content = readFileSync(gitignorePath, 'utf-8');
|
|
481
|
-
|
|
486
|
+
// Split on \r?\n so a CRLF .gitignore (the Windows default) doesn't leave a
|
|
487
|
+
// trailing \r on each entry - otherwise `lines.includes('.gipity/')` never
|
|
488
|
+
// matches '.gipity/\r' and every run re-appends the entries as duplicates.
|
|
489
|
+
const lines = content.split(/\r?\n/);
|
|
482
490
|
const toAdd = entries.filter(e => !lines.includes(e));
|
|
483
491
|
if (toAdd.length > 0) {
|
|
484
492
|
content = content.trimEnd() + '\n' + toAdd.join('\n') + '\n';
|
package/dist/sync.js
CHANGED
|
@@ -235,6 +235,11 @@ async function downloadAll(projectGuid, onBytes) {
|
|
|
235
235
|
});
|
|
236
236
|
extract.on('finish', () => resolve(files));
|
|
237
237
|
extract.on('error', reject);
|
|
238
|
+
// pipe() does NOT forward source-stream errors to the destination, so a
|
|
239
|
+
// truncated/aborted HTTP body would otherwise surface as a clean tar
|
|
240
|
+
// 'finish' with a partial file set. Reject explicitly so a short download is
|
|
241
|
+
// an error the caller can recover from, never a silent partial.
|
|
242
|
+
stream.on('error', reject);
|
|
238
243
|
stream.pipe(extract);
|
|
239
244
|
});
|
|
240
245
|
}
|
|
@@ -579,6 +584,60 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
579
584
|
deferredDeletes: 0,
|
|
580
585
|
};
|
|
581
586
|
}
|
|
587
|
+
// Uncertain-merge guard (armed via confirmMerge): syncing INTO a populated
|
|
588
|
+
// directory we've never synced for this project (empty baseline + local files
|
|
589
|
+
// that would upload or collide). Local-only files get pushed UP into the
|
|
590
|
+
// project and same-path differences fork into conflict copies - a two-way
|
|
591
|
+
// merge that may be unintended. Nothing here can delete.
|
|
592
|
+
//
|
|
593
|
+
// Three ways to resolve it, so both outcomes are scriptable:
|
|
594
|
+
// --yes / autoConfirm → proceed (merge confirmed)
|
|
595
|
+
// interactive TTY → prompt the user
|
|
596
|
+
// non-interactive → fail safe: ABORT rather than merge blindly
|
|
597
|
+
if (opts.confirmMerge &&
|
|
598
|
+
remote.size > 0 && // project already has files - a real merge target
|
|
599
|
+
Object.keys(baseline.files).length === 0 && // we've never synced this folder for it
|
|
600
|
+
(planned.uploads > 0 || planned.conflicts > 0) // and local content would push up or collide
|
|
601
|
+
) {
|
|
602
|
+
const f = (n) => `${n} file${n === 1 ? '' : 's'}`;
|
|
603
|
+
const shape = [` Server: ${f(remote.size)} · Local: ${f(local.size)}`];
|
|
604
|
+
if (planned.downloads > 0)
|
|
605
|
+
shape.push(` ↓ download ${f(planned.downloads)} from the project into this folder`);
|
|
606
|
+
if (planned.uploads > 0)
|
|
607
|
+
shape.push(` ↑ upload ${f(planned.uploads)} from this folder INTO the project (they become part of it)`);
|
|
608
|
+
if (planned.conflicts > 0)
|
|
609
|
+
shape.push(` ! ${f(planned.conflicts)} differ on both sides — both kept (your copy is renamed)`);
|
|
610
|
+
const abort = () => ({
|
|
611
|
+
plan: planned, applied: 0, skipped: planned.actions.length, errors: [],
|
|
612
|
+
summary: [
|
|
613
|
+
`This folder has files that haven't been synced with this project yet — merge not confirmed.`,
|
|
614
|
+
...shape,
|
|
615
|
+
`Re-run with --yes to merge, or sync into an empty folder.`,
|
|
616
|
+
].join('\n'),
|
|
617
|
+
deferredDeletes: 0, aborted: true,
|
|
618
|
+
});
|
|
619
|
+
if (getAutoConfirm()) {
|
|
620
|
+
// --yes: proceed with the merge; the caller reports the applied counts.
|
|
621
|
+
}
|
|
622
|
+
else if (interactive) {
|
|
623
|
+
const answer = await prompt([
|
|
624
|
+
'',
|
|
625
|
+
` This folder has files that haven't been synced with this project yet.`,
|
|
626
|
+
` Syncing here MERGES the two — nothing is deleted:`,
|
|
627
|
+
'',
|
|
628
|
+
...shape,
|
|
629
|
+
'',
|
|
630
|
+
` Continue? [y/N]: `,
|
|
631
|
+
].join('\n'));
|
|
632
|
+
if (!/^(y|yes|continue)$/i.test(answer.trim()))
|
|
633
|
+
return abort();
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
// Non-interactive and not confirmed: don't silently merge a folder we
|
|
637
|
+
// weren't told to. `--yes` opts in.
|
|
638
|
+
return abort();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
582
641
|
// Bulk-delete guard over the *planned* deletes.
|
|
583
642
|
const knownFiles = local.size + remote.size;
|
|
584
643
|
const deletesOk = await bulkDeleteGuard(planned, knownFiles, { ...opts, interactive });
|
|
@@ -590,8 +649,14 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
590
649
|
// ── Pre-fetch remote bytes once for all downloads (conflict-originating
|
|
591
650
|
// remote versions are fetched on demand after 409). ──
|
|
592
651
|
const downloadedBytes = new Map();
|
|
593
|
-
|
|
594
|
-
|
|
652
|
+
// Set when the download phase could not retrieve every byte the plan needs.
|
|
653
|
+
// A delete is only safe against a complete, authoritative view, so an
|
|
654
|
+
// incomplete download disarms the deletes pass below - this is what breaks
|
|
655
|
+
// the "truncated pull → files missing locally → next sync deletes them"
|
|
656
|
+
// amplification loop.
|
|
657
|
+
let downloadIncomplete = false;
|
|
658
|
+
const wantedDownloads = plannedToApply.filter(a => a.kind === 'download' || a.kind === 'conflict');
|
|
659
|
+
if (wantedDownloads.length) {
|
|
595
660
|
// The tree endpoint streams the *whole* remote tree as one tar (the caller
|
|
596
661
|
// then picks out only the paths it planned to apply), so the bytes that
|
|
597
662
|
// actually move = the sum of every remote file's size. That's the honest
|
|
@@ -609,22 +674,46 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
609
674
|
: undefined;
|
|
610
675
|
try {
|
|
611
676
|
const all = await downloadAll(config.projectGuid, onBytes);
|
|
612
|
-
for (const a of
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
downloadedBytes.set(a.path, buf);
|
|
617
|
-
}
|
|
677
|
+
for (const a of wantedDownloads) {
|
|
678
|
+
const buf = all.get(a.path);
|
|
679
|
+
if (buf)
|
|
680
|
+
downloadedBytes.set(a.path, buf);
|
|
618
681
|
}
|
|
619
682
|
}
|
|
620
683
|
catch (err) {
|
|
621
|
-
|
|
684
|
+
// The bulk tar can truncate mid-stream on a large project (transport or
|
|
685
|
+
// proxy timeout) and either reject here or "finish" with a partial set.
|
|
686
|
+
// Either way we fall through to the single-file recovery below rather than
|
|
687
|
+
// proceeding on a half-empty tree - a partial download must never be
|
|
688
|
+
// mistaken for a complete one.
|
|
689
|
+
errors.push(`Bulk download incomplete (${err.message}); recovering files individually…`);
|
|
622
690
|
}
|
|
623
691
|
finally {
|
|
624
692
|
// Settle the bar even if the extracted-byte tally fell short of the
|
|
625
693
|
// estimate (the live line stays open until something hits 100% or finish()).
|
|
626
694
|
p?.finish();
|
|
627
695
|
}
|
|
696
|
+
// Recover whatever the bulk tar dropped over the reliable single-file
|
|
697
|
+
// endpoint. This is what lets a project whose tar keeps truncating still
|
|
698
|
+
// sync to completion - and what recovers a checkout left half-downloaded by
|
|
699
|
+
// an earlier truncated pull.
|
|
700
|
+
const missing = wantedDownloads.filter(a => !downloadedBytes.has(a.path));
|
|
701
|
+
if (missing.length) {
|
|
702
|
+
p?.phase(`Recovering ${missing.length} file${missing.length === 1 ? '' : 's'} the bulk download dropped…`);
|
|
703
|
+
for (const a of missing) {
|
|
704
|
+
let buf = null;
|
|
705
|
+
for (let attempt = 0; attempt < 3 && !buf; attempt++) {
|
|
706
|
+
buf = await fetchOne(config.projectGuid, a.path);
|
|
707
|
+
}
|
|
708
|
+
if (buf)
|
|
709
|
+
downloadedBytes.set(a.path, buf);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Anything still missing is a hard failure: the plan needs bytes we could
|
|
713
|
+
// not retrieve. Mark the download incomplete so the deletes pass is skipped;
|
|
714
|
+
// the per-path "Download missing" errors below carry the detail.
|
|
715
|
+
if (wantedDownloads.some(a => !downloadedBytes.has(a.path)))
|
|
716
|
+
downloadIncomplete = true;
|
|
628
717
|
}
|
|
629
718
|
// ── Writes pass: uploads, downloads, conflicts (rename + download + upload copy) ──
|
|
630
719
|
// We serialize conflicts; uploads run with bounded concurrency.
|
|
@@ -911,7 +1000,17 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
911
1000
|
}
|
|
912
1001
|
}
|
|
913
1002
|
// ── Deletes pass ──
|
|
1003
|
+
// A delete is only safe against a complete, authoritative view. If the
|
|
1004
|
+
// download phase couldn't retrieve everything it planned, the local tree is
|
|
1005
|
+
// not a trustworthy deletion signal - this is exactly how a truncated pull
|
|
1006
|
+
// turns "files we failed to fetch" into "delete those files." Skip ALL deletes
|
|
1007
|
+
// this run and let a clean sync replan them once the pull succeeds.
|
|
1008
|
+
let deletesSkippedIncomplete = 0;
|
|
914
1009
|
for (const a of plannedToApply) {
|
|
1010
|
+
if (downloadIncomplete && (a.kind === 'delete-local' || a.kind === 'delete-remote')) {
|
|
1011
|
+
deletesSkippedIncomplete++;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
915
1014
|
if (a.kind === 'delete-local') {
|
|
916
1015
|
try {
|
|
917
1016
|
unlinkSync(resolveInRoot(root, a.path));
|
|
@@ -977,6 +1076,10 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
977
1076
|
}
|
|
978
1077
|
}
|
|
979
1078
|
}
|
|
1079
|
+
if (deletesSkippedIncomplete > 0) {
|
|
1080
|
+
errors.push(`Skipped ${deletesSkippedIncomplete} deletion${deletesSkippedIncomplete === 1 ? '' : 's'} because the download was incomplete - ` +
|
|
1081
|
+
`nothing was deleted. Re-run \`gipity sync\` once the pull finishes to apply any real deletions.`);
|
|
1082
|
+
}
|
|
980
1083
|
// Clean up empty local directories after delete-local actions.
|
|
981
1084
|
cleanupEmptyDirs(root, config.ignore);
|
|
982
1085
|
baseline.lastFullSync = new Date().toISOString();
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
|
2
2
|
import { spawnSync } from 'child_process';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { LOCAL_DIR, LOCAL_ENTRY, LOCAL_PKG_DIR, writeState, readState } from './state.js';
|
|
5
|
+
import { resolveCommand } from '../platform.js';
|
|
5
6
|
export function isBootstrapped() {
|
|
6
7
|
return existsSync(LOCAL_ENTRY);
|
|
7
8
|
}
|
|
@@ -25,7 +26,7 @@ export function bootstrap(version, quiet = false) {
|
|
|
25
26
|
// --ignore-scripts: don't run install lifecycle hooks (gipity ships
|
|
26
27
|
// precompiled, deps need no build), so a compromised package can't execute
|
|
27
28
|
// code during this self-managed install.
|
|
28
|
-
const res = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--ignore-scripts', `gipity@${version}`], {
|
|
29
|
+
const res = spawnSync(resolveCommand('npm'), ['install', '--no-audit', '--no-fund', '--ignore-scripts', `gipity@${version}`], {
|
|
29
30
|
cwd: LOCAL_DIR,
|
|
30
31
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
32
|
encoding: 'utf-8',
|
|
@@ -38,7 +39,10 @@ export function bootstrap(version, quiet = false) {
|
|
|
38
39
|
process.stderr.write(`gipity v${version} is not yet published to npm - using the currently installed build.\n`);
|
|
39
40
|
}
|
|
40
41
|
else {
|
|
41
|
-
|
|
42
|
+
// res.error (e.g. ENOENT when npm can't be launched) carries the real
|
|
43
|
+
// cause; res.status is null in that case, so prefer the error message.
|
|
44
|
+
const firstLine = stderr.split('\n').map(l => l.trim()).find(l => l.length > 0)
|
|
45
|
+
|| (res.error ? res.error.message : `npm exit ${res.status}`);
|
|
42
46
|
const reason = firstLine.length > 160 ? firstLine.slice(0, 157) + '...' : firstLine;
|
|
43
47
|
process.stderr.write(`gipity: could not set up local install (${reason}). Using the currently installed build.\n`);
|
|
44
48
|
}
|
package/dist/updater/check.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { spawnSync } from 'child_process';
|
|
5
5
|
import { appendFileSync, existsSync } from 'fs';
|
|
6
6
|
import { LOCAL_DIR, LOCAL_ENTRY, UPDATE_LOG, readState, writeState, updatesDisabled } from './state.js';
|
|
7
|
+
import { resolveCommand } from '../platform.js';
|
|
7
8
|
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
8
9
|
function log(line) {
|
|
9
10
|
try {
|
|
@@ -37,10 +38,12 @@ function installVersion(version) {
|
|
|
37
38
|
// --ignore-scripts: this runs unattended in the background, so don't let a
|
|
38
39
|
// compromised package's install lifecycle hooks execute. gipity ships
|
|
39
40
|
// precompiled (dist/) and its deps need no build step, so nothing is lost.
|
|
40
|
-
const res = spawnSync('npm', ['install', '--silent', '--no-audit', '--no-fund', '--ignore-scripts', `gipity@${version}`], {
|
|
41
|
+
const res = spawnSync(resolveCommand('npm'), ['install', '--silent', '--no-audit', '--no-fund', '--ignore-scripts', `gipity@${version}`], {
|
|
41
42
|
cwd: LOCAL_DIR,
|
|
42
43
|
stdio: 'ignore',
|
|
43
44
|
});
|
|
45
|
+
if (res.error)
|
|
46
|
+
log(`npm spawn failed: ${res.error.message}`);
|
|
44
47
|
return res.status === 0 && existsSync(LOCAL_ENTRY);
|
|
45
48
|
}
|
|
46
49
|
export async function runCheck(opts = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.398",
|
|
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",
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
"build": "tsc && chmod +x dist/index.js dist/gipcc.js dist/gipccd.js dist/updater/shim.js dist/updater/check.js",
|
|
13
13
|
"dev": "tsc --watch",
|
|
14
14
|
"test": "npm run test:smoke",
|
|
15
|
-
"test:smoke": "tsc && node --test dist/__tests__/utils.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-lock.test.js dist/__tests__/auth-lock.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__/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__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/flag-aliases.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-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-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-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",
|
|
16
|
-
"test:e2e": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-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",
|
|
15
|
+
"test:smoke": "tsc && node --test dist/__tests__/utils.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-lock.test.js dist/__tests__/auth-lock.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__/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__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.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-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-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-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",
|
|
16
|
+
"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-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",
|
|
17
|
+
"test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",
|
|
17
18
|
"test:e2e:sandbox": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sandbox-live.test.js"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|