gipity 1.1.4 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adopt-cwd.js +1 -1
- package/dist/banner.js +3 -5
- package/dist/commands/add.js +1 -1
- package/dist/commands/build.js +12 -11
- package/dist/commands/chat.js +1 -1
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/generate.js +5 -5
- package/dist/commands/gmail.js +1 -1
- package/dist/commands/job.js +10 -5
- package/dist/commands/load.js +2 -2
- package/dist/commands/login.js +7 -2
- package/dist/commands/page-eval.js +5 -5
- package/dist/commands/page-fetch.js +14 -10
- package/dist/commands/page-inspect.js +2 -2
- package/dist/commands/page-screenshot.js +5 -5
- package/dist/commands/page-test.js +1 -1
- package/dist/commands/remove.js +1 -1
- package/dist/commands/sandbox.js +1 -1
- package/dist/commands/save.js +1 -1
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/service.js +1 -1
- package/dist/commands/setup.js +4 -6
- package/dist/commands/status.js +41 -12
- package/dist/commands/storage.js +2 -2
- package/dist/commands/sync.js +15 -0
- package/dist/commands/test.js +1 -1
- package/dist/commands/upload.js +1 -1
- package/dist/commands/workflow.js +1 -1
- package/dist/index.js +495 -415
- package/dist/knowledge.js +3 -0
- package/dist/login-flow.js +44 -2
- package/dist/relay/onboarding.js +26 -44
- package/dist/sync.js +6 -6
- package/dist/utils.js +60 -3
- package/package.json +2 -2
package/dist/adopt-cwd.js
CHANGED
|
@@ -167,7 +167,7 @@ export function formatCwdLabel(cwd) {
|
|
|
167
167
|
const tail = norm.slice(home.length + 1).split(sep);
|
|
168
168
|
if (tail.length <= 2)
|
|
169
169
|
return '~/' + tail.join('/');
|
|
170
|
-
return '
|
|
170
|
+
return '~/.../' + tail.slice(-2).join('/');
|
|
171
171
|
}
|
|
172
172
|
// Outside home: show parent/this for non-root paths.
|
|
173
173
|
const parts = norm.split(sep).filter(Boolean);
|
package/dist/banner.js
CHANGED
|
@@ -115,15 +115,13 @@ function padR(s, width) {
|
|
|
115
115
|
const gap = width - visLen(s);
|
|
116
116
|
return gap > 0 ? s + ' '.repeat(gap) : s;
|
|
117
117
|
}
|
|
118
|
-
/** Truncate from the left with a leading
|
|
118
|
+
/** Truncate from the left with a leading "..." if longer than maxW (plain text). */
|
|
119
119
|
function leadingEllipsify(s, maxW) {
|
|
120
120
|
if (s.length <= maxW)
|
|
121
121
|
return s;
|
|
122
|
-
if (maxW <=
|
|
122
|
+
if (maxW <= 4)
|
|
123
123
|
return s.slice(-maxW);
|
|
124
|
-
|
|
125
|
-
return '…'.padStart(maxW);
|
|
126
|
-
return '…' + s.slice(-(maxW - 1));
|
|
124
|
+
return '...' + s.slice(-(maxW - 3));
|
|
127
125
|
}
|
|
128
126
|
function center(s, width) {
|
|
129
127
|
const gap = width - visLen(s);
|
package/dist/commands/add.js
CHANGED
|
@@ -207,7 +207,7 @@ export const addCommand = new Command('add')
|
|
|
207
207
|
const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
|
|
208
208
|
const res = opts.json
|
|
209
209
|
? await doAdd()
|
|
210
|
-
: await withSpinner('Installing
|
|
210
|
+
: await withSpinner('Installing...', doAdd, { done: null });
|
|
211
211
|
// Pull the created/installed files down to local.
|
|
212
212
|
const syncResult = await sync({ interactive: false, progress: opts.json ? undefined : createProgressReporter() });
|
|
213
213
|
const data = res.data;
|
package/dist/commands/build.js
CHANGED
|
@@ -50,7 +50,7 @@ const __clPkg = readOwnPkg(__clDir);
|
|
|
50
50
|
* incomplete pull, so we can tell the user plainly that nothing was deleted. */
|
|
51
51
|
function reportSyncResult(result) {
|
|
52
52
|
if (result.aborted) {
|
|
53
|
-
console.log(` ${warning('Merge cancelled
|
|
53
|
+
console.log(` ${warning('Merge cancelled - this folder was NOT synced with the project.')}`);
|
|
54
54
|
console.log(` ${muted('For a clean copy, quit and open the project in an empty folder. To merge anyway, run `gipity sync`.')}`);
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
@@ -58,11 +58,11 @@ function reportSyncResult(result) {
|
|
|
58
58
|
console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
|
|
59
59
|
}
|
|
60
60
|
if (result.errors.length) {
|
|
61
|
-
console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? '' : 's'}
|
|
61
|
+
console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? '' : 's'} - your local copy may be incomplete:`)}`);
|
|
62
62
|
for (const e of result.errors.slice(0, 8))
|
|
63
63
|
console.log(` - ${e}`);
|
|
64
64
|
if (result.errors.length > 8)
|
|
65
|
-
console.log(`
|
|
65
|
+
console.log(` ...and ${result.errors.length - 8} more.`);
|
|
66
66
|
console.log(` ${muted('Nothing was deleted. Re-run `gipity sync` to finish the pull.')}`);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -133,7 +133,7 @@ function localFsFallback(dir) {
|
|
|
133
133
|
};
|
|
134
134
|
walk(dir, 0);
|
|
135
135
|
const topLevel = topLevelEntries.length
|
|
136
|
-
? topLevelEntries.slice(0, 20).join(', ') + (topLevelEntries.length > 20 ? ',
|
|
136
|
+
? topLevelEntries.slice(0, 20).join(', ') + (topLevelEntries.length > 20 ? ', ...' : '')
|
|
137
137
|
: '(empty directory)';
|
|
138
138
|
return { fileCount, folderCount, totalBytes, topLevel };
|
|
139
139
|
}
|
|
@@ -502,6 +502,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
|
|
|
502
502
|
console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
|
|
503
503
|
}
|
|
504
504
|
setupProjectTools();
|
|
505
|
+
if (!nonInteractive)
|
|
506
|
+
console.log(` ${muted('Installed Gipity plugin, skills, and hooks.')}`);
|
|
505
507
|
if (nonInteractive) {
|
|
506
508
|
// Headless: the -p message is wrapped later (Step 3). Just record
|
|
507
509
|
// whether to use the new-project framing for that wrap.
|
|
@@ -673,6 +675,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
|
|
|
673
675
|
// Claude directly what they want to build or do.
|
|
674
676
|
initialPrompt = '';
|
|
675
677
|
setupProjectTools();
|
|
678
|
+
if (!nonInteractive)
|
|
679
|
+
console.log(` ${muted('Installed Gipity plugin, skills, and hooks.')}`);
|
|
676
680
|
console.log(` ${success(`Project "${project.name}" ready.`)}\n`);
|
|
677
681
|
}
|
|
678
682
|
// ── Step 3: Pick the coding agent + model ─────────────────────────
|
|
@@ -812,9 +816,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
|
|
|
812
816
|
}
|
|
813
817
|
if (!nonInteractive) {
|
|
814
818
|
console.log(` ${bold('Launching Claude Code, powered by Gipity.')}`);
|
|
815
|
-
console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude can do,")}`);
|
|
816
|
-
console.log(` ${muted('plus hosting, databases, and live deploys on Gipity.')}`);
|
|
817
819
|
if (convGuidForHooks) {
|
|
820
|
+
console.log('');
|
|
818
821
|
console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand('prompt.gipity.ai')}. (--no-capture on init to disable.)`)}`);
|
|
819
822
|
}
|
|
820
823
|
console.log('');
|
|
@@ -960,12 +963,11 @@ async function pickAgent(lastUsed) {
|
|
|
960
963
|
const defaultIdx = AGENT_ADAPTERS.findIndex(a => a.key === defaultKey) + 1;
|
|
961
964
|
console.log(` ${bold('Which coding agent?')}\n`);
|
|
962
965
|
AGENT_ADAPTERS.forEach((a, i) => {
|
|
963
|
-
|
|
966
|
+
// Keep the list clean: no install-state notes (picking an uninstalled
|
|
967
|
+
// agent installs it, or fails with the install hint at that point).
|
|
964
968
|
const notes = [];
|
|
965
969
|
if (a.key === lastUsed)
|
|
966
970
|
notes.push('last used');
|
|
967
|
-
if (!installed)
|
|
968
|
-
notes.push(a.ensureInstalled ? "not installed - we'll install it" : 'not installed');
|
|
969
971
|
if (a.key === 'codex' && !a.hooksSupportedOnPlatform(process.platform)) {
|
|
970
972
|
notes.push('session recording unavailable on Windows');
|
|
971
973
|
}
|
|
@@ -1101,9 +1103,8 @@ async function launchNonClaudeAgent(adapter, ctx) {
|
|
|
1101
1103
|
else {
|
|
1102
1104
|
agentArgs = [...adapter.buildInteractiveArgs({ resume, model }), ...extras];
|
|
1103
1105
|
console.log(` ${bold(`Launching ${adapter.displayName}, powered by Gipity.`)}`);
|
|
1104
|
-
console.log(` ${muted(`Just tell ${adapter.displayName} what you'd like to build or do - plus hosting,`)}`);
|
|
1105
|
-
console.log(` ${muted('databases, and live deploys on Gipity.')}`);
|
|
1106
1106
|
if (convGuid) {
|
|
1107
|
+
console.log('');
|
|
1107
1108
|
console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand('prompt.gipity.ai')}. (--no-capture on init to disable.)`)}`);
|
|
1108
1109
|
}
|
|
1109
1110
|
if (adapter.oneTimeSetupNote) {
|
package/dist/commands/chat.js
CHANGED
|
@@ -25,7 +25,7 @@ export const chatCommand = new Command('chat')
|
|
|
25
25
|
const doChat = () => post(endpoint, body);
|
|
26
26
|
const res = opts.json
|
|
27
27
|
? await doChat()
|
|
28
|
-
: await withSpinner('Thinking
|
|
28
|
+
: await withSpinner('Thinking...', doChat, { done: null });
|
|
29
29
|
// Save conversation guid for continuity. Skipped in one-off mode: the
|
|
30
30
|
// config was resolved from the server's Home project and there is no
|
|
31
31
|
// local `.gipity.json` to update - persisting here would create one in
|
package/dist/commands/deploy.js
CHANGED
|
@@ -57,7 +57,7 @@ export const deployCommand = new Command('deploy')
|
|
|
57
57
|
});
|
|
58
58
|
const res = opts.json
|
|
59
59
|
? await doDeploy()
|
|
60
|
-
: await withSpinner(`Deploying to ${target}
|
|
60
|
+
: await withSpinner(`Deploying to ${target}...`, doDeploy, { done: null });
|
|
61
61
|
const d = res.data;
|
|
62
62
|
// Deploy + verify in one command: after a successful deploy, run the
|
|
63
63
|
// same inspect pipeline `page inspect` uses against the live URL. An
|
|
@@ -219,7 +219,7 @@ Examples:
|
|
|
219
219
|
seed: Number.isFinite(opts.seed) ? opts.seed : undefined,
|
|
220
220
|
input_images: inputImages,
|
|
221
221
|
});
|
|
222
|
-
const verb = inputImages ? 'Editing image
|
|
222
|
+
const verb = inputImages ? 'Editing image...' : 'Generating image...';
|
|
223
223
|
const result = opts.json
|
|
224
224
|
? await doGenerate()
|
|
225
225
|
: await withSpinner(verb, doGenerate, { done: null });
|
|
@@ -285,7 +285,7 @@ Examples:
|
|
|
285
285
|
// Veo runs 30-120s; the bouncing bar + timer keeps the wait honest.
|
|
286
286
|
const result = opts.json
|
|
287
287
|
? await doGenerate()
|
|
288
|
-
: await withSpinner('Generating video
|
|
288
|
+
: await withSpinner('Generating video...', doGenerate, { done: null });
|
|
289
289
|
const filename = opts.output || 'generated.mp4';
|
|
290
290
|
const savedPath = await downloadFile(result.url, filename, !!opts.output);
|
|
291
291
|
if (opts.json) {
|
|
@@ -349,7 +349,7 @@ Examples:
|
|
|
349
349
|
});
|
|
350
350
|
const result = opts.json
|
|
351
351
|
? await doGenerate()
|
|
352
|
-
: await withSpinner('Generating speech
|
|
352
|
+
: await withSpinner('Generating speech...', doGenerate, { done: null });
|
|
353
353
|
const filename = opts.output || 'speech.mp3';
|
|
354
354
|
const savedPath = await downloadFile(result.url, filename, !!opts.output);
|
|
355
355
|
if (opts.json) {
|
|
@@ -402,7 +402,7 @@ Examples:
|
|
|
402
402
|
});
|
|
403
403
|
const result = opts.json
|
|
404
404
|
? await doGenerate()
|
|
405
|
-
: await withSpinner('Generating music
|
|
405
|
+
: await withSpinner('Generating music...', doGenerate, { done: null });
|
|
406
406
|
const filename = opts.output || 'music.mp3';
|
|
407
407
|
const savedPath = await downloadFile(result.url, filename, !!opts.output);
|
|
408
408
|
if (opts.json) {
|
|
@@ -453,7 +453,7 @@ Examples:
|
|
|
453
453
|
});
|
|
454
454
|
const result = opts.json
|
|
455
455
|
? await doGenerate()
|
|
456
|
-
: await withSpinner('Generating sound effect
|
|
456
|
+
: await withSpinner('Generating sound effect...', doGenerate, { done: null });
|
|
457
457
|
const filename = opts.output || 'sound.mp3';
|
|
458
458
|
const savedPath = await downloadFile(result.url, filename, !!opts.output);
|
|
459
459
|
if (opts.json) {
|
package/dist/commands/gmail.js
CHANGED
|
@@ -61,7 +61,7 @@ gmailCommand
|
|
|
61
61
|
.option('--cc <email>', 'Cc recipient (repeatable)', collect, [])
|
|
62
62
|
.option('--bcc <email>', 'Bcc recipient (repeatable)', collect, [])
|
|
63
63
|
.option('--reply-to <email>', 'Reply-To header address')
|
|
64
|
-
.requiredOption('--subject <s>', 'Subject (usually "Re:
|
|
64
|
+
.requiredOption('--subject <s>', 'Subject (usually "Re: ..." of the original)')
|
|
65
65
|
.requiredOption('--body <text>', 'Reply body (you compose the quote header)')
|
|
66
66
|
.option('--json', 'Output as JSON')
|
|
67
67
|
.action((opts) => run('Reply', async () => {
|
package/dist/commands/job.js
CHANGED
|
@@ -63,8 +63,13 @@ jobCommand
|
|
|
63
63
|
const r = res.data;
|
|
64
64
|
const statusColor = r.status === 'success' ? success : r.status === 'failed' ? clrError : muted;
|
|
65
65
|
console.log(`${statusColor(r.status)} ${muted(r.guid)}`);
|
|
66
|
-
if (r.progress_pct != null)
|
|
67
|
-
|
|
66
|
+
if (r.progress_pct != null) {
|
|
67
|
+
const prog = `${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ''}`;
|
|
68
|
+
// A failed run can have reported 100% (done) right before dying; a bare
|
|
69
|
+
// "progress: 100% (done)" next to status=failed reads as success. Label it.
|
|
70
|
+
const failedish = r.status === 'failed' || r.status === 'cancelled' || r.status === 'canceled' || r.status === 'error';
|
|
71
|
+
console.log(failedish ? `last progress before ${r.status}: ${prog}` : `progress: ${prog}`);
|
|
72
|
+
}
|
|
68
73
|
if (r.duration_ms != null)
|
|
69
74
|
console.log(`duration: ${r.duration_ms}ms`);
|
|
70
75
|
if (r.error)
|
|
@@ -98,7 +103,7 @@ jobCommand
|
|
|
98
103
|
? `${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ''}`
|
|
99
104
|
: r.status;
|
|
100
105
|
if (prog !== lastProgress) {
|
|
101
|
-
console.error(muted(
|
|
106
|
+
console.error(muted(`... ${prog}`));
|
|
102
107
|
lastProgress = prog;
|
|
103
108
|
}
|
|
104
109
|
}
|
|
@@ -198,7 +203,7 @@ jobCommand
|
|
|
198
203
|
}
|
|
199
204
|
catch (e) {
|
|
200
205
|
if (peekedOut) {
|
|
201
|
-
console.error(muted(
|
|
206
|
+
console.error(muted(`... still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
|
|
202
207
|
return;
|
|
203
208
|
}
|
|
204
209
|
throw e;
|
|
@@ -221,7 +226,7 @@ jobCommand
|
|
|
221
226
|
catch (e) {
|
|
222
227
|
// Peek window elapsed mid-stream: report cleanly and exit 0 (not an error).
|
|
223
228
|
if (peekedOut) {
|
|
224
|
-
console.error(muted(
|
|
229
|
+
console.error(muted(`... still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
|
|
225
230
|
return;
|
|
226
231
|
}
|
|
227
232
|
throw e;
|
package/dist/commands/load.js
CHANGED
|
@@ -152,7 +152,7 @@ export const loadCommand = new Command('load')
|
|
|
152
152
|
if (opts.inspect) {
|
|
153
153
|
const res = opts.json
|
|
154
154
|
? await doInspect()
|
|
155
|
-
: await withSpinner('Inspecting
|
|
155
|
+
: await withSpinner('Inspecting...', doInspect, { done: null });
|
|
156
156
|
if (opts.json) {
|
|
157
157
|
console.log(JSON.stringify(res.data));
|
|
158
158
|
return;
|
|
@@ -162,7 +162,7 @@ export const loadCommand = new Command('load')
|
|
|
162
162
|
}
|
|
163
163
|
const { data, partial } = opts.json
|
|
164
164
|
? await doImport()
|
|
165
|
-
: await withSpinner('Importing
|
|
165
|
+
: await withSpinner('Importing...', doImport, { done: null });
|
|
166
166
|
const project = data.project;
|
|
167
167
|
// Materialize a local dir and link it, exactly like `gipity project create`:
|
|
168
168
|
// `.gipity.json` is written directly inside the new dir, then a soft sync
|
package/dist/commands/login.js
CHANGED
|
@@ -4,6 +4,7 @@ import { publicPost } from '../api.js';
|
|
|
4
4
|
import { prompt, decodeJwtExp } from '../utils.js';
|
|
5
5
|
import { success, error as clrError, muted } from '../colors.js';
|
|
6
6
|
import { flushBugQueue } from '../bug-queue.js';
|
|
7
|
+
import { warnBeforeCodeIfUnexpectedNewAccount, warnIfUnexpectedNewAccount } from '../login-flow.js';
|
|
7
8
|
export const loginCommand = new Command('login')
|
|
8
9
|
.description('Log in or sign up')
|
|
9
10
|
.option('--email <email>', 'Email address')
|
|
@@ -19,9 +20,10 @@ export const loginCommand = new Command('login')
|
|
|
19
20
|
}
|
|
20
21
|
// Email only → send code and exit (non-interactive step 1)
|
|
21
22
|
if (email && !code) {
|
|
22
|
-
await publicPost('/auth/login', { email });
|
|
23
|
+
const sendRes = await publicPost('/auth/login', { email });
|
|
23
24
|
console.log('Check your email for a 6-digit code.');
|
|
24
25
|
console.log(muted(`Then run: gipity login --email ${email} --code <code>`));
|
|
26
|
+
warnBeforeCodeIfUnexpectedNewAccount(sendRes.isNewUser, email);
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
29
|
// Fully interactive flow
|
|
@@ -34,9 +36,10 @@ export const loginCommand = new Command('login')
|
|
|
34
36
|
console.error(clrError('Email required.'));
|
|
35
37
|
process.exit(1);
|
|
36
38
|
}
|
|
37
|
-
await publicPost('/auth/login', { email });
|
|
39
|
+
const sendRes = await publicPost('/auth/login', { email });
|
|
38
40
|
console.log('');
|
|
39
41
|
console.log('Check your email for a 6-digit code.');
|
|
42
|
+
warnBeforeCodeIfUnexpectedNewAccount(sendRes.isNewUser, email);
|
|
40
43
|
code = await prompt('Code: ');
|
|
41
44
|
await verify(email, code);
|
|
42
45
|
}
|
|
@@ -46,6 +49,7 @@ export const loginCommand = new Command('login')
|
|
|
46
49
|
}
|
|
47
50
|
});
|
|
48
51
|
async function verify(email, code) {
|
|
52
|
+
const priorAuth = getAuth();
|
|
49
53
|
const res = await publicPost('/auth/verify', { email, code });
|
|
50
54
|
const exp = decodeJwtExp(res.accessToken);
|
|
51
55
|
if (!exp) {
|
|
@@ -60,6 +64,7 @@ async function verify(email, code) {
|
|
|
60
64
|
expiresAt,
|
|
61
65
|
});
|
|
62
66
|
console.log(success(`Logged in (${email}).`));
|
|
67
|
+
warnIfUnexpectedNewAccount(res.isNewUser, email, priorAuth);
|
|
63
68
|
// A fresh session is the clearest "we're reconnected" signal - clear any bug
|
|
64
69
|
// reports that got stranded while this account's session was expired/offline.
|
|
65
70
|
const delivered = await flushBugQueue().catch(() => 0);
|
|
@@ -106,7 +106,7 @@ export function summarizeExpr(expr) {
|
|
|
106
106
|
if (oneLine && expr.trim().length <= EXPR_ECHO_MAX_CHARS)
|
|
107
107
|
return expr.trim();
|
|
108
108
|
const first = (meaningful[0] ?? '').trim();
|
|
109
|
-
const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS -
|
|
109
|
+
const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS - 3)}...` : first;
|
|
110
110
|
const shape = oneLine
|
|
111
111
|
? `(${expr.trim().length} chars)`
|
|
112
112
|
: `(+${meaningful.length - 1} more ${meaningful.length - 1 === 1 ? 'line' : 'lines'}, ${expr.trim().length} chars)`;
|
|
@@ -379,7 +379,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
379
379
|
.description('Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead')
|
|
380
380
|
.argument('<url>', 'URL to load')
|
|
381
381
|
.argument('[expr]', `JavaScript to evaluate in page context (inline expression or statement body; await works, and the trailing expression is returned automatically — REPL-style — so no explicit return is needed; result is JSON-serialized). Omit when using --file. Time budget: the body has ${EVAL_SCRIPT_BUDGET_MS / 1000}s to finish after page load (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1000}s with --camera) - raise it with --timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1000}s.`)
|
|
382
|
-
.option('--file <path>', `Read the script body from a file instead of the inline <expr> arg (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<'EOF'
|
|
382
|
+
.option('--file <path>', `Read the script body from a file instead of the inline <expr> arg (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<'EOF' ... EOF) with no tmp file. Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`)
|
|
383
383
|
.option('--step <expr>', `Run another expression against the SAME loaded page, after <expr> (repeat, max ${MAX_EVAL_STEPS}). Whatever that page load paid for — a vision model coming up, a game booting, a socket connecting — stays up for every step, so an N-part check costs ONE page load instead of N. Each step gets its own in-page budget and its own reported result.`, (val, prev) => [...prev, val], [])
|
|
384
384
|
.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. The hosted URL has permissive CORS, so `new Image(); img.crossOrigin="anonymous"; img.src=fixtureUrl` decodes untainted for a canvas/pixel read - this is the UPLOAD analog of --camera: feed a known photo/video to a file-upload vision app (web-vision-detect) and run its detector, no need to deploy a throwaway test asset into the app. Repeat for several files (single-value so it never swallows the inline <expr>).', (val, prev) => [...prev, val], [])
|
|
385
385
|
.option('--reload <expr>', 'After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload: seed/assert state with <expr>, then assert the restored UI here.')
|
|
@@ -426,7 +426,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
426
426
|
}
|
|
427
427
|
catch {
|
|
428
428
|
pageEvalCommand.error(opts.file === '-'
|
|
429
|
-
? 'error: --file - reads the script from stdin, but stdin was empty/unreadable
|
|
429
|
+
? 'error: --file - reads the script from stdin, but stdin was empty/unreadable - pipe it in, e.g. gipity page eval "<url>" --file - <<\'EOF\' ... EOF'
|
|
430
430
|
: `error: Cannot read file: ${opts.file}`);
|
|
431
431
|
}
|
|
432
432
|
}
|
|
@@ -540,12 +540,12 @@ export const pageEvalCommand = new Command('eval')
|
|
|
540
540
|
projectGuid = config.projectGuid;
|
|
541
541
|
}
|
|
542
542
|
if (opts.camera) {
|
|
543
|
-
console.log(muted(`Hosting camera feed ${opts.camera}
|
|
543
|
+
console.log(muted(`Hosting camera feed ${opts.camera}...`));
|
|
544
544
|
camera = await uploadCameraFeed(projectGuid, opts.camera);
|
|
545
545
|
}
|
|
546
546
|
if (fixturePaths.length) {
|
|
547
547
|
for (const p of fixturePaths) {
|
|
548
|
-
console.log(muted(`Hosting fixture ${p}
|
|
548
|
+
console.log(muted(`Hosting fixture ${p}...`));
|
|
549
549
|
hosted.push(await uploadPublicFixture(projectGuid, p));
|
|
550
550
|
}
|
|
551
551
|
const map = {};
|
|
@@ -30,9 +30,6 @@ function looksLikeHtml(body) {
|
|
|
30
30
|
const head = body.slice(0, 300).toLowerCase();
|
|
31
31
|
return /<!doctype html|<html[\s>]/.test(head);
|
|
32
32
|
}
|
|
33
|
-
function sha256(s) {
|
|
34
|
-
return createHash('sha256').update(s).digest('hex');
|
|
35
|
-
}
|
|
36
33
|
function baseWithSlash(url) {
|
|
37
34
|
return url.endsWith('/') ? url : url + '/';
|
|
38
35
|
}
|
|
@@ -43,8 +40,16 @@ function resolveUrl(base, path) {
|
|
|
43
40
|
async function fetchRaw(url) {
|
|
44
41
|
try {
|
|
45
42
|
const res = await fetch(url, { redirect: 'follow' });
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
// Read raw bytes, not text(): decoding binary as UTF-8 turns every invalid
|
|
44
|
+
// byte into a 3-byte replacement char, inflating the measured size ~1.7x.
|
|
45
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
46
|
+
return {
|
|
47
|
+
status: res.status,
|
|
48
|
+
contentType: res.headers.get('content-type'),
|
|
49
|
+
body: buf.toString('utf-8'),
|
|
50
|
+
bytes: buf.length,
|
|
51
|
+
sha256: createHash('sha256').update(buf).digest('hex'),
|
|
52
|
+
};
|
|
48
53
|
}
|
|
49
54
|
catch {
|
|
50
55
|
return null; // DNS failure, connection refused, etc.
|
|
@@ -59,21 +64,20 @@ async function probeShell(base) {
|
|
|
59
64
|
return {
|
|
60
65
|
served: r.status >= 200 && r.status < 300,
|
|
61
66
|
status: r.status,
|
|
62
|
-
sha256:
|
|
67
|
+
sha256: r.sha256,
|
|
63
68
|
isHtml: looksLikeHtml(r.body),
|
|
64
69
|
};
|
|
65
70
|
}
|
|
66
71
|
function classify(path, r, shell) {
|
|
67
72
|
if (!r)
|
|
68
73
|
return { status: null, contentType: null, bytes: 0, verdict: 'MISSING', detail: 'fetch failed (host unreachable)' };
|
|
69
|
-
const
|
|
70
|
-
const base = { status: r.status, contentType: r.contentType, bytes };
|
|
74
|
+
const base = { status: r.status, contentType: r.contentType, bytes: r.bytes };
|
|
71
75
|
// Honest 404/5xx - the file simply isn't there.
|
|
72
76
|
if (r.status >= 400)
|
|
73
77
|
return { ...base, verdict: 'MISSING', detail: `HTTP ${r.status}` };
|
|
74
78
|
const expect = expectedType(path);
|
|
75
79
|
// 1) Served the catch-all shell verbatim → 200, but the file isn't deployed.
|
|
76
|
-
if (shell.served && shell.sha256 &&
|
|
80
|
+
if (shell.served && shell.sha256 && r.sha256 === shell.sha256) {
|
|
77
81
|
return { ...base, verdict: 'MISSING', detail: `HTTP ${r.status} but served the SPA shell (file not deployed)` };
|
|
78
82
|
}
|
|
79
83
|
// 2) Asked for a non-HTML asset but got an HTML body → a per-path shell variant.
|
|
@@ -92,7 +96,7 @@ const TAG = {
|
|
|
92
96
|
'WRONG-TYPE': warning('WRONG-TYPE'),
|
|
93
97
|
};
|
|
94
98
|
export const pageFetchCommand = new Command('fetch')
|
|
95
|
-
.description('Verify deployed non-rendered files (llms.txt, AGENTS.md, robots.txt, served JSON
|
|
99
|
+
.description('Verify deployed non-rendered files (llms.txt, AGENTS.md, robots.txt, served JSON...) really exist - catches the static-host trap where a missing file returns 200 with the SPA shell instead of a 404')
|
|
96
100
|
.argument('<url>', 'Deployed app base URL; file paths resolve relative to it')
|
|
97
101
|
.argument('<paths...>', 'One or more file paths to verify, e.g. llms.txt AGENTS.md robots.txt')
|
|
98
102
|
.option('--json', 'Output as JSON')
|
|
@@ -26,10 +26,10 @@ function shortUrl(url, truncate = true, maxLen = 100) {
|
|
|
26
26
|
}
|
|
27
27
|
if (!truncate || result.length <= maxLen)
|
|
28
28
|
return result;
|
|
29
|
-
const keep = maxLen -
|
|
29
|
+
const keep = maxLen - 3;
|
|
30
30
|
const headLen = Math.ceil(keep / 2);
|
|
31
31
|
const tailLen = Math.floor(keep / 2);
|
|
32
|
-
return result.slice(0, headLen) + '
|
|
32
|
+
return result.slice(0, headLen) + '...' + result.slice(-tailLen);
|
|
33
33
|
}
|
|
34
34
|
export const pageInspectCommand = new Command('inspect')
|
|
35
35
|
.description('Inspect a web page (console, failed resources, timing, layout overflow). ONE passive load - to verify realtime/presence across concurrent clients use `page test --observe`')
|
|
@@ -235,8 +235,8 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
235
235
|
.option('--wait <ms>', `Sleep this many ms after DOMContentLoaded before capturing (default 1000, max ${MAX_POST_LOAD_DELAY_MS}). With --camera it defaults to ${CAMERA_DEFAULT_DELAY_MS} so the vision model is up. Same flag as page eval/inspect.`)
|
|
236
236
|
.option('--wait-for <selector>', `Wait until this CSS selector appears before capturing, then capture (deterministic - beats guessing a --wait). Same flag as page eval/inspect. Gate on what you are photographing ('[data-vision="ready"]', '#verdict:not(:empty)'). Max ${WAIT_FOR_MAX_MS}ms (--wait-timeout).`)
|
|
237
237
|
.option('--wait-timeout <ms>', `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`)
|
|
238
|
-
.option('--action <js>', 'Run JS in the page before capturing
|
|
239
|
-
.option('--file <path>', 'Read the pre-capture script from a file instead of inline --action (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<\'EOF\'
|
|
238
|
+
.option('--action <js>', 'Run JS in the page before capturing - e.g. click a button to enter a state ("document.getElementById(\'play\').click()"). Runs as an async function body, so const/await and app-relative import(\'./...\') work. Runs after the post-load delay, then settles again before the shot. If it throws, the capture still happens and the failure is reported.')
|
|
239
|
+
.option('--file <path>', 'Read the pre-capture script from a file instead of inline --action (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<\'EOF\' ... EOF) with no tmp file. For a multi-step driver - click through a flow, wait for it to render - then capture. Same async-function-body semantics as --action; same --file flag as page eval.')
|
|
240
240
|
.option('--full', 'Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.')
|
|
241
241
|
.option('-o, --output <file>', 'Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)')
|
|
242
242
|
.option('--no-reload-between', 'Skip reload between viewports (faster, lower fidelity - only safe for static pages)')
|
|
@@ -283,7 +283,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
283
283
|
}
|
|
284
284
|
catch {
|
|
285
285
|
throw new Error(opts.file === '-'
|
|
286
|
-
? "--file - reads the pre-capture script from stdin, but stdin was empty/unreadable
|
|
286
|
+
? "--file - reads the pre-capture script from stdin, but stdin was empty/unreadable - pipe it in, e.g. gipity page screenshot \"<url>\" --file - <<'EOF' ... EOF"
|
|
287
287
|
: `Cannot read file: ${opts.file}`);
|
|
288
288
|
}
|
|
289
289
|
}
|
|
@@ -372,7 +372,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
372
372
|
assertCameraFile(opts.camera);
|
|
373
373
|
if (!projectGuid)
|
|
374
374
|
throw new Error('--camera needs a linked project (the frame is hosted for the browser to fetch) — run `gipity link` first.');
|
|
375
|
-
console.log(muted(`Hosting camera feed ${opts.camera}
|
|
375
|
+
console.log(muted(`Hosting camera feed ${opts.camera}...`));
|
|
376
376
|
camera = await uploadCameraFeed(projectGuid, opts.camera);
|
|
377
377
|
}
|
|
378
378
|
// The pre-capture script the server runs after the post-load delay: the
|
|
@@ -407,7 +407,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
407
407
|
try {
|
|
408
408
|
entries = opts.json
|
|
409
409
|
? await doShoot()
|
|
410
|
-
: await withSpinner('Capturing
|
|
410
|
+
: await withSpinner('Capturing...', doShoot, { done: null });
|
|
411
411
|
}
|
|
412
412
|
catch (err) {
|
|
413
413
|
// The sandbox budget covers the WHOLE capture — load, --action, settle,
|
|
@@ -268,7 +268,7 @@ export const pageTestCommand = new Command('test')
|
|
|
268
268
|
// Interactive mode (--observe drives it):
|
|
269
269
|
.option('--observe <expr>', 'Interactive: JS expression sampled in each client to read shared state (e.g. presence count). Switches on interactive mode.')
|
|
270
270
|
.option('--action <expr>', 'Interactive: one-time JS run in each client before observing (e.g. fill a name + submit). {{label}}/{{i}} are substituted per client.')
|
|
271
|
-
.option('--labels <csv>', 'Per-client labels substituted for {{label}} in the URL/--action/--observe (default client-0, client-1,
|
|
271
|
+
.option('--labels <csv>', 'Per-client labels substituted for {{label}} in the URL/--action/--observe (default client-0, client-1, ...)')
|
|
272
272
|
.option('--hold <ms>', `Interactive: total observe window per client (${MIN_HOLD_MS}-${MAX_HOLD_MS}ms)`, '8000')
|
|
273
273
|
.option('--samples <k>', 'Interactive: number of observations across the hold window (2-30)', '6')
|
|
274
274
|
.option('--wait-for <selector>', 'Interactive: wait for this CSS selector before running --action (deterministic readiness gate)')
|
package/dist/commands/remove.js
CHANGED
|
@@ -22,7 +22,7 @@ export const removeCommand = new Command('remove')
|
|
|
22
22
|
const doRemove = () => post(`/projects/${config.projectGuid}/remove`, { name: kit });
|
|
23
23
|
const res = opts.json
|
|
24
24
|
? await doRemove()
|
|
25
|
-
: await withSpinner('Removing
|
|
25
|
+
: await withSpinner('Removing...', doRemove, { done: null });
|
|
26
26
|
const data = res.data;
|
|
27
27
|
// Pull the kit's deletions locally. Whitelist ONLY the kit's own removed files
|
|
28
28
|
// so they bypass the bulk-delete guard (the removal is explicit), while any
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -464,7 +464,7 @@ GCC/Rust).
|
|
|
464
464
|
});
|
|
465
465
|
const res = opts.json
|
|
466
466
|
? await doRun()
|
|
467
|
-
: await withSpinner('Running in sandbox
|
|
467
|
+
: await withSpinner('Running in sandbox...', doRun, { done: null });
|
|
468
468
|
// Pull sandbox-written outputs down to the local cwd automatically. The
|
|
469
469
|
// server has already mirrored them into the project (VFS) and handed back
|
|
470
470
|
// the exact list, so honoring it here means files land locally without a
|
package/dist/commands/save.js
CHANGED
|
@@ -52,7 +52,7 @@ export const saveCommand = new Command('save')
|
|
|
52
52
|
const doExport = () => downloadWithHeaders(`/projects/${config.projectGuid}/export`);
|
|
53
53
|
const { buffer, headers } = opts.json
|
|
54
54
|
? await doExport()
|
|
55
|
-
: await withSpinner('Exporting
|
|
55
|
+
: await withSpinner('Exporting...', doExport, { done: null });
|
|
56
56
|
const filename = dispositionFilename(headers) ?? `${config.projectSlug}.gip`;
|
|
57
57
|
let dest = path.resolve(output ?? filename);
|
|
58
58
|
if (output && fs.existsSync(dest) && fs.statSync(dest).isDirectory()) {
|
package/dist/commands/secrets.js
CHANGED
|
@@ -34,7 +34,7 @@ secretsCommand
|
|
|
34
34
|
}
|
|
35
35
|
console.log(bold(`${res.data.length} ${scope} secret${res.data.length === 1 ? '' : 's'}:`));
|
|
36
36
|
for (const s of res.data) {
|
|
37
|
-
const masked = s.preview ? muted(
|
|
37
|
+
const masked = s.preview ? muted(`...${s.preview}`) : muted('(hidden)');
|
|
38
38
|
console.log(` ${s.name} ${masked} ${muted(`updated ${new Date(s.updated_at).toLocaleDateString()}`)}`);
|
|
39
39
|
}
|
|
40
40
|
}));
|
package/dist/commands/service.js
CHANGED
|
@@ -21,7 +21,7 @@ const SERVICES = [
|
|
|
21
21
|
];
|
|
22
22
|
export const serviceCommand = new Command('service')
|
|
23
23
|
.description('Call an app service (llm, tts, image, transcribe, ...)')
|
|
24
|
-
.addHelpText('after', '\nCall a Gipity app service: LLM, image, music, TTS, and more
|
|
24
|
+
.addHelpText('after', '\nCall a Gipity app service: LLM, image, music, TTS, and more.\nPer-service docs: gipity skill read app-llm | app-tts | app-image | app-video | app-audio (transcribe, sound, music) | app-location');
|
|
25
25
|
serviceCommand
|
|
26
26
|
.command('list')
|
|
27
27
|
.description('List callable app services')
|
package/dist/commands/setup.js
CHANGED
|
@@ -19,8 +19,7 @@ import { Command } from 'commander';
|
|
|
19
19
|
import { getAuth, sessionExpired, refreshTokenIfNeeded } from '../auth.js';
|
|
20
20
|
import { interactiveLogin } from '../login-flow.js';
|
|
21
21
|
import { runRelaySetup } from '../relay/onboarding.js';
|
|
22
|
-
import
|
|
23
|
-
import { bold, brand, success, muted, error as clrError } from '../colors.js';
|
|
22
|
+
import { bold, muted, error as clrError } from '../colors.js';
|
|
24
23
|
export const connectCommand = new Command('connect')
|
|
25
24
|
.alias('setup') // legacy name, kept working but not advertised
|
|
26
25
|
.description('Connect this computer to gipity.ai so the web CLI can drive it (no project, no launch)')
|
|
@@ -46,11 +45,10 @@ export const connectCommand = new Command('connect')
|
|
|
46
45
|
console.log('');
|
|
47
46
|
// ── Step 2: Relay setup (always run — the user asked for it) ───────
|
|
48
47
|
const enabled = await runRelaySetup({ mode: 'run-now' });
|
|
49
|
-
// ── Step 3: Done. No project, no Claude Code launch.
|
|
48
|
+
// ── Step 3: Done. No project, no Claude Code launch. The shared flow
|
|
49
|
+
// already printed its own "Done!" line - just add the manage hint. ──
|
|
50
50
|
if (enabled) {
|
|
51
|
-
|
|
52
|
-
console.log(` ${success('Done')} — your relay ${running ? 'is running in the background' : 'is set up'} and will start with your computer.`);
|
|
53
|
-
console.log(` ${muted('Open')} ${brand('gipity.ai')} ${muted('and start a chat to drive your coding agent here. Manage it with `gipity relay status`.')}`);
|
|
51
|
+
console.log(` ${muted('Manage it anytime with `gipity relay status`.')}`);
|
|
54
52
|
}
|
|
55
53
|
else {
|
|
56
54
|
console.log(` ${muted('No relay set up. Run `gipity connect` again anytime, or `gipity build` to start building.')}`);
|