gipity 1.0.386 → 1.0.387
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/api.js +5 -2
- package/dist/auth.js +12 -4
- package/dist/commands/add.js +2 -1
- package/dist/commands/claude.js +4 -4
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/page-eval.js +65 -22
- package/dist/commands/records.js +42 -8
- package/dist/commands/remove.js +42 -0
- package/dist/commands/test.js +4 -1
- package/dist/config.js +46 -2
- package/dist/helpers/sync.js +4 -2
- package/dist/index.js +2 -1
- package/dist/page-fixtures.js +41 -0
- package/dist/project-setup.js +2 -2
- package/dist/relay/daemon.js +2 -2
- package/dist/relay/device-http.js +2 -2
- package/dist/sync.js +96 -15
- package/package.json +2 -2
package/dist/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Readable } from 'stream';
|
|
2
2
|
import * as tar from 'tar-stream';
|
|
3
3
|
import { getAuth, refreshTokenIfNeeded } from './auth.js';
|
|
4
|
-
import {
|
|
4
|
+
import { resolveApiBase, requireConfig, saveConfig } from './config.js';
|
|
5
5
|
export class ApiError extends Error {
|
|
6
6
|
statusCode;
|
|
7
7
|
code;
|
|
@@ -27,7 +27,7 @@ async function getHeaders() {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
function baseUrl() {
|
|
30
|
-
return
|
|
30
|
+
return resolveApiBase();
|
|
31
31
|
}
|
|
32
32
|
/** Exposed so streaming consumers (SSE) can build URLs without re-implementing
|
|
33
33
|
* the override / config resolution. */
|
|
@@ -101,6 +101,9 @@ export async function postForTarEntries(path, body) {
|
|
|
101
101
|
export function put(path, body) {
|
|
102
102
|
return request('PUT', path, body);
|
|
103
103
|
}
|
|
104
|
+
export function patch(path, body) {
|
|
105
|
+
return request('PATCH', path, body);
|
|
106
|
+
}
|
|
104
107
|
export function del(path, body) {
|
|
105
108
|
return request('DELETE', path, body);
|
|
106
109
|
}
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, chmodSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { decodeJwtExp } from './utils.js';
|
|
@@ -33,8 +33,16 @@ export function readAuthFresh() {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
export function saveAuth(data) {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Lock down to owner-only: this file holds the account access + 7-day refresh
|
|
37
|
+
// tokens, so a default 0644/0755 would let any other local user read them.
|
|
38
|
+
// (The relay state file already does this; auth.json is the more sensitive of
|
|
39
|
+
// the two.) chmod after write to also tighten any pre-existing loose file.
|
|
40
|
+
mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
41
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
42
|
+
try {
|
|
43
|
+
chmodSync(AUTH_FILE, 0o600);
|
|
44
|
+
}
|
|
45
|
+
catch { /* best-effort on platforms without chmod */ }
|
|
38
46
|
cached = data;
|
|
39
47
|
}
|
|
40
48
|
export function clearAuth() {
|
|
@@ -73,7 +81,7 @@ export async function refreshTokenIfNeeded() {
|
|
|
73
81
|
return; // not logged in, caller will handle
|
|
74
82
|
try {
|
|
75
83
|
const config = await import('./config.js');
|
|
76
|
-
const apiBase = config.
|
|
84
|
+
const apiBase = config.resolveApiBase();
|
|
77
85
|
const res = await fetch(`${apiBase}/auth/refresh`, {
|
|
78
86
|
method: 'POST',
|
|
79
87
|
headers: { 'Content-Type': 'application/json' },
|
package/dist/commands/add.js
CHANGED
|
@@ -179,7 +179,8 @@ export const addCommand = new Command('add')
|
|
|
179
179
|
}
|
|
180
180
|
const { name: labelName, files } = buildLocalPayload(resolved);
|
|
181
181
|
const kind = sniffPayloadKind(files);
|
|
182
|
-
|
|
182
|
+
// Progress goes to stderr so `gipity add … --json` keeps stdout pure JSON.
|
|
183
|
+
console.error(muted(`Uploading ${files.length} file(s) from ${resolved} (${kind}) ...`));
|
|
183
184
|
body = {
|
|
184
185
|
name: labelName,
|
|
185
186
|
title: opts.title,
|
package/dist/commands/claude.js
CHANGED
|
@@ -19,7 +19,7 @@ function resolveCommand(cmd) {
|
|
|
19
19
|
}
|
|
20
20
|
import { getAuth, saveAuth, clearAuth } from '../auth.js';
|
|
21
21
|
import { get, post, publicPost, ApiError, getAccountSlug } from '../api.js';
|
|
22
|
-
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, getConfigPath } from '../config.js';
|
|
22
|
+
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, DEFAULT_API_BASE, getConfigPath } from '../config.js';
|
|
23
23
|
import { sync } from '../sync.js';
|
|
24
24
|
import { slugify, setupClaudeHooks, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE, isSyncIgnored } from '../setup.js';
|
|
25
25
|
import { buildProjectContextBlock as buildProjectContextBlockText, buildNewProjectPrompt, buildResumeWrap, buildFreshWrap, } from '../prompts.js';
|
|
@@ -384,7 +384,7 @@ export const claudeCommand = new Command('claude')
|
|
|
384
384
|
accountSlug,
|
|
385
385
|
agentGuid,
|
|
386
386
|
conversationGuid: null,
|
|
387
|
-
apiBase: getApiBaseOverride() ||
|
|
387
|
+
apiBase: getApiBaseOverride() || DEFAULT_API_BASE,
|
|
388
388
|
ignore: DEFAULT_SYNC_IGNORE,
|
|
389
389
|
});
|
|
390
390
|
console.log(`\n Using ${projectDir}`);
|
|
@@ -470,7 +470,7 @@ export const claudeCommand = new Command('claude')
|
|
|
470
470
|
err?.code === 'ETIMEDOUT' || err?.cause?.code === 'ECONNREFUSED' ||
|
|
471
471
|
err?.cause?.code === 'ENOTFOUND' || err?.cause?.code === 'ETIMEDOUT';
|
|
472
472
|
if (isConnectionError) {
|
|
473
|
-
const apiBase = getApiBaseOverride() ||
|
|
473
|
+
const apiBase = getApiBaseOverride() || DEFAULT_API_BASE;
|
|
474
474
|
console.error(` ${clrError(`Could not connect to ${apiBase}`)}`);
|
|
475
475
|
console.error(` ${muted('Check your connection and try again.')}`);
|
|
476
476
|
process.exit(1);
|
|
@@ -554,7 +554,7 @@ export const claudeCommand = new Command('claude')
|
|
|
554
554
|
accountSlug,
|
|
555
555
|
agentGuid,
|
|
556
556
|
conversationGuid: null,
|
|
557
|
-
apiBase: getApiBaseOverride() ||
|
|
557
|
+
apiBase: getApiBaseOverride() || DEFAULT_API_BASE,
|
|
558
558
|
ignore: DEFAULT_SYNC_IGNORE,
|
|
559
559
|
});
|
|
560
560
|
console.log(`\n Using ${projectDir}`);
|
package/dist/commands/deploy.js
CHANGED
|
@@ -20,7 +20,7 @@ export const deployCommand = new Command('deploy')
|
|
|
20
20
|
.argument('[target]', 'dev or prod', 'dev')
|
|
21
21
|
.option('--source-dir <dir>', 'Source directory to deploy from')
|
|
22
22
|
.option('--only <phases>', 'Run only specific phases (comma-separated)')
|
|
23
|
-
.option('--force', 'Re-run all phases
|
|
23
|
+
.option('--force', 'Re-run all phases (ignore checksums) and bypass the sync bulk-deletion guard')
|
|
24
24
|
.option('--no-sync', 'Skip sync-up before deploy')
|
|
25
25
|
.option('--optimize', 'Run build optimization')
|
|
26
26
|
.option('--json', 'Output as JSON')
|
|
@@ -3,6 +3,8 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { post, get, ApiError } from '../api.js';
|
|
4
4
|
import { brand, bold, muted, warning } from '../colors.js';
|
|
5
5
|
import { run } from '../helpers/index.js';
|
|
6
|
+
import { resolveProjectContext } from '../config.js';
|
|
7
|
+
import { uploadPublicFixture, deleteFixture } from '../page-fixtures.js';
|
|
6
8
|
// Shown when an eval runs cleanly but returns nothing serializable. Turns a
|
|
7
9
|
// bare/opaque `null` into a deterministic, actionable nudge so the agent shapes
|
|
8
10
|
// a returnable value instead of guessing and retrying.
|
|
@@ -135,6 +137,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
135
137
|
.argument('<url>', 'URL to load')
|
|
136
138
|
.argument('[expr]', 'JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file.')
|
|
137
139
|
.option('--file <path>', 'Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work.')
|
|
140
|
+
.option('--fixture <path>', 'Host a local file and expose it to the eval as `fixtureUrl` (and under `fixtures` by basename) to fetch in-page. For verifying a render/parse path against a real binary (an MP3, an image) - no size limit, auto-deleted after the run. Repeat for several files (single-value so it never swallows the inline <expr>).', (val, prev) => [...prev, val], [])
|
|
138
141
|
.option('--wait <ms>', 'Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000)', '500')
|
|
139
142
|
.option('--wait-for <selector>', 'Wait until this CSS selector appears before evaluating (deterministic; replaces --wait)')
|
|
140
143
|
.option('--wait-timeout <ms>', 'Max ms to wait for --wait-for before giving up', '5000')
|
|
@@ -161,30 +164,66 @@ export const pageEvalCommand = new Command('eval')
|
|
|
161
164
|
const waitMs = capWaitMs(opts.wait, url);
|
|
162
165
|
const parsedTimeout = parseInt(opts.waitTimeout, 10);
|
|
163
166
|
const waitForTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout >= 0 ? parsedTimeout : 5000;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
// --fixture: host each file publicly, then splice `fixtures` / `fixtureUrl`
|
|
168
|
+
// into the eval scope so the page can fetch the bytes. The prelude makes the
|
|
169
|
+
// body a statement (const/return), so the server's expression form fails to
|
|
170
|
+
// parse and it falls back to the function-body form - which runs both inline
|
|
171
|
+
// exprs (wrapped in `return (...)`) and --file scripts. Cleanup in `finally`.
|
|
172
|
+
const fixturePaths = opts.fixture ?? [];
|
|
173
|
+
const hosted = [];
|
|
174
|
+
let projectGuid;
|
|
175
|
+
let sentExpr = expr;
|
|
176
|
+
try {
|
|
177
|
+
if (fixturePaths.length) {
|
|
178
|
+
const { config } = await resolveProjectContext({});
|
|
179
|
+
projectGuid = config.projectGuid;
|
|
180
|
+
for (const p of fixturePaths) {
|
|
181
|
+
console.log(muted(`Hosting fixture ${p}…`));
|
|
182
|
+
hosted.push(await uploadPublicFixture(projectGuid, p));
|
|
183
|
+
}
|
|
184
|
+
const map = {};
|
|
185
|
+
for (const h of hosted)
|
|
186
|
+
map[h.name] = h.url;
|
|
187
|
+
const prelude = `const fixtures=${JSON.stringify(map)};const fixtureUrl=${JSON.stringify(hosted[0].url)};`;
|
|
188
|
+
sentExpr = opts.file ? `${prelude}\n${expr}` : `${prelude}\nreturn (${expr});`;
|
|
189
|
+
}
|
|
190
|
+
const kickoff = await post('/tools/browser/eval', {
|
|
191
|
+
url, expr: sentExpr, waitMs,
|
|
192
|
+
waitForSelector: opts.waitFor || undefined,
|
|
193
|
+
waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : undefined,
|
|
194
|
+
});
|
|
195
|
+
const d = await pollEvalResult(kickoff.data.evalJobId, waitMs);
|
|
196
|
+
const { result, noValue } = normalizeEvalResult(d.result);
|
|
197
|
+
const execTimeout = evalExecTimeoutMessage(d.result);
|
|
198
|
+
if (execTimeout)
|
|
199
|
+
throw new Error(execTimeout);
|
|
200
|
+
if (opts.json) {
|
|
201
|
+
console.log(JSON.stringify(noValue ? { ...d, result, hint: EVAL_NO_VALUE_HINT } : { ...d, result }));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log(`${brand('Eval')} ${bold(d.url || url)}`);
|
|
205
|
+
if (d.navigationIncomplete) {
|
|
206
|
+
console.log(`${warning('⚠ Navigation incomplete:')} ${d.note || 'page did not reach full load'}`);
|
|
207
|
+
}
|
|
208
|
+
if (hosted.length)
|
|
209
|
+
console.log(`${muted('Fixtures:')} ${hosted.map((h) => h.name).join(', ')}`);
|
|
210
|
+
console.log(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${expr}`);
|
|
211
|
+
console.log(`\n${result.trim() ? result : muted('(empty result)')}`);
|
|
212
|
+
if (noValue)
|
|
213
|
+
console.log(muted(`\n${EVAL_NO_VALUE_HINT}`));
|
|
214
|
+
if (d.truncated)
|
|
215
|
+
console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
|
|
177
216
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
217
|
+
finally {
|
|
218
|
+
for (const h of hosted) {
|
|
219
|
+
try {
|
|
220
|
+
await deleteFixture(projectGuid, h.guid);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
console.error(warning(`⚠ Could not auto-delete fixture "${h.name}" (${h.guid}) — still hosted at ${h.url}: ${err.message}`));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
181
226
|
}
|
|
182
|
-
console.log(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${expr}`);
|
|
183
|
-
console.log(`\n${result.trim() ? result : muted('(empty result)')}`);
|
|
184
|
-
if (noValue)
|
|
185
|
-
console.log(muted(`\n${EVAL_NO_VALUE_HINT}`));
|
|
186
|
-
if (d.truncated)
|
|
187
|
-
console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
|
|
188
227
|
}));
|
|
189
228
|
// Each `page eval` call runs to completion before the next starts, so two evals
|
|
190
229
|
// fired back-to-back never coexist in time - they CANNOT test whether two live
|
|
@@ -197,6 +236,10 @@ Examples:
|
|
|
197
236
|
# Functionally test a page's own code paths: save a script that drives the UI
|
|
198
237
|
# and returns a JSON-serializable result, then run it (no /tmp + shell quoting):
|
|
199
238
|
gipity page eval "https://dev.gipity.ai/me/app/" --file ./tests/draw-flow.js --json
|
|
239
|
+
# Verify a render/parse path against a REAL file: --fixture hosts it, injects a
|
|
240
|
+
# fetch-able 'fixtureUrl', runs the eval, then deletes the hosted copy:
|
|
241
|
+
gipity page eval "https://dev.gipity.ai/me/app/" --fixture ./sample.mp3 \\
|
|
242
|
+
"(async()=>{ const b = await fetch(fixtureUrl).then(r=>r.arrayBuffer()); return window.App.parseId3(b); })()"
|
|
200
243
|
|
|
201
244
|
The eval body runs under a ~20s in-page execution budget (its own await/setTimeout
|
|
202
245
|
pauses count; --wait only sleeps BEFORE the eval and does not extend it). For a long
|
package/dist/commands/records.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { get, post, put, del } from '../api.js';
|
|
2
|
+
import { get, post, put, patch, del } from '../api.js';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { bold, muted } from '../colors.js';
|
|
5
5
|
import { run, printList, printResult } from '../helpers/index.js';
|
|
6
6
|
import { confirm } from '../utils.js';
|
|
7
|
+
// All commands hit the app API (https://a.gipity.ai/api/<guid>/records/...),
|
|
8
|
+
// which authorizes the logged-in owner via their Bearer token. (The native
|
|
9
|
+
// Records API is the only records surface that exists server-side; there is no
|
|
10
|
+
// /projects/<guid>/records mirror.)
|
|
7
11
|
export const recordsCommand = new Command('records')
|
|
8
12
|
.description('Manage records');
|
|
9
13
|
recordsCommand
|
|
@@ -12,8 +16,38 @@ recordsCommand
|
|
|
12
16
|
.option('--json', 'Output as JSON')
|
|
13
17
|
.action((opts) => run('List', async () => {
|
|
14
18
|
const config = requireConfig();
|
|
15
|
-
const res = await get(`/
|
|
16
|
-
printList(res.data, opts, 'No tables configured for Records API.', t => `${bold(t.table_name)} ${muted(t.auth_level)} ${muted(`pk=${t.primary_key_column}`)} ${muted(`db=${t.database_name}`)}`);
|
|
19
|
+
const res = await get(`/api/${config.projectGuid}/records-config`);
|
|
20
|
+
printList(res.data, opts, 'No tables configured for Records API. Configure one with `gipity records config <table> --auth <level>`.', t => `${bold(t.table_name)} ${muted(t.auth_level)} ${muted(`pk=${t.primary_key_column}`)} ${muted(`db=${t.database_name}`)}`);
|
|
21
|
+
}));
|
|
22
|
+
recordsCommand
|
|
23
|
+
.command('config <table>')
|
|
24
|
+
.description('Show or set a table\'s Records API config (auth level, search, etc.)')
|
|
25
|
+
.option('--auth <level>', 'Auth level: public (anonymous writes), member (sign-in), or user')
|
|
26
|
+
.option('--searchable <bool>', 'Enable full-text search (true/false)')
|
|
27
|
+
.option('--primary-key <col>', 'Primary key column (default: id)')
|
|
28
|
+
.option('--soft-delete <col>', 'Soft-delete column (pass "none" to clear)')
|
|
29
|
+
.option('--json', 'Output as JSON')
|
|
30
|
+
.action((table, opts) => run('Config', async () => {
|
|
31
|
+
const config = requireConfig();
|
|
32
|
+
const base = `/api/${config.projectGuid}/records/${table}/config`;
|
|
33
|
+
// No setter flags → just show the current config.
|
|
34
|
+
const setting = opts.auth || opts.searchable !== undefined || opts.primaryKey || opts.softDelete;
|
|
35
|
+
if (!setting) {
|
|
36
|
+
const res = await get(base);
|
|
37
|
+
printResult(JSON.stringify(res.data, null, 2), opts, res.data);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const body = {};
|
|
41
|
+
if (opts.auth)
|
|
42
|
+
body.auth_level = opts.auth;
|
|
43
|
+
if (opts.searchable !== undefined)
|
|
44
|
+
body.searchable = /^(true|1|on|yes)$/i.test(String(opts.searchable));
|
|
45
|
+
if (opts.primaryKey)
|
|
46
|
+
body.primary_key_column = opts.primaryKey;
|
|
47
|
+
if (opts.softDelete)
|
|
48
|
+
body.soft_delete_column = opts.softDelete === 'none' ? null : opts.softDelete;
|
|
49
|
+
const res = await patch(base, body);
|
|
50
|
+
printResult(`Configured "${table}": auth=${res.data.auth_level}, searchable=${res.data.searchable}, pk=${res.data.primary_key_column}`, opts, res.data);
|
|
17
51
|
}));
|
|
18
52
|
recordsCommand
|
|
19
53
|
.command('query <table>')
|
|
@@ -35,7 +69,7 @@ recordsCommand
|
|
|
35
69
|
params.set('offset', opts.offset);
|
|
36
70
|
if (opts.fields)
|
|
37
71
|
params.set('fields', opts.fields);
|
|
38
|
-
const res = await get(`/
|
|
72
|
+
const res = await get(`/api/${config.projectGuid}/records/${table}?${params}`);
|
|
39
73
|
if (opts.json) {
|
|
40
74
|
console.log(JSON.stringify(res));
|
|
41
75
|
}
|
|
@@ -54,7 +88,7 @@ recordsCommand
|
|
|
54
88
|
.option('--json', 'Output as JSON')
|
|
55
89
|
.action((table, id, opts) => run('Get', async () => {
|
|
56
90
|
const config = requireConfig();
|
|
57
|
-
const res = await get(`/
|
|
91
|
+
const res = await get(`/api/${config.projectGuid}/records/${table}/${id}`);
|
|
58
92
|
console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
|
|
59
93
|
}));
|
|
60
94
|
recordsCommand
|
|
@@ -65,7 +99,7 @@ recordsCommand
|
|
|
65
99
|
.action((table, opts) => run('Create', async () => {
|
|
66
100
|
const config = requireConfig();
|
|
67
101
|
const data = JSON.parse(opts.data);
|
|
68
|
-
const res = await post(`/
|
|
102
|
+
const res = await post(`/api/${config.projectGuid}/records/${table}`, data);
|
|
69
103
|
printResult(`Created: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
70
104
|
}));
|
|
71
105
|
recordsCommand
|
|
@@ -76,7 +110,7 @@ recordsCommand
|
|
|
76
110
|
.action((table, id, opts) => run('Update', async () => {
|
|
77
111
|
const config = requireConfig();
|
|
78
112
|
const data = JSON.parse(opts.data);
|
|
79
|
-
const res = await put(`/
|
|
113
|
+
const res = await put(`/api/${config.projectGuid}/records/${table}/${id}`, data);
|
|
80
114
|
printResult(`Updated: ${JSON.stringify(res.data)}`, opts, res.data);
|
|
81
115
|
}));
|
|
82
116
|
recordsCommand
|
|
@@ -88,7 +122,7 @@ recordsCommand
|
|
|
88
122
|
return;
|
|
89
123
|
}
|
|
90
124
|
const config = requireConfig();
|
|
91
|
-
await del(`/
|
|
125
|
+
await del(`/api/${config.projectGuid}/records/${table}/${id}`);
|
|
92
126
|
printResult('Deleted.', { json: false });
|
|
93
127
|
}));
|
|
94
128
|
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { post } from '../api.js';
|
|
3
|
+
import { requireConfig } from '../config.js';
|
|
4
|
+
import { sync } from '../sync.js';
|
|
5
|
+
import { success, muted } from '../colors.js';
|
|
6
|
+
import { run } from '../helpers/index.js';
|
|
7
|
+
import { confirm } from '../utils.js';
|
|
8
|
+
export const removeCommand = new Command('remove')
|
|
9
|
+
.description('Remove an installed kit from the project (inverse of `gipity add <kit>`).')
|
|
10
|
+
.argument('<kit>', 'Kit key/directory under src/packages/ to remove')
|
|
11
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
12
|
+
.option('--json', 'Output as JSON')
|
|
13
|
+
.action((kit, opts) => run('Remove', async () => {
|
|
14
|
+
const config = requireConfig();
|
|
15
|
+
if (!opts.yes && !opts.json) {
|
|
16
|
+
if (!await confirm(`Remove the "${kit}" kit (its files, import-map entries, and gipity.yaml wiring)?`)) {
|
|
17
|
+
console.log('Cancelled.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const res = await post(`/projects/${config.projectGuid}/remove`, { name: kit });
|
|
22
|
+
// Force the pull so the kit's deletions land locally without tripping the
|
|
23
|
+
// bulk-deletion guard - the removal is an explicit, user-invoked action.
|
|
24
|
+
const syncResult = await sync({ interactive: false, force: true });
|
|
25
|
+
const data = res.data;
|
|
26
|
+
if (opts.json) {
|
|
27
|
+
console.log(JSON.stringify({ ...data, synced: syncResult.applied }));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log(success(`Removed the "${data.kit}" kit.`));
|
|
31
|
+
for (const r of data.removed)
|
|
32
|
+
console.log(muted(` - ${r}`));
|
|
33
|
+
if (data.notes?.length) {
|
|
34
|
+
console.log('');
|
|
35
|
+
for (const n of data.notes)
|
|
36
|
+
console.log(n);
|
|
37
|
+
}
|
|
38
|
+
if (syncResult.applied > 0) {
|
|
39
|
+
console.log(`\nPulled ${syncResult.applied} change(s) to local.`);
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
//# sourceMappingURL=remove.js.map
|
package/dist/commands/test.js
CHANGED
|
@@ -100,8 +100,11 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
|
|
|
100
100
|
const progress = data.totalFiles === 0
|
|
101
101
|
? 'starting up'
|
|
102
102
|
: `${data.completedFiles}/${data.totalFiles} files`;
|
|
103
|
+
// "so far" so a heartbeat line, if captured on its own (tail/grep),
|
|
104
|
+
// can't be mistaken for the final tally — the partial count climbing
|
|
105
|
+
// toward the total previously read as "tests vanished".
|
|
103
106
|
const tally = data.passed + data.failed > 0
|
|
104
|
-
? ` (${data.passed} passed${data.failed > 0 ? `, ${data.failed} failed` : ''})`
|
|
107
|
+
? ` (${data.passed} passed${data.failed > 0 ? `, ${data.failed} failed` : ''} so far)`
|
|
105
108
|
: '';
|
|
106
109
|
console.log(muted(` … still running — ${progress}${tally}, ${elapsed}s elapsed`));
|
|
107
110
|
if (now - startTime >= LONG_RUN_MS && !longRunHintShown) {
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
2
|
import { dirname, resolve } from 'path';
|
|
3
3
|
const CONFIG_FILE = '.gipity.json';
|
|
4
|
+
export const DEFAULT_API_BASE = 'https://a.gipity.ai';
|
|
4
5
|
let cached = null;
|
|
5
6
|
let cachedPath = null;
|
|
6
7
|
/** Global --api-base override (set from root CLI option, takes precedence over config file) */
|
|
@@ -11,6 +12,49 @@ export function setApiBaseOverride(url) {
|
|
|
11
12
|
export function getApiBaseOverride() {
|
|
12
13
|
return apiBaseOverride;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Hosts we will attach the account/device token to. `.gipity.json` is found by
|
|
17
|
+
* walking up from cwd, so its `apiBase` is attacker-controllable: cloning a repo
|
|
18
|
+
* (or installing a template) that ships `{"apiBase":"https://evil.example"}`
|
|
19
|
+
* would otherwise redirect the very next `gipity` command's bearer — and, on
|
|
20
|
+
* refresh, the 7-day refresh token — to that host. Tokens are account-global, so
|
|
21
|
+
* that's account takeover from merely cd-ing into a poisoned tree. Only Gipity
|
|
22
|
+
* hosts over https may receive tokens; the explicit `--api-base` flag is trusted
|
|
23
|
+
* (it's how local dev points at localhost). */
|
|
24
|
+
export function isAllowedApiHost(url) {
|
|
25
|
+
try {
|
|
26
|
+
const { protocol, hostname } = new URL(url);
|
|
27
|
+
if (protocol !== 'https:')
|
|
28
|
+
return false;
|
|
29
|
+
return hostname === 'gipity.ai' || hostname.endsWith('.gipity.ai');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const warnedHosts = new Set();
|
|
36
|
+
/**
|
|
37
|
+
* The API base for token-bearing requests. Precedence:
|
|
38
|
+
* 1. explicit `--api-base` flag (trusted — any host, enables local dev)
|
|
39
|
+
* 2. the project config's `apiBase`, but only if it's an allowed Gipity host
|
|
40
|
+
* 3. the production default
|
|
41
|
+
* A config `apiBase` that fails the allowlist is dropped (with a one-time
|
|
42
|
+
* warning) rather than trusted — see {@link isAllowedApiHost}. */
|
|
43
|
+
export function resolveApiBase() {
|
|
44
|
+
const override = getApiBaseOverride();
|
|
45
|
+
if (override)
|
|
46
|
+
return override;
|
|
47
|
+
const fromConfig = getConfig()?.apiBase;
|
|
48
|
+
if (fromConfig) {
|
|
49
|
+
if (isAllowedApiHost(fromConfig))
|
|
50
|
+
return fromConfig;
|
|
51
|
+
if (!warnedHosts.has(fromConfig)) {
|
|
52
|
+
warnedHosts.add(fromConfig);
|
|
53
|
+
console.error(`⚠ Ignoring untrusted apiBase "${fromConfig}" from .gipity.json — not a gipity.ai host. Using ${DEFAULT_API_BASE}.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return DEFAULT_API_BASE;
|
|
57
|
+
}
|
|
14
58
|
/** Find .gipity.json starting from cwd and walking up */
|
|
15
59
|
function findConfigPath() {
|
|
16
60
|
let dir = process.cwd();
|
|
@@ -92,7 +136,7 @@ export async function resolveProjectContext(opts) {
|
|
|
92
136
|
accountSlug,
|
|
93
137
|
agentGuid: agents.data[0]?.short_guid ?? '',
|
|
94
138
|
conversationGuid: null,
|
|
95
|
-
apiBase: getApiBaseOverride() ||
|
|
139
|
+
apiBase: getApiBaseOverride() || DEFAULT_API_BASE,
|
|
96
140
|
ignore: [],
|
|
97
141
|
},
|
|
98
142
|
oneOff: true,
|
|
@@ -121,7 +165,7 @@ export async function resolveProjectContext(opts) {
|
|
|
121
165
|
accountSlug: res.data.accountSlug,
|
|
122
166
|
agentGuid: res.data.agentGuid ?? '',
|
|
123
167
|
conversationGuid: null,
|
|
124
|
-
apiBase: getApiBaseOverride() ||
|
|
168
|
+
apiBase: getApiBaseOverride() || DEFAULT_API_BASE,
|
|
125
169
|
ignore: [],
|
|
126
170
|
},
|
|
127
171
|
oneOff: true,
|
package/dist/helpers/sync.js
CHANGED
|
@@ -7,12 +7,14 @@ import { muted, error as clrError } from '../colors.js';
|
|
|
7
7
|
* Sync local files with the server before an action (deploy, test, scaffold).
|
|
8
8
|
* Respects --no-sync and --json flags. Non-interactive: bulk-deletion guard
|
|
9
9
|
* blocks accidental wipes from hooks; user must run `gipity sync` manually
|
|
10
|
-
* (or pass --force) to unblock.
|
|
10
|
+
* (or pass --force) to unblock. `--force` on the action (e.g. `deploy --force`)
|
|
11
|
+
* forwards to the sync so it bypasses the guard too - one flag, forceful across
|
|
12
|
+
* the board, matching `gipity sync --force`.
|
|
11
13
|
*/
|
|
12
14
|
export async function syncBeforeAction(opts) {
|
|
13
15
|
if (opts.sync === false)
|
|
14
16
|
return;
|
|
15
|
-
const result = await sync({ interactive: false });
|
|
17
|
+
const result = await sync({ interactive: false, force: opts.force });
|
|
16
18
|
if (result.applied > 0 && !opts.json) {
|
|
17
19
|
console.log(muted(`Synced ${result.applied} change${result.applied > 1 ? 's' : ''}`));
|
|
18
20
|
}
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { planCommand } from './commands/plan.js';
|
|
|
28
28
|
import { fileCommand } from './commands/file.js';
|
|
29
29
|
import { claudeCommand } from './commands/claude.js';
|
|
30
30
|
import { addCommand } from './commands/add.js';
|
|
31
|
+
import { removeCommand } from './commands/remove.js';
|
|
31
32
|
import { logsCommand } from './commands/logs.js';
|
|
32
33
|
import { pageCommand } from './commands/page.js';
|
|
33
34
|
import { recordsCommand } from './commands/records.js';
|
|
@@ -100,7 +101,7 @@ const program = new Command();
|
|
|
100
101
|
// --from Y`). enablePositionalOptions draws the boundary at the first command.
|
|
101
102
|
program.enablePositionalOptions();
|
|
102
103
|
// ── Command groups (logical ordering within each) ──────────────────────
|
|
103
|
-
const commonGroup = [skillCommand, projectCommand, addCommand, deployCommand];
|
|
104
|
+
const commonGroup = [skillCommand, projectCommand, addCommand, removeCommand, deployCommand];
|
|
104
105
|
const connectGroup = [claudeCommand, relayCommand];
|
|
105
106
|
const projectGroup = [domainCommand, statusCommand, initCommand];
|
|
106
107
|
const filesGroup = [fileCommand, syncCommand, pushCommand, uploadCommand];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Host a local file as a public asset so a `gipity page eval` can fetch it
|
|
2
|
+
// in-page, then delete it afterwards. The eval body runs inside the browser and
|
|
3
|
+
// can only receive bulk/binary data over HTTP (the eval source itself is capped
|
|
4
|
+
// and goes through the OS argv limit), so to verify a render/parse path against
|
|
5
|
+
// a real fixture we upload it to the app's public file store (served from
|
|
6
|
+
// media.gipity.ai with permissive CORS) and hand the eval a URL to fetch.
|
|
7
|
+
//
|
|
8
|
+
// Uses the same presigned init -> PUT -> complete flow the app file-upload
|
|
9
|
+
// service exposes, with `public: true`. Cleanup goes through the matching
|
|
10
|
+
// DELETE /api/:appGuid/uploads/:guid, which removes the public object too.
|
|
11
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
12
|
+
import { basename } from 'node:path';
|
|
13
|
+
import { post, del } from './api.js';
|
|
14
|
+
import { guessMime } from './upload.js';
|
|
15
|
+
/** Upload a local file to the app's public file store and return its URL. */
|
|
16
|
+
export async function uploadPublicFixture(projectGuid, localPath) {
|
|
17
|
+
const name = basename(localPath);
|
|
18
|
+
const size = statSync(localPath).size;
|
|
19
|
+
const contentType = guessMime(localPath);
|
|
20
|
+
const init = await post(`/api/${projectGuid}/uploads/init`, { filename: name, content_type: contentType, size, public: true });
|
|
21
|
+
// Fixtures use the single-part path; a multipart fixture would be unusually
|
|
22
|
+
// large for a verification asset, so steer the caller to a smaller file.
|
|
23
|
+
if (init.data.method !== 'PUT' || !init.data.url) {
|
|
24
|
+
throw new Error(`fixture "${name}" is too large to host as a verification asset (${size} bytes)`);
|
|
25
|
+
}
|
|
26
|
+
const res = await fetch(init.data.url, {
|
|
27
|
+
method: 'PUT',
|
|
28
|
+
headers: { 'Content-Type': contentType },
|
|
29
|
+
body: readFileSync(localPath),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`upload of fixture "${name}" failed: ${res.status} ${res.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
const done = await post(`/api/${projectGuid}/uploads/complete`, { upload_guid: init.data.upload_guid });
|
|
35
|
+
return { guid: done.data.guid, url: done.data.url, name, localPath };
|
|
36
|
+
}
|
|
37
|
+
/** Delete a hosted fixture (public object + VFS node). */
|
|
38
|
+
export async function deleteFixture(projectGuid, guid) {
|
|
39
|
+
await del(`/api/${projectGuid}/uploads/${guid}`);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=page-fixtures.js.map
|
package/dist/project-setup.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* drop the Claude Code hooks/skills/gitignore into the target dir - consolidating
|
|
5
5
|
* here keeps both call sites honest and the wording consistent.
|
|
6
6
|
*/
|
|
7
|
-
import { clearConfigCache, saveConfigAt, getApiBaseOverride } from './config.js';
|
|
7
|
+
import { clearConfigCache, saveConfigAt, getApiBaseOverride, DEFAULT_API_BASE } from './config.js';
|
|
8
8
|
import { sync } from './sync.js';
|
|
9
9
|
import { createProgressReporter } from './progress.js';
|
|
10
10
|
import { setupClaudeHooks, setupGitignore, DEFAULT_TOOLS, DEFAULT_SYNC_IGNORE } from './setup.js';
|
|
@@ -20,7 +20,7 @@ export async function finalizeLocalProject(opts) {
|
|
|
20
20
|
accountSlug: opts.accountSlug,
|
|
21
21
|
agentGuid: opts.agentGuid,
|
|
22
22
|
conversationGuid: null,
|
|
23
|
-
apiBase: getApiBaseOverride() ||
|
|
23
|
+
apiBase: getApiBaseOverride() || DEFAULT_API_BASE,
|
|
24
24
|
ignore: [...DEFAULT_SYNC_IGNORE],
|
|
25
25
|
};
|
|
26
26
|
saveConfigAt(opts.dir, config);
|
package/dist/relay/daemon.js
CHANGED
|
@@ -26,7 +26,7 @@ import { stat, readFile } from 'fs/promises';
|
|
|
26
26
|
import { createInterface } from 'readline';
|
|
27
27
|
import { homedir, hostname, platform as osPlatform } from 'os';
|
|
28
28
|
import { join } from 'path';
|
|
29
|
-
import { getApiBaseOverride,
|
|
29
|
+
import { getApiBaseOverride, DEFAULT_API_BASE } from '../config.js';
|
|
30
30
|
import { getProjectsRoot } from './paths.js';
|
|
31
31
|
import { setupClaudeHooks, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE } from '../setup.js';
|
|
32
32
|
import { getAuth, readAuthFresh } from '../auth.js';
|
|
@@ -782,7 +782,7 @@ async function resolveCwdForProject(d) {
|
|
|
782
782
|
}
|
|
783
783
|
log('info', 'bootstrapping new project dir', { slug: d.project_slug, path });
|
|
784
784
|
mkdirSync(path, { recursive: true });
|
|
785
|
-
const apiBase = getApiBaseOverride() ||
|
|
785
|
+
const apiBase = getApiBaseOverride() || DEFAULT_API_BASE;
|
|
786
786
|
writeFileSync(configPath, JSON.stringify({
|
|
787
787
|
projectGuid: d.project_guid,
|
|
788
788
|
projectSlug: d.project_slug,
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* `gipity login` / the relay onboarding flow and never leaves this file
|
|
9
9
|
* or the Authorization header.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { resolveApiBase } from '../config.js';
|
|
12
12
|
import * as state from './state.js';
|
|
13
13
|
export function apiBase() {
|
|
14
|
-
return
|
|
14
|
+
return resolveApiBase();
|
|
15
15
|
}
|
|
16
16
|
export function deviceToken() {
|
|
17
17
|
const d = state.getDevice();
|
package/dist/sync.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* the next pass so every client sees it. No content merging, ever.
|
|
23
23
|
*/
|
|
24
24
|
import { writeFileSync, mkdirSync, existsSync, statSync, unlinkSync, readdirSync, rmdirSync, readFileSync, renameSync, openSync, closeSync } from 'fs';
|
|
25
|
-
import { join, relative, dirname, extname } from 'path';
|
|
25
|
+
import { join, relative, dirname, extname, resolve, sep } from 'path';
|
|
26
26
|
import { hostname } from 'os';
|
|
27
27
|
import { get, del, downloadStream, ApiError } from './api.js';
|
|
28
28
|
import { requireConfig, shouldIgnore, getConfigPath } from './config.js';
|
|
@@ -189,6 +189,23 @@ async function ensureLocalHashes(root, local, paths) {
|
|
|
189
189
|
function normalizeTreePath(p) {
|
|
190
190
|
return p.replace(/^\/+/, '');
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Resolve a relative path against the project root and assert it stays inside.
|
|
194
|
+
* Remote-supplied paths (the server's `/files/tree`, tar entry names, and the
|
|
195
|
+
* conflict-rename targets derived from them) are untrusted: `normalizeTreePath`
|
|
196
|
+
* strips a leading slash but NOT `..` segments, so a path like
|
|
197
|
+
* `../../.ssh/authorized_keys` would otherwise resolve outside the project and
|
|
198
|
+
* be written/renamed/deleted there. The relay daemon runs `sync` unattended on
|
|
199
|
+
* every dispatch, so an unchecked traversal is arbitrary file write with no
|
|
200
|
+
* human in the loop. Throws on escape; callers skip the offending action. */
|
|
201
|
+
export function resolveInRoot(root, relPath) {
|
|
202
|
+
const rootResolved = resolve(root);
|
|
203
|
+
const full = resolve(rootResolved, relPath);
|
|
204
|
+
if (full !== rootResolved && !full.startsWith(rootResolved + sep)) {
|
|
205
|
+
throw new Error(`Refusing path outside project root: ${relPath}`);
|
|
206
|
+
}
|
|
207
|
+
return full;
|
|
208
|
+
}
|
|
192
209
|
async function fetchRemote(projectGuid) {
|
|
193
210
|
const res = await get(`/projects/${projectGuid}/files/tree`);
|
|
194
211
|
const out = new Map();
|
|
@@ -223,6 +240,22 @@ async function downloadAll(projectGuid, onBytes) {
|
|
|
223
240
|
});
|
|
224
241
|
}
|
|
225
242
|
async function fetchOne(projectGuid, path) {
|
|
243
|
+
// Exact single-file read first. The tree-tar endpoint below treats its `path`
|
|
244
|
+
// as a DIRECTORY prefix, so a single root file (e.g. `gipity.yaml`) comes back
|
|
245
|
+
// empty — which silently broke conflict restores and trapped sync in an
|
|
246
|
+
// unresolvable delete-vs-newer loop. `/files/read` is the exact-path endpoint
|
|
247
|
+
// (what `gipity file cat` uses); it returns text content, reliable for the
|
|
248
|
+
// config/code files that actually hit a restore. Binary falls through to tar.
|
|
249
|
+
try {
|
|
250
|
+
const res = await get(`/projects/${projectGuid}/files/read?path=${encodeURIComponent(path)}`);
|
|
251
|
+
const content = res?.data?.content;
|
|
252
|
+
if (typeof content === 'string' && isTextMime(res?.data?.mime, path)) {
|
|
253
|
+
return Buffer.from(content, 'utf-8');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
/* fall through to the tar path */
|
|
258
|
+
}
|
|
226
259
|
try {
|
|
227
260
|
const stream = await downloadStream(`/projects/${projectGuid}/files/tree?content=tar&path=${encodeURIComponent(path)}`);
|
|
228
261
|
const extract = tar.extract();
|
|
@@ -248,6 +281,14 @@ async function fetchOne(projectGuid, path) {
|
|
|
248
281
|
return null;
|
|
249
282
|
}
|
|
250
283
|
}
|
|
284
|
+
// Treat a file as text (safe to round-trip through `/files/read`'s string body)
|
|
285
|
+
// from its mime or, failing that, a code/config extension. Binary needs the
|
|
286
|
+
// byte-exact tar path.
|
|
287
|
+
function isTextMime(mime, path) {
|
|
288
|
+
if (mime && (mime.startsWith('text/') || /(json|javascript|xml|yaml|x-sh|sql)/.test(mime)))
|
|
289
|
+
return true;
|
|
290
|
+
return /\.(js|mjs|cjs|ts|tsx|jsx|json|yaml|yml|sql|md|txt|html|css|svg|csv|env|sh|toml|ini)$/i.test(path);
|
|
291
|
+
}
|
|
251
292
|
// ─── Classification ────────────────────────────────────────────
|
|
252
293
|
function classifyLocal(info, base) {
|
|
253
294
|
if (!info && !base)
|
|
@@ -373,9 +414,13 @@ export function plan(local, remote, baseline) {
|
|
|
373
414
|
// deleted × absent → baseline is stale, drop it silently (no action)
|
|
374
415
|
if (lSide === 'deleted' && rSide === 'absent')
|
|
375
416
|
continue;
|
|
376
|
-
// deleted × unchanged → delete remote
|
|
417
|
+
// deleted × unchanged → delete remote. Use the remote's CURRENT version for
|
|
418
|
+
// the optimistic-delete check, not the baseline's: the content can be equal
|
|
419
|
+
// (rSide 'unchanged' is sha-based) while the server version moved ahead - the
|
|
420
|
+
// baseline version would then fail the CAS and the delete would loop. The
|
|
421
|
+
// remote read we already have carries the live version.
|
|
377
422
|
if (lSide === 'deleted' && rSide === 'unchanged') {
|
|
378
|
-
actions.push({ path, kind: 'delete-remote', remoteSize: R.size, expectedServerVersion:
|
|
423
|
+
actions.push({ path, kind: 'delete-remote', remoteSize: R.size, expectedServerVersion: R.serverVersion });
|
|
379
424
|
continue;
|
|
380
425
|
}
|
|
381
426
|
// deleted × modified → remote wins, restore locally — but only if the remote
|
|
@@ -603,7 +648,14 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
603
648
|
errors.push(`Download missing: ${a.path}`);
|
|
604
649
|
continue;
|
|
605
650
|
}
|
|
606
|
-
|
|
651
|
+
let full;
|
|
652
|
+
try {
|
|
653
|
+
full = resolveInRoot(root, a.path);
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
errors.push(e.message);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
607
659
|
mkdirSync(dirname(full), { recursive: true });
|
|
608
660
|
writeFileSync(full, buf);
|
|
609
661
|
const stat = statSync(full);
|
|
@@ -619,8 +671,16 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
619
671
|
// upload the renamed copy. If the upload of the renamed copy fails, we
|
|
620
672
|
// still keep the rename on disk - next sync picks it up as "added".
|
|
621
673
|
for (const a of conflictQueue) {
|
|
622
|
-
|
|
623
|
-
|
|
674
|
+
let full;
|
|
675
|
+
let renamed;
|
|
676
|
+
try {
|
|
677
|
+
full = resolveInRoot(root, a.path);
|
|
678
|
+
renamed = resolveInRoot(root, a.renamedLocalTo);
|
|
679
|
+
}
|
|
680
|
+
catch (e) {
|
|
681
|
+
errors.push(e.message);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
624
684
|
try {
|
|
625
685
|
mkdirSync(dirname(renamed), { recursive: true });
|
|
626
686
|
renameSync(full, renamed);
|
|
@@ -680,7 +740,14 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
680
740
|
if (idx >= uploadQueue.length)
|
|
681
741
|
return;
|
|
682
742
|
const a = uploadQueue[idx];
|
|
683
|
-
|
|
743
|
+
let full;
|
|
744
|
+
try {
|
|
745
|
+
full = resolveInRoot(root, a.path);
|
|
746
|
+
}
|
|
747
|
+
catch (e) {
|
|
748
|
+
errors.push(e.message);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
684
751
|
try {
|
|
685
752
|
const result = await uploadOneFile(config.projectGuid, full, a.path, {
|
|
686
753
|
expectedServerVersion: a.expectedServerVersion,
|
|
@@ -701,8 +768,16 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
701
768
|
// re-upload the rename.
|
|
702
769
|
const currentBytes = await fetchOne(config.projectGuid, a.path);
|
|
703
770
|
const renamedRel = conflictedCopyName(a.path);
|
|
771
|
+
let renamedFull;
|
|
772
|
+
try {
|
|
773
|
+
renamedFull = resolveInRoot(root, renamedRel);
|
|
774
|
+
}
|
|
775
|
+
catch (e) {
|
|
776
|
+
errors.push(e.message);
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
704
779
|
try {
|
|
705
|
-
renameSync(full,
|
|
780
|
+
renameSync(full, renamedFull);
|
|
706
781
|
}
|
|
707
782
|
catch (e) {
|
|
708
783
|
errors.push(`Rename failed for ${a.path}: ${e.message}`);
|
|
@@ -719,9 +794,9 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
719
794
|
};
|
|
720
795
|
}
|
|
721
796
|
try {
|
|
722
|
-
const result = await uploadOneFile(config.projectGuid,
|
|
723
|
-
const stat = statSync(
|
|
724
|
-
const { sha256 } = await hashFile(
|
|
797
|
+
const result = await uploadOneFile(config.projectGuid, renamedFull, renamedRel, { expectedServerVersion: null });
|
|
798
|
+
const stat = statSync(renamedFull);
|
|
799
|
+
const { sha256 } = await hashFile(renamedFull);
|
|
725
800
|
baseline.files[renamedRel] = {
|
|
726
801
|
size: stat.size, mtime: stat.mtime.toISOString(),
|
|
727
802
|
sha256, serverVersion: result.serverVersion,
|
|
@@ -744,9 +819,9 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
744
819
|
for (const a of plannedToApply) {
|
|
745
820
|
if (a.kind === 'delete-local') {
|
|
746
821
|
try {
|
|
747
|
-
unlinkSync(
|
|
822
|
+
unlinkSync(resolveInRoot(root, a.path));
|
|
748
823
|
}
|
|
749
|
-
catch { /* already gone */ }
|
|
824
|
+
catch { /* already gone or outside root */ }
|
|
750
825
|
delete baseline.files[a.path];
|
|
751
826
|
applied++;
|
|
752
827
|
}
|
|
@@ -773,7 +848,7 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
773
848
|
const buf = await fetchOne(config.projectGuid, a.path);
|
|
774
849
|
if (!buf)
|
|
775
850
|
throw new Error('remote bytes unavailable');
|
|
776
|
-
const full =
|
|
851
|
+
const full = resolveInRoot(root, a.path);
|
|
777
852
|
mkdirSync(dirname(full), { recursive: true });
|
|
778
853
|
writeFileSync(full, buf);
|
|
779
854
|
const stat = statSync(full);
|
|
@@ -787,7 +862,13 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
787
862
|
errors.push(`Could not delete ${a.path}: server has a newer version - restored the server copy locally (delete it again and re-sync to confirm)`);
|
|
788
863
|
}
|
|
789
864
|
catch {
|
|
790
|
-
|
|
865
|
+
// Restore failed (e.g. the bytes truly couldn't be fetched). DROP the
|
|
866
|
+
// baseline entry rather than leave a stale one: a stale entry re-plans
|
|
867
|
+
// the same impossible delete every run (the original loop). With no
|
|
868
|
+
// baseline, the next sync re-evaluates this path from scratch — as a
|
|
869
|
+
// remote 'added' it downloads cleanly, no loop.
|
|
870
|
+
delete baseline.files[a.path];
|
|
871
|
+
errors.push(`Could not delete ${a.path}: server has a newer version - reset its sync state; re-run \`gipity sync\` to pull the server copy.`);
|
|
791
872
|
}
|
|
792
873
|
}
|
|
793
874
|
else if (err instanceof ApiError && err.statusCode === 404) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.387",
|
|
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,7 +12,7 @@
|
|
|
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__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-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-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-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",
|
|
15
|
+
"test:smoke": "tsc && node --test dist/__tests__/utils.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__/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-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
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",
|
|
17
17
|
"test:e2e:sandbox": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sandbox-live.test.js"
|
|
18
18
|
},
|