gipity 1.0.422 → 1.0.425
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 -3
- package/dist/commands/add.js +14 -3
- package/dist/commands/bug.js +21 -1
- package/dist/commands/claude.js +20 -2
- package/dist/commands/deploy.js +29 -1
- package/dist/commands/fn.js +21 -2
- package/dist/commands/page-eval.js +87 -7
- package/dist/commands/page-inspect.js +201 -186
- package/dist/commands/page-screenshot.js +27 -2
- package/dist/commands/page.js +1 -1
- package/dist/commands/push.js +12 -7
- package/dist/commands/sandbox.js +76 -14
- package/dist/commands/test.js +52 -3
- package/dist/commands/workflow.js +24 -16
- package/dist/helpers/sync.js +8 -1
- package/dist/index.js +20126 -386
- package/dist/knowledge.js +7 -5
- package/dist/sync.js +46 -3
- package/dist/trace.js +69 -0
- package/dist/updater/check.js +159 -84
- package/dist/updater/shim.js +203 -70
- package/package.json +5 -3
package/dist/api.js
CHANGED
|
@@ -353,12 +353,14 @@ export async function getAccountSlug() {
|
|
|
353
353
|
accountSlugCache = res.data.accountSlug;
|
|
354
354
|
return accountSlugCache;
|
|
355
355
|
}
|
|
356
|
-
/** Unauthenticated request (for login/verify
|
|
357
|
-
|
|
356
|
+
/** Unauthenticated request (for login/verify, and anonymous-visitor calls like
|
|
357
|
+
* `fn call --anon`). extraHeaders lets callers attach non-auth headers such as
|
|
358
|
+
* X-App-Token; no Authorization header is ever sent. */
|
|
359
|
+
export async function publicPost(path, body, extraHeaders) {
|
|
358
360
|
const url = `${baseUrl()}${path}`;
|
|
359
361
|
const res = await fetch(url, {
|
|
360
362
|
method: 'POST',
|
|
361
|
-
headers: { ...clientHeaders(), 'Content-Type': 'application/json' },
|
|
363
|
+
headers: { ...clientHeaders(), 'Content-Type': 'application/json', ...extraHeaders },
|
|
362
364
|
body: JSON.stringify(body),
|
|
363
365
|
});
|
|
364
366
|
if (!res.ok) {
|
package/dist/commands/add.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from 'commander';
|
|
|
5
5
|
import { post } from '../api.js';
|
|
6
6
|
import { requireConfig } from '../config.js';
|
|
7
7
|
import { sync } from '../sync.js';
|
|
8
|
-
import { success, muted, bold, warning } from '../colors.js';
|
|
8
|
+
import { success, muted, bold, warning, error as clrError } from '../colors.js';
|
|
9
9
|
import { run } from '../helpers/index.js';
|
|
10
10
|
import { createProgressReporter, withSpinner } from '../progress.js';
|
|
11
11
|
const STARTERS = [
|
|
@@ -164,9 +164,14 @@ export const addCommand = new Command('add')
|
|
|
164
164
|
}
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
|
-
// No name
|
|
167
|
+
// No name is a usage error, not a help request: error + help on STDERR and
|
|
168
|
+
// a nonzero exit. (It used to outputHelp() to stdout and exit 0, which an
|
|
169
|
+
// agent piping through `tail` reads as a successful add that printed
|
|
170
|
+
// help-shaped output.)
|
|
168
171
|
if (!name) {
|
|
169
|
-
|
|
172
|
+
console.error(clrError('Missing template or kit name. Usage: gipity add <name> (see the catalog below, or `gipity add --list`)'));
|
|
173
|
+
command.outputHelp({ error: true });
|
|
174
|
+
process.exitCode = 1;
|
|
170
175
|
return;
|
|
171
176
|
}
|
|
172
177
|
const config = requireConfig();
|
|
@@ -201,6 +206,12 @@ export const addCommand = new Command('add')
|
|
|
201
206
|
}
|
|
202
207
|
// The server runs the whole install pipeline before responding; animate the
|
|
203
208
|
// wait, then clear the spinner so the installed-files list is the result.
|
|
209
|
+
// The spinner is TTY-only, so piped/agent transcripts would otherwise show
|
|
210
|
+
// a silent multi-second gap (favicon generation on a cold cache can take
|
|
211
|
+
// ~10s, cli#117) - leave one stderr marker so the wait reads as work.
|
|
212
|
+
if (!opts.json && !process.stdout.isTTY) {
|
|
213
|
+
console.error(muted('Installing (server writes files + generates favicons; first add for a title can take ~10s)...'));
|
|
214
|
+
}
|
|
204
215
|
const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
|
|
205
216
|
const res = opts.json
|
|
206
217
|
? await doAdd()
|
package/dist/commands/bug.js
CHANGED
|
@@ -15,7 +15,9 @@ export const bugCommand = new Command('bug')
|
|
|
15
15
|
`\nso the team can triage it into a fix.\n` +
|
|
16
16
|
`\nCategories: ${CATEGORIES.join(', ')}` +
|
|
17
17
|
`\nSeverity: ${SEVERITY_HINT}` +
|
|
18
|
-
`\nNever include PII or user data — describe the platform problem in the abstract.`
|
|
18
|
+
`\nNever include PII or user data — describe the platform problem in the abstract.` +
|
|
19
|
+
`\nFiled one by mistake? Withdraw it yourself with \`gipity bug retract <id>\`` +
|
|
20
|
+
`\n(works until a human picks it up) — don't file a second report asking for a close.`);
|
|
19
21
|
bugCommand
|
|
20
22
|
.command('report')
|
|
21
23
|
.description('File a bug / friction report about the Gipity platform')
|
|
@@ -43,6 +45,24 @@ bugCommand
|
|
|
43
45
|
}
|
|
44
46
|
console.log(success(`✓ Bug report filed (${res.data.report_guid}) — queued for triage.`));
|
|
45
47
|
}));
|
|
48
|
+
bugCommand
|
|
49
|
+
.command('retract <id>')
|
|
50
|
+
.description('Withdraw a bug report you filed (e.g. it was your own mistake)')
|
|
51
|
+
.option('--reason <text>', 'Short note for triage on why you are retracting')
|
|
52
|
+
.option('--project <guid-or-slug>', 'Resolve auth against a specific project instead of cwd / Home')
|
|
53
|
+
.option('--json', 'Output raw JSON')
|
|
54
|
+
.addHelpText('after', `\nOnly works on your own reports, and only while they are still queued` +
|
|
55
|
+
`\n(status new/triaged). Once a report is filed/fixed/dismissed, a human` +
|
|
56
|
+
`\nowns closing it out. Find report ids with \`gipity bug list\`.`)
|
|
57
|
+
.action((id, opts) => run('Bug report', async () => {
|
|
58
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
59
|
+
const res = await post(`/api/${config.projectGuid}/services/bug-report/retract`, { report_guid: id, reason: opts.reason });
|
|
60
|
+
if (opts.json) {
|
|
61
|
+
console.log(JSON.stringify(res.data));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(success(`✓ Bug report ${res.data.report_guid} retracted — it is out of the triage queue.`));
|
|
65
|
+
}));
|
|
46
66
|
bugCommand
|
|
47
67
|
.command('list')
|
|
48
68
|
.description('List the bug / friction reports you have filed')
|
package/dist/commands/claude.js
CHANGED
|
@@ -20,7 +20,25 @@ import { printBanner } from '../banner.js';
|
|
|
20
20
|
import { scanForAdoption, isLikelyEmpty, canAdoptCwd, formatCwdLabel, formatBytes, adoptCurrentDir, ADOPT_THRESHOLDS, } from '../adopt-cwd.js';
|
|
21
21
|
import { isClaudeInstalled, ensureClaudeInstalled, CLAUDE_PACKAGE } from '../claude-setup.js';
|
|
22
22
|
const __clDir = dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
|
|
23
|
+
// Walk up to the nearest gipity package.json instead of a hardcoded '../..':
|
|
24
|
+
// the per-module tsc layout puts this file at dist/commands/, the bundled CLI
|
|
25
|
+
// at dist/, so a fixed depth breaks one of the two layouts.
|
|
26
|
+
function readOwnPkg(fromDir) {
|
|
27
|
+
for (let d = fromDir;; d = dirname(d)) {
|
|
28
|
+
const p = join(d, 'package.json');
|
|
29
|
+
if (existsSync(p)) {
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(readFileSync(p, 'utf-8'));
|
|
32
|
+
if (pkg.name === 'gipity')
|
|
33
|
+
return pkg;
|
|
34
|
+
}
|
|
35
|
+
catch { /* keep walking */ }
|
|
36
|
+
}
|
|
37
|
+
if (dirname(d) === d)
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const __clPkg = readOwnPkg(__clDir);
|
|
24
42
|
/** Report a sync run to the user. Beyond the applied-changes line, this SURFACES
|
|
25
43
|
* sync.errors - a download that came back incomplete (truncated bulk tar, a file
|
|
26
44
|
* that couldn't be refetched) lands here, so we never print "ready" over a
|
|
@@ -292,7 +310,7 @@ export const claudeCommand = new Command('claude')
|
|
|
292
310
|
process.exit(1);
|
|
293
311
|
}
|
|
294
312
|
if (!nonInteractive) {
|
|
295
|
-
printBanner({ version: __clPkg.version, email: auth?.email, cwd: process.cwd() });
|
|
313
|
+
printBanner({ version: __clPkg.version ?? 'unknown', email: auth?.email, cwd: process.cwd() });
|
|
296
314
|
}
|
|
297
315
|
// A GIPITY_TOKEN env token authenticates every request on its own, so the
|
|
298
316
|
// saved session's expiry is irrelevant when one is set — never re-login or
|
package/dist/commands/deploy.js
CHANGED
|
@@ -5,6 +5,7 @@ import { formatSize } from '../utils.js';
|
|
|
5
5
|
import { success, error as clrError, warning, muted, bold, brand } from '../colors.js';
|
|
6
6
|
import { run, syncBeforeAction } from '../helpers/index.js';
|
|
7
7
|
import { withSpinner } from '../progress.js';
|
|
8
|
+
import { inspectPage } from './page-inspect.js';
|
|
8
9
|
// ── Status icons ───────────────────────────────────────────────────────
|
|
9
10
|
function statusIcon(status) {
|
|
10
11
|
if (status === 'ok')
|
|
@@ -26,6 +27,7 @@ export const deployCommand = new Command('deploy')
|
|
|
26
27
|
.option('--optimize', 'Force Vite build optimization on (default for prod; use this to optimize a dev deploy too)')
|
|
27
28
|
.option('--no-optimize', 'Skip build optimization and upload files as-is - the escape hatch for plain-HTML apps whose <script src> tags are not type="module"')
|
|
28
29
|
.option('--json', 'Output as JSON')
|
|
30
|
+
.option('--inspect [path]', 'After a successful deploy, run `page inspect` on the deployed URL (or URL + path) in the same command - one build-loop step instead of two. With --json, emits two JSON lines: deploy, then inspect.')
|
|
29
31
|
.action((target, opts) => run('Deploy', async () => {
|
|
30
32
|
if (target !== 'dev' && target !== 'prod') {
|
|
31
33
|
console.error(clrError('Target must be "dev" or "prod"'));
|
|
@@ -44,8 +46,32 @@ export const deployCommand = new Command('deploy')
|
|
|
44
46
|
? await doDeploy()
|
|
45
47
|
: await withSpinner(`Deploying to ${target}…`, doDeploy, { done: null });
|
|
46
48
|
const d = res.data;
|
|
49
|
+
// Deploy + verify in one command: after a successful deploy, run the
|
|
50
|
+
// same inspect pipeline `page inspect` uses against the live URL. An
|
|
51
|
+
// inspect failure is reported but never turns a successful deploy into
|
|
52
|
+
// a nonzero exit - the deploy DID land.
|
|
53
|
+
const inspectAfter = async () => {
|
|
54
|
+
if (!opts.inspect)
|
|
55
|
+
return;
|
|
56
|
+
if (!d.url) {
|
|
57
|
+
console.error(warning('--inspect skipped: this deploy produced no URL (e.g. --only database)'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const url = typeof opts.inspect === 'string' ? new URL(opts.inspect, d.url).toString() : d.url;
|
|
61
|
+
if (!opts.json)
|
|
62
|
+
console.log('');
|
|
63
|
+
try {
|
|
64
|
+
await inspectPage(url, { json: opts.json });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(warning(`Inspect failed (deploy itself succeeded): ${err?.message ?? err}`));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
47
70
|
if (opts.json) {
|
|
48
71
|
console.log(JSON.stringify(d));
|
|
72
|
+
const failedJson = d.phases?.some(p => p.status === 'failed');
|
|
73
|
+
if (!failedJson)
|
|
74
|
+
await inspectAfter();
|
|
49
75
|
return;
|
|
50
76
|
}
|
|
51
77
|
// Format output
|
|
@@ -54,7 +80,8 @@ export const deployCommand = new Command('deploy')
|
|
|
54
80
|
console.log(muted('─'.repeat(40)));
|
|
55
81
|
if (d.phases && d.phases.length > 0) {
|
|
56
82
|
for (const phase of d.phases) {
|
|
57
|
-
|
|
83
|
+
const ms = phase.elapsedMs != null ? muted(` (${phase.elapsedMs}ms)`) : '';
|
|
84
|
+
console.log(`${statusIcon(phase.status)} ${bold(phase.name)}: ${phase.summary}${ms}`);
|
|
58
85
|
}
|
|
59
86
|
}
|
|
60
87
|
else {
|
|
@@ -101,6 +128,7 @@ export const deployCommand = new Command('deploy')
|
|
|
101
128
|
// has to reconstruct the URL convention or guess a subdomain.
|
|
102
129
|
if (d.url)
|
|
103
130
|
console.log(`${muted('Live:')} ${brand(d.url)}`);
|
|
131
|
+
await inspectAfter();
|
|
104
132
|
}
|
|
105
133
|
}));
|
|
106
134
|
//# sourceMappingURL=deploy.js.map
|
package/dist/commands/fn.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { get, post, del } from '../api.js';
|
|
2
|
+
import { get, post, del, publicPost } from '../api.js';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { error as clrError, bold, muted, success, warning } from '../colors.js';
|
|
5
5
|
import { run, printList, emitField } from '../helpers/index.js';
|
|
@@ -42,17 +42,36 @@ fnCommand
|
|
|
42
42
|
return line;
|
|
43
43
|
});
|
|
44
44
|
}));
|
|
45
|
+
/** Invoke a function exactly like an anonymous visitor: mint the same
|
|
46
|
+
* short-lived public app token the browser SDK uses (best-effort — public
|
|
47
|
+
* functions run with no token at all), POST with no user credentials, and
|
|
48
|
+
* return the standard envelope. A 401 here is the correct answer for an
|
|
49
|
+
* auth-gated function — the server's message is surfaced as-is, with no
|
|
50
|
+
* "run: gipity login" misdirection. */
|
|
51
|
+
async function callAnon(projectGuid, name, body) {
|
|
52
|
+
let appToken;
|
|
53
|
+
try {
|
|
54
|
+
const minted = await publicPost('/api/token', { app: projectGuid });
|
|
55
|
+
appToken = minted.data.token;
|
|
56
|
+
}
|
|
57
|
+
catch { /* public functions work without a token; auth-gated ones will 401 with the real reason */ }
|
|
58
|
+
return publicPost(`/api/${projectGuid}/fn/${encodeURIComponent(name)}`, body, appToken ? { 'X-App-Token': appToken } : undefined);
|
|
59
|
+
}
|
|
45
60
|
fnCommand
|
|
46
61
|
.command('call <name> [body]')
|
|
47
62
|
.description('Call a function')
|
|
48
63
|
.option('--data <json>', 'JSON request body')
|
|
64
|
+
.option('--anon', 'Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account')
|
|
49
65
|
.option('--field <path>', 'Print only this field of the result (dot path, e.g. items.0.short_guid)')
|
|
50
66
|
.option('--json', 'Output as JSON')
|
|
51
67
|
.action((name, bodyArg, opts) => run('Call', async () => {
|
|
52
68
|
const config = requireConfig();
|
|
53
69
|
const raw = bodyArg || opts.data || '{}';
|
|
54
70
|
const body = JSON.parse(raw);
|
|
55
|
-
const
|
|
71
|
+
const path = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
|
|
72
|
+
const res = opts.anon
|
|
73
|
+
? await callAnon(config.projectGuid, name, body)
|
|
74
|
+
: await post(path, body);
|
|
56
75
|
if (opts.field) {
|
|
57
76
|
emitField(res.data, opts.field);
|
|
58
77
|
return;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { Command, Option } from 'commander';
|
|
3
3
|
import { post, get, ApiError } from '../api.js';
|
|
4
|
-
import { brand, bold, muted, warning } from '../colors.js';
|
|
4
|
+
import { brand, bold, muted, warning, success } from '../colors.js';
|
|
5
5
|
import { run } from '../helpers/index.js';
|
|
6
|
+
import { getAuth } from '../auth.js';
|
|
6
7
|
import { resolveProjectContext } from '../config.js';
|
|
7
8
|
import { uploadPublicFixture, deleteFixture } from '../page-fixtures.js';
|
|
8
9
|
// Shown when an eval runs cleanly but returns nothing serializable. Turns a
|
|
@@ -43,6 +44,26 @@ export function normalizeEvalResult(raw) {
|
|
|
43
44
|
}
|
|
44
45
|
return { result: raw, noValue: false };
|
|
45
46
|
}
|
|
47
|
+
// A one-line inline expr is worth echoing back — it's the thing you're asserting
|
|
48
|
+
// on. A multi-line driver script is not: echoing 30 lines of the caller's own
|
|
49
|
+
// source above the result buries the value, and an echo that lands right before
|
|
50
|
+
// `(empty result)` reads like the parser choked on the script rather than like
|
|
51
|
+
// the script returned nothing. Collapse anything bigger than a single short line
|
|
52
|
+
// to its first line plus a shape summary.
|
|
53
|
+
const EXPR_ECHO_MAX_CHARS = 120;
|
|
54
|
+
export function summarizeExpr(expr) {
|
|
55
|
+
const lines = expr.split('\n');
|
|
56
|
+
const meaningful = lines.filter((l) => l.trim() !== '');
|
|
57
|
+
const oneLine = meaningful.length <= 1;
|
|
58
|
+
if (oneLine && expr.trim().length <= EXPR_ECHO_MAX_CHARS)
|
|
59
|
+
return expr.trim();
|
|
60
|
+
const first = (meaningful[0] ?? '').trim();
|
|
61
|
+
const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS - 1)}…` : first;
|
|
62
|
+
const shape = oneLine
|
|
63
|
+
? `(${expr.trim().length} chars)`
|
|
64
|
+
: `(+${meaningful.length - 1} more ${meaningful.length - 1 === 1 ? 'line' : 'lines'}, ${expr.trim().length} chars)`;
|
|
65
|
+
return `${head} ${shape}`;
|
|
66
|
+
}
|
|
46
67
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
47
68
|
// A single browser session is held open synchronously for the whole --wait, so
|
|
48
69
|
// the server caps it at the gateway idle timeout. Longer is impossible in one
|
|
@@ -140,11 +161,13 @@ const JS_DECOY_FLAGS = ['--js', '--javascript', '--script', '--code', '--expr',
|
|
|
140
161
|
// tools, undo/redo, transforms) and `return` a JSON-serializable result —
|
|
141
162
|
// no /tmp + shell command-substitution harness needed.
|
|
142
163
|
export const pageEvalCommand = new Command('eval')
|
|
143
|
-
.description('Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script)')
|
|
164
|
+
.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')
|
|
144
165
|
.argument('<url>', 'URL to load')
|
|
145
|
-
.argument('[expr]', 'JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file.')
|
|
146
|
-
.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.')
|
|
166
|
+
.argument('[expr]', 'JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file. Time budget: the body has ~20s to finish after page load - keep driver scripts within it.')
|
|
167
|
+
.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. Same ~20s post-load budget as <expr>.')
|
|
147
168
|
.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], [])
|
|
169
|
+
.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.')
|
|
170
|
+
.option('--reload-file <path>', 'Read the post-reload expression from a file instead of inline --reload (mutually exclusive)')
|
|
148
171
|
.option('--wait <ms>', 'Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000)', '500')
|
|
149
172
|
.option('--wait-for <selector>', 'Wait until this CSS selector appears before evaluating (deterministic; replaces --wait)')
|
|
150
173
|
.option('--wait-timeout <ms>', 'Max ms to wait for --wait-for before giving up', '5000')
|
|
@@ -167,6 +190,16 @@ export const pageEvalCommand = new Command('eval')
|
|
|
167
190
|
if (exprArg === undefined && !opts.file) {
|
|
168
191
|
pageEvalCommand.error('error: Provide an inline <expr> arg or --file <path>');
|
|
169
192
|
}
|
|
193
|
+
// Catch a swapped <url>/<expr> locally: the server's bare "Invalid URL"
|
|
194
|
+
// names neither the bad argument nor the expected order, so it reads like
|
|
195
|
+
// the page failed to evaluate rather than like a mis-invocation.
|
|
196
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
197
|
+
const flatUrl = url.replace(/\s+/g, ' ').trim();
|
|
198
|
+
const shownUrl = flatUrl.length > 60 ? `${flatUrl.slice(0, 57)}...` : flatUrl;
|
|
199
|
+
pageEvalCommand.error(exprArg !== undefined && /^https?:\/\//i.test(exprArg)
|
|
200
|
+
? `error: arguments are swapped — the URL is the FIRST positional: gipity page eval "${exprArg}" '<expr>'`
|
|
201
|
+
: `error: <url> must be an absolute http(s) URL (got: "${shownUrl}") — usage: gipity page eval <url> [expr]`);
|
|
202
|
+
}
|
|
170
203
|
let expr = exprArg;
|
|
171
204
|
if (opts.file) {
|
|
172
205
|
try {
|
|
@@ -176,6 +209,20 @@ export const pageEvalCommand = new Command('eval')
|
|
|
176
209
|
pageEvalCommand.error(`error: Cannot read file: ${opts.file}`);
|
|
177
210
|
}
|
|
178
211
|
}
|
|
212
|
+
// Post-reload expression: inline --reload or --reload-file, same shape
|
|
213
|
+
// rules as the primary <expr>/--file pair.
|
|
214
|
+
if (opts.reload !== undefined && opts.reloadFile) {
|
|
215
|
+
pageEvalCommand.error('error: Pass either --reload <expr> or --reload-file <path>, not both');
|
|
216
|
+
}
|
|
217
|
+
let reloadExpr = opts.reload;
|
|
218
|
+
if (opts.reloadFile) {
|
|
219
|
+
try {
|
|
220
|
+
reloadExpr = readFileSync(opts.reloadFile, 'utf8');
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
pageEvalCommand.error(`error: Cannot read file: ${opts.reloadFile}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
179
226
|
const waitMs = capWaitMs(opts.wait, url);
|
|
180
227
|
const parsedTimeout = parseInt(opts.waitTimeout, 10);
|
|
181
228
|
const waitForTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout >= 0 ? parsedTimeout : 5000;
|
|
@@ -204,31 +251,54 @@ export const pageEvalCommand = new Command('eval')
|
|
|
204
251
|
}
|
|
205
252
|
const kickoff = await post('/tools/browser/eval', {
|
|
206
253
|
url, expr: sentExpr, waitMs,
|
|
254
|
+
reloadExpr,
|
|
207
255
|
waitForSelector: opts.waitFor || undefined,
|
|
208
256
|
waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : undefined,
|
|
209
257
|
auth: opts.auth || undefined,
|
|
210
258
|
});
|
|
211
|
-
|
|
259
|
+
// The reload leg re-runs the settle before its eval — budget for both.
|
|
260
|
+
const d = await pollEvalResult(kickoff.data.evalJobId, reloadExpr !== undefined ? waitMs * 2 : waitMs);
|
|
212
261
|
const { result, noValue } = normalizeEvalResult(d.result);
|
|
262
|
+
const reload = d.reloadResult !== undefined ? normalizeEvalResult(d.reloadResult) : undefined;
|
|
213
263
|
const execTimeout = evalExecTimeoutMessage(d.result);
|
|
214
264
|
if (execTimeout)
|
|
215
265
|
throw new Error(execTimeout);
|
|
216
266
|
if (opts.json) {
|
|
217
|
-
console.log(JSON.stringify(
|
|
267
|
+
console.log(JSON.stringify({
|
|
268
|
+
...d, result,
|
|
269
|
+
...(reload ? { reloadResult: reload.result } : {}),
|
|
270
|
+
...(noValue ? { hint: EVAL_NO_VALUE_HINT } : {}),
|
|
271
|
+
}));
|
|
218
272
|
return;
|
|
219
273
|
}
|
|
220
274
|
console.log(`${brand('Eval')} ${bold(d.url || url)}`);
|
|
221
275
|
if (d.navigationIncomplete) {
|
|
222
276
|
console.log(`${warning('⚠ Navigation incomplete:')} ${d.note || 'page did not reach full load'}`);
|
|
223
277
|
}
|
|
278
|
+
// Auth state: without this line an agent can't distinguish "signed-in
|
|
279
|
+
// eval" from "--auth silently no-op'd against the anonymous page".
|
|
280
|
+
if (d.auth?.requested) {
|
|
281
|
+
const who = getAuth()?.email;
|
|
282
|
+
console.log(d.auth.established
|
|
283
|
+
? `${muted('Auth:')} ${success('session established')}${who ? muted(` as ${who}`) : ''} ${muted('(what the page renders with it is app-defined)')}`
|
|
284
|
+
: `${warning('Auth: session NOT established')}${d.auth.detail ? ` — ${d.auth.detail}` : ''} ${muted('(this is the anonymous view)')}`);
|
|
285
|
+
}
|
|
224
286
|
if (hosted.length)
|
|
225
287
|
console.log(`${muted('Fixtures:')} ${hosted.map((h) => h.name).join(', ')}`);
|
|
226
|
-
console.log(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${expr}`);
|
|
288
|
+
console.log(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${summarizeExpr(expr)}`);
|
|
227
289
|
console.log(`\n${result.trim() ? result : muted('(empty result)')}`);
|
|
228
290
|
if (noValue)
|
|
229
291
|
console.log(muted(`\n${EVAL_NO_VALUE_HINT}`));
|
|
230
292
|
if (d.truncated)
|
|
231
293
|
console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
|
|
294
|
+
if (reload) {
|
|
295
|
+
console.log(`\n${bold('After reload')} ${muted('(page reloaded in place — storage preserved)')}`);
|
|
296
|
+
console.log(reload.result.trim() ? reload.result : muted('(empty result)'));
|
|
297
|
+
if (reload.noValue)
|
|
298
|
+
console.log(muted(EVAL_NO_VALUE_HINT));
|
|
299
|
+
if (d.reloadTruncated)
|
|
300
|
+
console.log(muted('(reload result truncated to fit context - narrow the expression for the full value)'));
|
|
301
|
+
}
|
|
232
302
|
}
|
|
233
303
|
finally {
|
|
234
304
|
for (const h of hosted) {
|
|
@@ -261,6 +331,16 @@ Examples:
|
|
|
261
331
|
# fetch-able 'fixtureUrl', runs the eval, then deletes the hosted copy:
|
|
262
332
|
gipity page eval "https://dev.gipity.ai/me/app/" --fixture ./sample.mp3 \\
|
|
263
333
|
"(async()=>{ const b = await fetch(fixtureUrl).then(r=>r.arrayBuffer()); return window.App.parseId3(b); })()"
|
|
334
|
+
# Verify persisted state survives a reload (localStorage/sessionStorage kept):
|
|
335
|
+
# run <expr>, reload the page in place, then run the --reload expression:
|
|
336
|
+
gipity page eval "https://dev.gipity.ai/me/app/" \\
|
|
337
|
+
"localStorage.setItem('todo','milk'); document.title" \\
|
|
338
|
+
--reload "({ restored: localStorage.getItem('todo'), heading: document.querySelector('h1')?.textContent })"
|
|
339
|
+
|
|
340
|
+
Module resolution: dynamic import() specifiers starting with ./ or ../ resolve
|
|
341
|
+
against the PAGE URL, so import('./packages/i18n/index.js') loads the app's own
|
|
342
|
+
module without hand-building the deployed /account/project/ path. Absolute paths
|
|
343
|
+
and full URLs pass through unchanged.
|
|
264
344
|
|
|
265
345
|
The eval body runs under a ~20s in-page execution budget (its own await/setTimeout
|
|
266
346
|
pauses count; --wait only sleeps BEFORE the eval and does not extend it). For a long
|